Self-hosting mastodon with docker
This is my attempt to document how I deploy my own mastodon instance. My setup is not super straightforward, and this is one of the reasons I decided to write a post about it.
Let me start by listing my requirements:
- Run on my NAS
- Run on docker
- Be publicly accessible (I don’t have static public IP)
- Be under my main domain: loop0.sh
- Avoid active management/moderation
The infrastructure (#1, #2, and #3)
Here is how I have it deployed:
┌──────────┐
│ User │
└────┬─────┘
│ HTTPS
▼
┌────────────────────────────────┐
│ VPS │
│ ┌─────────┐ ┌─────────┐ │
│ │ Traefik │───▶│ Nginx │ │
│ └─────────┘ └────┬────┘ │
│ │ │
│ ┌────▼──────┐ │
│ │ WireGuard │ │
│ │ (server) │ │
│ └─────┬─────┘ │
└────────────────────────┼───────┘
│
┌─────────────────┘
▼
┌──────│──────────────────────────────────┐
│ │ NAS │
│ ┌───│────────────────────────────────┐ │
│ │ │ compose.yml │ │
│ │ └────┐ │ │
│ │ ┌───────────┐◀───────┌─────────┐ │ │
│ │ │ WireGuard │◀────┐ │ Sidekiq │ │ │
│ │ └────▲──────┘ │ └─────────┘ │ │
│ │ │ │ │ │
│ │ ┌────────┐ ┌───────────┐ │ │
│ │ │ Web │ │ Streaming │ │ │
│ │ └────────┘ └───────────┘ │ │
│ │ ┌────────┐ ┌─────────┐ │ │
│ │ │Postgres│ │ Redis │ │ │
│ │ └────────┘ └─────────┘ │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
In this configuration, all inbound requests reach traefik (used for automatic https with letsencrypt), which forwards the traffic to nginx, which is configured as a proxy and forwards the requests to mastodon’s web and streaming containers.
All outbound traffic from the web and sidekiq gets routed through the wireguard container and out through my VPS, so it does not expose my home ip address to the instances it is communicating with.
Note: on the VPS, I have traefik and nginx also running under docker (omitted to simplify)
Even with all those hoops, the responsiveness is pretty good and I have been enjoying having my instance.
Here is my docker compose file that I use to run on the NAS side:
services:
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: wireguard
restart: always
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
dns:
- 1.1.1.1
volumes:
- ./wireguard:/config
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
networks:
- mastodon_external
- mastodon_internal
db:
restart: always
image: postgres:14-alpine
shm_size: 256mb
networks:
- mastodon_internal
healthcheck:
test:
- CMD
- pg_isready
- -U
- postgres
volumes:
- ./postgres:/var/lib/postgresql/data
env_file: env-postgres
redis:
restart: always
image: redis:7-alpine
networks:
- mastodon_internal
healthcheck:
test:
- CMD
- redis-cli
- ping
volumes:
- ./redis:/data
web:
image: ghcr.io/mastodon/mastodon:v4.6
restart: always
env_file: env
command: bundle exec puma -C config/puma.rb
network_mode: service:wireguard
healthcheck:
test:
- CMD-SHELL
- curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' ||
exit 1
depends_on:
- db
- redis
- wireguard
volumes:
- ./mastodon/public/system:/mastodon/public/system:z
streaming:
image: ghcr.io/mastodon/mastodon-streaming:v4.6
restart: always
env_file: env
command: node ./streaming/index.js
network_mode: service:wireguard
healthcheck:
test:
- CMD-SHELL
- curl -s --noproxy localhost localhost:4000/api/v1/streaming/health |
grep -q 'OK' || exit 1
depends_on:
- db
- redis
- wireguard
sidekiq:
image: ghcr.io/mastodon/mastodon:v4.6.0
restart: always
env_file: env
command: bundle exec sidekiq
depends_on:
- db
- redis
- wireguard
network_mode: service:wireguard
volumes:
- ./mastodon/public/system:/mastodon/public/system:z
healthcheck:
test:
- CMD-SHELL
- ps aux | grep '[s]idekiq 8' || false
networks:
mastodon_external: null
mastodon_internal:
internal: true
The domain (#4)
Problem: I wanted my instance to respond at my main domain https://loop0.sh, so my username would be @root@loop0.sh. The problem is that I have my blog running under that domain, so how can I host mastodon on a subdomain, like https://social.loop0.sh but have it respond on the apex?
Solution: mastodon’s doc says you have to redirect requests with the path /.well-known/webfinger to domain running the instance. My blog is a static one, generated by hugo and running with docker compose as well, served with traefik. With that in mind, I had to add the following labels to my compose.ymlfile:
service:
web:
labels:
# WebFinger redirect router
- "traefik.http.routers.loop0-webfinger-router.rule=Host(`loop0.sh`) && PathPrefix(`/.well-known/webfinger`)"
- "traefik.http.routers.loop0-webfinger-router.entrypoints=web,websecure"
- "traefik.http.routers.loop0-webfinger-router.tls=true"
- "traefik.http.routers.loop0-webfinger-router.tls.certresolver=myresolver"
- "traefik.http.routers.loop0-webfinger-router.middlewares=webfinger-redirect"
- "traefik.http.routers.loop0-webfinger-router.service=noop@internal"
# WebFinger redirect middleware (preserves query string via {query})
- "traefik.http.middlewares.webfinger-redirect.redirectregex.regex=^https://loop0\\.sh(/.well-known/webfinger.*)"
- "traefik.http.middlewares.webfinger-redirect.redirectregex.replacement=https://social.loop0.sh$${1}"
- "traefik.http.middlewares.webfinger-redirect.redirectregex.permanent=true"
And have mastodon’s env file configured like the following:
LOCAL_DOMAIN=loop0.sh
WEB_DOMAIN=social.loop0.sh
The maintenance (#5)
I don’t have a lot of time to maintain my stuff and I wanted it to be as hands-off as possible, so I decided that the instance had to federate in an allow-list mode, where only domains I choose to federate with would communicate with my instance, and I also wanted to keep it just for my use. Mastodon has a pretty straightforward way to do this, and I just had to add the following lines to my env file:
SINGLE_USER_MODE=true
AUTHORIZED_FETCH=true
LIMITED_FEDERATION_MODE=true
Now with everything running, I had to export my followers and following from my old instance and generate a list of allowed domains.
Closing words
This setup is working pretty well, and I was able to keep everyone from my old account without any issues. It also gives me peace of mind that I don’t have to actively defederate/block domains.
I omitted a bunch of steps because most of it I just took it from mastodon’s own documentation. If you’re a more experienced mastodon admin and see any issues with my deployment, please let me know so I can fix it! You can find me at @root@loop0.sh on mastodon.
Cheers!