How I host my software on the web!

Hello there! I thought I would try to summarize what I did to get this very instance of writefreely up and running on my server host!

Be warned, I don't consider this a very simple guide to follow, it's also very opinionated, as it's how I currently like to set up my hosted services.

This is also a living document, as I will try my best to make changes in the event that you or someone else submits a comment on it if something is unclear! Submitting comments on this is done via the Fediverse! (search up the URL to this article on your favourite federated / ActivityPub platform! 🙌)


First I should mention that I have a particular way of setting up my servers, I like to try and containerize as much as possible! (with a few exceptions). This way I can cheaply experiment and host as many things as I want on a single VPS, without any piece of software stepping on the toes of another!

I also like to have my data close to the consumer of that data, so when I pull in a repository of software, for example shimmie2 for my image booru (hosted at, or mastodon (hosted at it lives alongside a data/ directory as shown.

|-- booru
|   |-- data
|   `-- shimmie2
|-- mastodon
|   |-- data
|   `-- mastodon
`-- writefreely
    |-- data
    `-- writefreely

The data/ directory then becomes where i mount my docker volumes. Here is my modified writefreely docker-compose.yml (if you want, you can manually diff this file against the official one.)

version: "3"

    internal: true

    container_name: "writefreely-web"
    build: .
    # image: "writeas/writefreely:latest"

      - ../data/web-keys:/go/keys
      - type: bind
        source: ../data/config.ini
        target: /go/config.ini

      - "internal_writefreely"
      - "external_writefreely"

      - "8080:8080"

      - "writefreely-db"

    restart: unless-stopped

    container_name: "writefreely-db"
    image: "mariadb:latest"

      - "../data/db:/var/lib/mysql/data"

      - "internal_writefreely"

      - MYSQL_DATABASE=writefreely
    restart: unless-stopped

The most notable changes are that all the volumes (where appropriate) have been prefixed with ../data/ so that data stored by the different services is neatly contained within the data/ directory! 🙌

NOTE: I also commented out the image property and chose to instead build the docker image from source. It's not necessary for following along with this write-up to get things to work, so I'll leave out the specifics.

Due to the way that docker volume binds work, you'll have to create the file ../data/config.ini as an empty file in order for docker to mount it.

touch ../data/config.ini

Next, you should now set the ownership of the files under /data to match the user id of the correct owning user inside the relevant docker container. This is important when hosting these docker volumes on a shared filesystem like this.

You can check the current owner/permissions of any file with:

ls -la ../data/config.ini

To set the correct ownerships for what these docker containers expect, run the following:

sudo chown -R 2:2 ../data/config.ini
sudo chown -R 2:2 ../data/web-keys

If web-keys doesn't exist on your filesystem yet you can create an empty directory for it as well. It should get created automatically when running the container via docker-compose. but if you're following this guide verbatim from top to bottom, it might not exist yet here, so run the following mkdir command before trying to change the permissions of it as shown above.

mkdir ../data/web-keys

NOTE: the user id 2 will not make sense for your host operating system! don't worry if it looks like it belongs to some completely unrelated user on the host. Files are mounted verbatim into the docker container, and this includes the file permissions as well.

Now you have some options, you can run the interactive config generator by running the docker container with its entrypoint set to /bin/sh and running commands manually (such as the command cmd/writefreely/writefreely config start). But I think I will just post my (masked) version of the ../data/config.ini file as that seems to be the simplest for now :)

hidden_host          =
port                 = 8080
bind                 =
tls_cert_path        =
tls_key_path         =
autocert             = false
templates_parent_dir =
static_parent_dir    =
pages_parent_dir     =
keys_parent_dir      =
hash_seed            =
gopher_port          = 0

type     = mysql
filename =
username = writefreely
password = CHANGEME
database = writefreely
host     = writefreely-db
port     = 3306
tls      = false

site_name             = write
site_description      =
host                  =
theme                 = write
editor                =
disable_js            = false
webfonts              = true
landing               =
simple_nav            = false
wf_modesty            = false
chorus                = false
forest                = false
disable_drafts        = false
single_user           = false
open_registration     = true
open_deletion         = false
min_username_len      = 3
max_blogs             = 1
federation            = true
public_stats          = true
monetization          = false
notes_only            = false
private               = false
local_timeline        = false
user_invites          =
default_visibility    = unlisted
update_checks         = false
disable_password_auth = false

client_id          =
client_secret      =
team_id            =
callback_proxy     =
callback_proxy_api =

client_id          =
client_secret      =
auth_location      =
token_location     =
inspect_location   =
callback_proxy     =
callback_proxy_api =

client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =

client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =

client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =
token_endpoint     =
inspect_endpoint   =
auth_endpoint      =
scope              =
allow_disconnect   = false
map_user_id        =
map_username       =
map_display_name   =
map_email          =

The only thing that should really need to be different for your instance would be the host variable under [app].

Next, ensure the database exists, and that it has the correct user with the correct password (as described in the configuration above).

docker-compose run --entrypoint /bin/bash writefreely-db
mysql -u root -p
<enter MYSQL_ROOT_PASSWORD from docker-compose.yml>
# GRANT ALL PRIVILEGES ON *.* TO 'writefreely'@'%';

Now, initialize the database.

docker-compose run --entrypoint /bin/sh writefreely-web
cmd/writefreely/writefreely db init

Then, generate the keys.

docker-compose run --entrypoint /bin/sh writefreely-web
cmd/writefreely/writefreely keys generate

Next, generate the CSS files (I don't know why this is not in the “common execution flow” of the application but it should be done regardless!)

cd less/

I use Nginx as the entry point to all services running on my host(s). So for this I follow the reverse proxy guidelines when I generated my config.ini above. My nginx config section is actually identical to the one supplied by the official guide, with the following exception of pointing to my static content.

location ~ ^/(css|img|js|fonts)/ {
    root /MY/PATH/TO/writefreely/writefreely/static;
    # Optionally cache these files in the browser:
    expires 1d;

Lastly, you should be able to run all the relevant containers with the single command:

docker-compose up

If anything went wrong, leave a comment on the fediverse! (or search the official forums)

Wacky words by