Skip to content

Instantly share code, notes, and snippets.

@aeble
Forked from TrillCyborg/mastodon-docker-setup.md
Last active October 25, 2021 18:29
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aeble/ffd4f85a592d8029361ba806d1bed7f3 to your computer and use it in GitHub Desktop.
Save aeble/ffd4f85a592d8029361ba806d1bed7f3 to your computer and use it in GitHub Desktop.
Mastodon Docker Setup

Mastodon Docker Setup

Setting up

Clone Mastodon's repository.

# Clone mastodon to ~/mastodon directory
git clone https://github.com/tootsuite/mastodon.git mastodon
# Change directory to ~/mastodon
cd ~/mastodon
# Checkout to the latest stable branch
git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)

Review the settings in docker-compose.yml. Note that it is not default to store the postgresql database and redis databases in a persistent storage location. If you plan on running your instance in production, you must uncomment the volumes directive in docker-compose.yml.

Getting the Mastodon image

Using a prebuilt image

If you're not making any local code changes or customizations on your instance, you can use a prebuilt Docker image to avoid the time and resource consumption of a build. Images are available from Docker Hub: https://hub.docker.com/r/tootsuite/mastodon/

To use the prebuilt images:

  1. Open docker-compose.yml in your favorite text editor.
    1. Comment out the build: . lines for all images (web, streaming, sidekiq).
    2. Edit the image: tootsuite/mastodon lines for all images to include the release you want. The default is latest which is the most recent stable version, however it recommended to explicitly pin a version: If you wanted to use v2.2.0 for example, you would edit the lines to say: image: tootsuite/mastodon:v2.2.0
    3. Save the file and exit the text editor.
  2. Run cp .env.production.sample .env.production to bootstrap the configuration. Edit the correct values now.
  3. Run docker-compose build. It will now pull the correct image from Docker Hub.
  4. Set correct file-owner with sudo chown -R 991:991 public/system

Building your own image

You must build your own image if you've made any code modifications. To build your own image:

  1. Open docker-compose.yml in your favorite text editor.

    1. Uncomment the build: . lines for all images (web, streaming, sidekiq) if needed.
    2. Correct the healthcheck command in the db section to test: ["CMD", "pg_isready", "-U", "mastodon", "-d", "mastodon_production"]
    3. Add environment variables to the db section:
      environment:
        POSTGRES_PASSWORD: xyz <-- choose a safe one, 20-30 chars
        POSTGRES_DB: mastodon_production
        POSTGRES_USER: mastodon
    4. Save the file and exit the text editor.
  2. Run cp .env.production.sample .env.production to bootstrap the configuration. Edit the correct values now.

    1. Adjust LOCAL_DOMAIN
    2. Change REDIS_HOST to REDIS_HOST=redis
    3. Likewise, change the database section to:
      # PostgreSQL
      # ----------
      DB_HOST=db
      DB_USER=mastodon
      DB_NAME=mastodon_production
      DB_PASS=xyz **<-- use the same one you entered in docker-compose.yml in the db section!**
      DB_PORT=5432
      
    4. Disable ElasticSearch (optional) by setting ES_ENABLED=false
    5. Disable S3 file storage (optional) by setting S3_ENABLED=false
  3. Run docker-compose build.

    Currently (2021-05-11, Version 3.3.0 (#15433)) the build process is broken as the package mimemagicis locked to 0.3.5 which is no longer in the rubygems repository. The newer package seems to have a license that taints all the rest of the project so it has been exchanged for another package. I'm not yet familiar enough with docker and ruby to have been able to backport this.

  4. Set correct file-owner with chown -R 991:991 public

Building the app

Now the image can be used to generate a configuration with:

docker-compose run --rm web bundle exec rake mastodon:setup

This is an interactive wizard that will guide you through the basic and necessary options and generate new app secrets.

  1. Enter the Fully Qualified Domain Name (FQDN) of your mastodon instance.
  2. Select if you want a Single User instance (not recommended, but if you prefer, use that).
  3. Obviously, you are running mastodon in a docker instance, so type Y (or hit return, as it's the default)
  4. The PostgreSQL host is db, the port is 5432 (again, default), the database is mastodon_production, the database user is mastodon and the password is the one from docker-compose.yml.
  5. The redis server is redis, the port is 6379 and the password is empty.
  6. If you want to store uploaded files on the cloud, enter Y here – I haven't tested that, but I expect you need an S3 or other cloud storage for that.
  7. If you want to send emails from the local machine, enter Y – considering we're in a docker environment, I have only used my local STMP server in FQDN form, not localhost. Enter port, user and password for SMTP submission. Select the SMTP authentication type (when submitting locally, plain should be fine). Decide if you want to verify the identity of the server and, if so, what type of verification you want to do. Choose what sender address the emails will have (I use mastodon@*my.domain*).

Now it will output your configuration. Copy and paste that into the .env.production file.

The wizard will setup the database schema and precompile assets. After it's done, you can launch Mastodon with:

docker-compose up -d

Reverse Proxy

You need a Reverse Proxy in front of your Mastodon instance. The preferred software for this and the most used and best documented for Mastodon is nginx. In case you have an Apache running on port 80 anyway, you can also use that apache2 instance as a reverse proxy.

nginx Configuration

You need to configure nginx to serve your Mastodon instance.

Reminder: Replace all occurrences of example.com with your own instance's domain or sub-domain.

cd to /etc/nginx/sites-available and open a new file:

nano /etc/nginx/sites-available/example.com.conf

Copy and paste the following and make edits as necessary:

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  root /home/mastodon/live/public;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;

  root /home/mastodon/live/public;

  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri @proxy;
  }
  
  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    try_files $uri @proxy;
  }

  location @proxy {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";

    proxy_pass http://127.0.0.1:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}

Activate the nginx configuration added:

cd /etc/nginx/sites-enabled
ln -s ../sites-available/example.com.conf

This configuration makes the assumption you are using Let's Encrypt as your TLS certificate provider.

If you are going to be using Let's Encrypt as your TLS certificate provider, see the next sub-section. If not edit the ssl_certificate and ssl_certificate_key values accordingly.

Apache2 mod_proxy Reverse Proxy Configuration

This is stub configuration for apache2 as reverse proxy. It is recommended to put this in a separate site configuration file.

Note to self: add detailed configuration steps here

Reminder: Replace all occurrences of example.com with your own instance's domain or sub-domain.

<IfModule mod_ssl.c>
<VirtualHost fqdn.example.com:443>
    ServerAdmin your_admin@example.com
    <IfModule mod_headers.c>
      Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
    </IfModule>

    Protocols h2 h2c http/1.1

    Header add Strict-Transport-Security "max-age=31536000"

    ProxyPreserveHost On

    ServerName fqdn.example.com

    DocumentRoot /var/www/html
    ProxyHTMLStripComments on
    ProxyRequests off
    SetOutputFilter proxy-html
    ProxyHTMLDoctype XHTML
    SSLProxyEngine on

    ProxyPass /.well-known !

    SetEnvIf Remote_Addr "(.*)" REMOTE_ADDR=$1

    ProxyPreserveHost On
    RequestHeader set X-Real-IP $REMOTE_ADDR
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set Proxy ""

    ProxyPass /api/v1/streaming/ ws://localhost:4000/
    ProxyPassReverse /api/v1/streaming/ ws://localhost:4000/

    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    ErrorLog ${APACHE_LOG_DIR}/mastodon-error.log
    ErrorDocument 403 /custom-errors/403/HauntedHouse/403.html
    ErrorDocument 404 /custom-errors/404/404.html

    CustomLog ${APACHE_LOG_DIR}/mastodon-access.log combined

    Include /etc/letsencrypt/options-ssl-apache.conf
    SSLCertificateFile /etc/letsencrypt/live/XXXXX/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/XXXXX/privkey.pem

    SSLHonorCipherOrder on
    SSLCipherSuite EECDH+AESGCM:AES256+EECDH:AES128+EECDH

    ErrorDocument 500 /500.html
    ErrorDocument 501 /500.html
    ErrorDocument 502 /500.html
    ErrorDocument 503 /500.html
    ErrorDocument 504 /500.html

</VirtualHost>
</IfModule>

Let's Encrypt

This section is only relevant if you are using Let's Encrypt as your TLS certificate provider.

Generation Of The Certificate

We need to generate Let's Encrypt certificates.

Make sure to replace any occurrence of 'example.com' with your Mastodon instance's domain.

Make sure that nginx is stopped at this point:

systemctl stop nginx

We will be creating the certificate twice, once with TLS SNI validation in standalone mode and the second time we will be using the webroot method. This is required due to the way nginx and the Let's Encrypt tool works.

certbot certonly --standalone -d example.com

After that successfully completes, we will use the webroot method. This requires nginx to be running:

systemctl start nginx
# The certbot tool will ask if you want to keep the existing certificate or renew it. Choose to renew it.
certbot certonly --webroot -d example.com -w /home/mastodon/live/public/

Automated Renewal Of Let's Encrypt Certificate

Let's Encrypt certificates have a validity period of 90 days.

You need to renew your certificate before the expiration date. Not doing so will make users of your instance unable to access the instance and users of other instances unable to federate with yours.

We can create a cron job that runs daily to do this:

nano /etc/cron.daily/letsencrypt-renew

Copy and paste this script into that file:

#!/usr/bin/env bash
certbot renew
systemctl reload nginx

Save and exit the file.

Make the script executable and restart the cron daemon so that the script runs daily:

chmod +x /etc/cron.daily/letsencrypt-renew
systemctl restart cron

That is it. Your server will renew your Let's Encrypt certificate.

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment