I’ve been using Mastodon for quite a while now. And I really like the idea behind it, especially from a distributed web perspective. Taking decentralization a step further, my next step was to set up an own instance of Mastodon.

In this blog post I’d like to take you on the journey of setting up an own instance of Mastodon using Docker(-Compose) and Traefik v2.9.

Why do I think we need yet another tutorial for this? Well, at first there seem to be not so many tutorials for Traefik v2 around yet. Searching the internet mostly yields Traefik v1 related guides and tutorials. Secondly, there are two things I just couldn’t achieve using the once existing Mastodon docker guide (by the time of writing this guide, Mastodon removed its docker guide completely):

Despite of having a good documentation, there is a design decision I dislike in the Mastodon docker guide. That is they place the nginx reverse-proxy outside of docker hence requiring the administrator to manually setup and configure a separate nginx on her box.

So, this guide goes another way :)

The new docker guide

This guide shows how you can setup your own instance of Mastodon using a single docker-compose file.

In the former Mastodon docker guide and the docker-compose.yml from Mastodons repository they place the nginx reverse-proxy outside of docker hence requiring the administrator to manually setup and configure a separate nginx on her box.

I really like keeping things as simple as possible so I tried reducing the complexity by integrating Traefik as reverse-proxy and its configuration into the docker-compose file ending up with a single file that could fire up the complete Mastodon instance :)

Additional features (over the original docker-compose from the repo):

Prerequisites

Before we start there are some things that we need to prepare:

social.yourdomain.com    A    ip.of.your.box
social.yourdomain.com    AAAA    ip6.of.your.box

Before you continue, make sure these things are done.

For the impatient

If you just want to get things running, follow these steps:

That should be it. You now have an instance of Mastodon running behind a traefik reverse-proxy handling HTTPS redirection, TLS termination and automagic setup and renewal of Let’s Encrypt certificates. Persistence data from the containers is stored in folders located in the same directory as your docker-compose.yml.

For the curious

Well, there are a lot of things going on in the docker-compose.yml that you might want to understand. Basically, it’s the whole setup of traefik and the corresponding Mastodon related configuration.

So let’s go through the services being started in the docker-compose file and see what happens.

Traefik

At first, we start traefik so we have someone answering requests from outside. More specifically, traefik’s job will be to route requests headed to your <DOMAIN> further to your Mastodon instance and back outside. While doing this, traefik handles:

Let’s have a look at the traefik part of the docker-compose.yml:

traefik:
    image: traefik:2.9
    container_name: "traefik"
    restart: always
    command:
#      - "--log.level=DEBUG"
      - "--api.dashboard=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=<LETSENCRYPT_MAIL_ADDRESS>"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    labels:
      - "traefik.enable=true"
      # Dashboard
      - "traefik.http.routers.traefik.rule`(Host(`<DOMAIN>`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`)))"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.middlewares=dashboardauth"
      - "traefik.http.middlewares.dashboardauth.basicauth.users=admin:<TRAEFIK_DASHBOARD_ADMIN_PASSWORD>"
      # HTTPS Redirect
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https@docker"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./letsencrypt:/letsencrypt
    networks:
      - external_network

At first glance, we see there is a lot of configuration covered by commands and labels. This is intended, as our goal is to have a docker-compose.yml that is as self-contained as possible. To understand why certain things are commands and others are labels we must know that Traefiks configuration is composed of a static part and a dynamic part. For further details, there are some great explanations in the Traefik documentation.

Static configuration

The static configuration deals with settings that are required at startup time. In this case that are all settings set as commands in our docker-compose.yml:

--api.dashboard=true

--entrypoints.web.address=:80

--entrypoints.websecure.address=:443

--providers.docker=true

--providers.docker.exposedbydefault=false

--certificatesresolvers.letsencrypt.acme.httpchallenge=true

--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web

--certificatesresolvers.letsencrypt.acme.email=<LETSENCRYPT_MAIL_ADDRESS>

--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json

That’s it for the static configuration. We have successfully set up the Traefik dashboard, endpoints for HTTP and HTTPS, docker as provider for dynamic configuration, and a certificate resolver handling Let’s Encrypt stuff.

Dynamic configuration

Let’s move on to the dynamic configuration found in the labels section. These configuration items are related to “How do I access Traefiks dashboard?” and “HTTPS redirection”.

We start with the relevant labels for accessing the Traefik dashboard:

traefik.enable=true

traefik.http.routers.traefik.rule=(Host(``<DOMAIN>``) && (PathPrefix(``/api``) || PathPrefix(``/dashboard``)))

traefik.http.routers.traefik.service=api@internal

traefik.http.routers.traefik.tls.certresolver=letsencrypt

traefik.http.routers.traefik.entrypoints=websecure

traefik.http.routers.traefik.middlewares=dashboardauth

traefik.http.middlewares.dashboardauth.basicauth.users=admin:<TRAEFIK_DASHBOARD_ADMIN_PASSWORD>

That’s all we need to have our Traefik dashboard being accessible via HTTPS.

The next labels make Traefik redirect HTTP requests on port 80 to HTTPS.

traefik.http.routers.http-catchall.rule=hostregexp({host:.+})

traefik.http.routers.http-catchall.entrypoints=web

traefik.http.routers.http-catchall.middlewares=redirect-to-https@docker

traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https

And that’s it for the HTTPS redirection. We now have completed the configuration of Traefik in our docker-compose.yml.

Mastodon

We wanted to do something meaningful over just firing up Traefik, remember? The rest of our docker-compose.yml is composed of services required by Mastodon. I won’t go into detail here. The part worth looking at are the services web and streaming as those must be accessible from the outside and hence need configuration for Traefik. We need web to deliver a nice UI for using Mastodon, and we need streaming to realize all the inter instance communication.

Luckily, the Traefik configuration is straight forward for both services and we know all the required parts from the labels setting up the Traefik dashboard.

Web
[...]
  web:
    [...]
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=mastodon_external_network"
      - "traefik.http.services.mastodon-web.loadbalancer.server.port=3000"
      - "traefik.http.routers.mastodon-web.rule=Host(`<DOMAIN>`)"
      - "traefik.http.routers.mastodon-web.entrypoints=websecure"
      - "traefik.http.routers.mastodon-web.tls.certresolver=letsencrypt"
    [...]
    networks:
      - external_network
      - internal_network
[...]

You might recognize these labels, so I will just decribe in a few words what they do:

Streaming
[...]
  streaming:
    [...]
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=mastodon_external_network"
      - "traefik.http.services.mastodon-streaming.loadbalancer.server.port=4000"
      - "traefik.http.routers.mastodon-streaming.rule=(Host(`<DOMAIN>`) && PathPrefix(`/api/v1/streaming`))"
      - "traefik.http.routers.mastodon-streaming.entrypoints=websecure"
      - "traefik.http.routers.mastodon-streaming.tls.certresolver=letsencrypt"
    [...]
    networks:
      - external_network
      - internal_network
[...]

For Mastodons streaming service this is very similar, let’s see:

Conclusion

I was not quite happy with the assumptions made by Mastodon regarding instance setup. Especially, that they make instance admins go through a hell of nginx configuration. My goal was to make the process of setting up a new Mastodon instance as easy as possible. The solution is the combination of Mastodon with Traefik instead of Nginx and a self-contained docker-compose.yml that sets up everything necessary.

I sincerely hope this guide is useful for other upcoming Mastodon admins or Traefik fans :)

Update (10.11.2020):

Implemented some feedback regarding the networks and ports for the web and streaming services. Additionally, I updated the guide to use the current versions of Mastodon and Traefik.

Update (14.11.2022):

TAGS