Adventures in Mastodon Self-Hosting: Migrate to docker

When I first installed mastodon on my little Digital Ocean droplet, I believed that “a docker container is just a lightweight virtual machine”. Given how small my droplet was, I didn’t think it had enough room for any kind of VM, no matter how lightweight. I’ve since learned that docker containers are not virtual machines, though thats a useful approximation when you’re just getting started with them.

Anyway, there’s very very little performance difference between a native install and a container, and they’re much easier to work with.

Here’s my notes on the process of moving an existing installation to docker. Keep in mind this is a single-user instance and I don’t care about downtime.

Check versions

The end goal is that nginx is running natively to front the application, but that redis, postgres, and the mastodon ruby code is all running in containers through docker-compose.

First checked that the databases were on the same major versions as the current docker compose file. I was concerned that a database file copy from an earlier redis version would not work.

redis-server --version
Redis server v=7.0.4 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=58fea801f69a84ba

pg_config --version
PostgreSQL 14.8 (Ubuntu 14.8-1.pgdg22.10+1)

At the time of this migration, the docker-compose file on v4.2.13 was

  • postgres:14-alpine
  • redis:7-alpine

Should be ok to use the redis db dump since its also on major version 7.

Install docker tools

Followed installation instructions: https://docs.docker.com/engine/install/ubuntu/

Made sure the mastodon user had access to docker

groupadd docker
usermod -aG docker mastodon
newgrp docker

Prepare the installation live folder

# as mastodon in live
# docker-compose.yml mounts these directories into the database services
mkdir postgres14
mkdir redis

# make sure the docker container can read & write to the media directories
# I got the 991 user id and group from running "docker compose exec web id" which prints
# what user the mastodon containers are running as. These are not uid or gids on the host
# so they have no names. I forgot this step and had to fix it later -- there were permissions
# errors in the logs from sidekiq trying to write to the media directories.
cd public/system
chown -R 991:991 *

Stop current natively-installed mastodon services

# as root
systemctl stop mastodon-*

Dump and restore postgres database

# as mastodon in ~/live
pg_dump --format=custom --no-acl --no-owner --clean mastodon_production > mastodon_production.dump

# start postgres in docker
docker compose up -d db

# restore database
docker exec -i live-db-1 pg_restore -U postgres -v --clean --no-acl --no-owner -d postgres < mastodon_production.dump

Stop native postgres since we won’t need it anymore

# as root
systemctl stop postgresql

Copy redis database file

# as root
# Get the redis data directory
redis-cli config get dir

# stop redis from running so the db file is consistent
systemctl stop redis

# copy it to the directory that will be mounted into the redis container
cp /var/lib/redis/dump.rdb /home/mastodon/live/redis/

# make sure mastodon can read it
chown mastodon /home/mastodon/live/redis/dump.rdb

# as mastodon in live
# start redis docker container
docker compose up -d redis

# check logs and make sure it started up ok
docker compose logs redis

Update config files

Update the .env.production config file in live

# since its running in the docker compose context, the hostnames of the services
# are just the name of the service as defined in docker-compose.yml

DB_HOST=db
DB_PORT=5432
# this changed from mastodon
DB_NAME=postgres
#this changed from mastodon_production
DB_USER=postgres
DB_PASS=

REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=

Bring up the mastodon ruby services in docker

# as mastodon in live
docker compose up -d

# check web logs
docker compose logs -f web

Update nginx config

In the native installation, nginx looked for static files (javascript, css, uploaded images) right on the disk. Instead, we want the containerized ruby process in the web container to serve files. So we need to update the nginx config to use rails as the reverse proxy target rather than going straight to disk. Update all the try_files directives to look like this:

Update /etc/nginx/sites-available/mastodon to change the try_files directive to try_files $uri @proxy;

# then run this to reload the config
nginx -s reload

Clear browser cache

It should be up and running now, but I was getting resource integrity errors. I could confirm that the js files were being served successfully, but my browser was caching some old data and refusing to execute the javascript. Cleared cache or used private browsing to confirm it was back up and running.

Optional: uninstall old stuff

Now I’m up and running on docker instead of a native installation. I could then start removing unneeded services

# remove native installations of databases and build tools since they ship
# in the container
apt --purge remove postgresql redis-server nodejs
systemctl disable mastodon-web.service
systemctl disable mastodon-streaming.service

# as mastodon in live
# remove the node_modules folder (large!)
rm -rf node_modules
# remove old compiled assets (these are now baked into the container)
rm -rf public/packs/*

I also had a couple maintenance scripts to update; basically just to run docker compose run --rm web instead of calling the rails & rake commands directly.

Next Update

This made my next update to 4.3.0 simple. The docker instructions in the release notes are really easy to follow. There were a few extra steps specific to 4.3.0, but generally its just checking out the next version in git and running docker compose up -d to load the latest containers and start them up. Sometimes I’ll have to run migrations, but I won’t have to do expensive builds anymore.