Skip to content

Instantly share code, notes, and snippets.

@redlegoman
Forked from TrillCyborg/mastodon-docker-setup.md
Last active July 11, 2023 13:34
Show Gist options
  • Save redlegoman/b083c81c628fe3952a52e5f168e70688 to your computer and use it in GitHub Desktop.
Save redlegoman/b083c81c628fe3952a52e5f168e70688 to your computer and use it in GitHub Desktop.
Mastodon Docker Setup

Mastodon Docker Setup

Setting up

Clone Mastodon's repository. You only really need the docker-compose.yml and the .env.production.sample files from the reo, so clone it to a temporary directory and copy those two files to ~/live

# Clone mastodon to `tmp` directory
git clone https://github.com/tootsuite/mastodon.git tmp

cp tmp/docker-compose.yml ~/live/.
cp tmp/.env.production.sample ~/live/.env.production

OR you could just wget the files like this wget https://github.com/mastodon/mastodon/blob/main/docker-compose.yml wget https://github.com/mastodon/mastodon/raw/main/.env.production.sample -O .env.production

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

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 v4.0.0 for example, you would edit the lines to say: image: tootsuite/mastodon:v4.0.0
    3. Save the file and exit the text editor.
  2. Edit .env.production.sample to bootstrap the configuration. Edit the correct values now. Don't worry about the SECRETS, they will be genreated in the bit further on in Building the app.
  3. Run docker-compose pull. It will now pull the correct images from Docker Hub.
  4. Set correct file-owner with sudo chown -R 991:991 public/system (991 is the uid of the mastadon user in the web image)

Configuration

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. Press through all the options and say yes to saving it. It won't overwrite your local .env.production though, because that is not inside the container. At some point it will output your configuration, copy and paste that configuration into the .env.production file.

$ docker-compose run --rm web bundle exec rake mastodon:setup
Creating network "test_internal_network" with the default driver
Creating network "test_external_network" with the default driver
Creating db_test    ... done
Creating redis_test ... done
Your instance is identified by its domain name. Changing it afterward will break things.
Domain name: mastodon.example.com

Single user mode disables registrations and redirects the landing page to your public profile.
Do you want to enable single user mode? yes

Are you using Docker to run Mastodon? Yes

PostgreSQL host: db
PostgreSQL port: 5432
Name of PostgreSQL database: postgres
Name of PostgreSQL user: postgres
Password of PostgreSQL user:
Database configuration works! 🎆

Redis host: redis
Redis port: 6379
Redis password:
Redis configuration works! 🎆

Do you want to store uploaded files on the cloud? No

Do you want to send e-mails from localhost? yes
E-mail address to send e-mails "from": Mastodon <notifications@mastodon.example.com>
Send a test e-mail with this configuration right now? no

This configuration will be written to .env.production
Save configuration? Yes
Below is your configuration, save it to an .env.production file outside Docker:


# Generated with mastodon:setup on 2022-11-16 08:04:27 UTC

# Some variables in this file will be interpreted differently whether you are
# using docker-compose or not.

LOCAL_DOMAIN=mastodon.example.com
SINGLE_USER_MODE=true
SECRET_KEY_BASE=X
OTP_SECRET=X
VAPID_PRIVATE_KEY=X
VAPID_PUBLIC_KEY=X
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
SMTP_SERVER=localhost
SMTP_PORT=25
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
SMTP_FROM_ADDRESS=Mastodon <notifications@mastodon.example.com>

It is also saved within this container so you can proceed with this wizard.

Now that configuration is saved, the database schema must be loaded.
If the database already exists, this will erase its contents.
Prepare the database now? Yes
Running `RAILS_ENV=production rails db:setup` ...


Database 'postgres' already exists
Done!

All done! You can now power on the Mastodon server 🐘

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

docker-compose up -d

Add an account

If you didn't choose to setup an account when running the setup wizwad, you can do so once your instance is up and running: docker exec -it web bash mastodon@76ac55baa710:~$ tootctl accounts create username --role=Owner --email=username@example.com --confirmed OK New password: cbe8012bab09342f8adad9c0d6494338 mastodon@76ac55baa710:~$ tootctl accounts approve username OK

The password is generated by the system and will be printed to screen

nginx Configuration

You need to configure nginx to serve your Mastodon instance.

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

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

vim /etc/nginx/sites-available/mastodon.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 mastodon.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 mastodon.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/mastodon.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/mastodon.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/mastodon.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.

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 'mastodon.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 mastodon.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 mastodon.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

Notes

SECRET_KEY_BASE=$(docker-compose run --rm web bundle exec rake secret)
sed -i -e "s/SECRET_KEY_BASE=/&${SECRET_KEY_BASE}/" .env.production
OTP_SECRET=$(docker-compose run --rm web bundle exec rake secret)
sed -i -e "s/OTP_SECRET=/&${OTP_SECRET}/" .env.production
PAPERCLIP_SECRET=$(docker-compose run --rm web bundle exec rake secret)
sed -i -e "s/PAPERCLIP_SECRET=/&${PAPERCLIP_SECRET}/" .env.production
docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment