Skip to content

Instantly share code, notes, and snippets.

@melroy89
Forked from akuechl/mastodon-docker-setup.md
Last active March 19, 2024 18:24
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save melroy89/6fe7d05bdc0cfd2153b77310abf62990 to your computer and use it in GitHub Desktop.
Save melroy89/6fe7d05bdc0cfd2153b77310abf62990 to your computer and use it in GitHub Desktop.
Mastodon Docker Setup - most complete and easiest guide online

Mastodon Docker Setup

In this guide we will only focus on using the prebuilt images from Docker Hub.

Prerequisites: You have Git, Docker, Docker compose and Nginx pre-installed.

Prepare

Clone Mastodon's repository.

# Clone mastodon git repo
git clone https://github.com/tootsuite/mastodon.git
# 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)

Docker Compose file

Open the docker-compose.yml file in your favorite text editor.

  1. Comment out the build: . lines for all images (web, streaming, sidekiq). You can do this by adding: # before those lines.
  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 for example, you would edit the lines to say: image: tootsuite/mastodon:v4.0
  3. Do maybe want to change the default volume mount location in the Docker compose file, see the web section. Default is ./public/system. If you change this to another location (let's say: /var/www/mastodon/public/system). Update the volumes section of the web section to:
volumes:
 - /var/www/mastodon/public/system:/mastodon/public/system
  1. Save the docker-compose.yml file and exit your text editor.

Create public web folder

We will now create the web folder on your host machine, we specified earlier in the docker-compose.yml file.

  1. To create this folder, execute:
mkdir -p /var/www/mastodon/public/system

Or when using the default settings, create a local public/system folder hierarchy:

mkdir -p public/system
  1. You also need to set the correct permissions to the public web folder. When you change the volume mount to the location I described earlier, use:
sudo chown -R 991:991 /var/www/mastodon/public

Or when you kept the default settings execute instead:

sudo chown -R 991:991 public

Mastodon environment config file

  1. It is adviced to use the sample configuration file as our starting point (so we have all the comments and options available). Thus, let's make a copy the example config using:
    cp .env.production.sample .env.production
  2. You could use the mastodon:setup task to help you generate several important configuration variables for the .env.production file.
    docker run -it --rm tootsuite/mastodon bundle exec rake mastodon:setup
    Hint: If it asks you to "Save configuration?" You can say: No/N.
  3. Open your local .env.production file and apply the generated settings. Pay extra attention to the following settings: LOCAL_DOMAIN, DB_*, ES_ENABLED, S3_ENABLED, SECRET_KEY_BASE and OTP_SECRET, VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY.
    1. LOCAL_DOMAIN should point to your public (sub)domain you want to use to serve Mastodon on.
    2. PostgreSQL config for Docker should be:
      DB_HOST=db
      DB_USER=postgres
      DB_NAME=postgres
      DB_PASS=
      DB_PORT=5432
    3. Redis can be set to redis for Docker:
      REDIS_HOST=redis
    4. Set both ES_ENABLED and S3_ENABLED to false, if you aren't using Elasticsearch or Amazon.
    5. The SECRET_KEY, OPT_SECRET, VAPID_*_KEY's are generated by the setup, which you can copy one-by-one.
    6. Try to enable SMTP as well. Example of Gmail config. Generate an Application password at your Google Account settings:
      SMTP_SERVER=smtp.gmail.com
      SMTP_PORT=587
      SMTP_LOGIN=your_email@gmail.com
      SMTP_PASSWORD=your_password
      SMTP_AUTH_METHOD=plain
      SMTP_OPENSSL_VERIFY_MODE=none
      SMTP_ENABLE_STARTTLS=auto
      SMTP_FROM_ADDRESS=Mastodon <notifications@example.com>
    7. When running in production it's advised to log level to warn (default info):
      RAILS_LOG_LEVEL=warn
    8. Depending on your servers load and usage, you could also increase some limits. (It's up to you to select the values that fit your needs):
      MAX_THREADS=15
      SIDEKIQ_CONCURRENCY=10
  4. Save all the changes to .env.production and exit your text editor.

More info about the configuration environment settings.

Continue with the Database setup, chapter below.

Database Setup

Before starting Mastodon for the first time. We need to setup the database once. You should now have configured both the docker-compose.yml and .env.production before continuing with this step.

To setup the database we wil run the db:setup task together with Docker compose command, execute:

docker-compose run --rm web bundle exec rails db:setup

Launching Mastodon

If the database setup went successfully; well done! We can now start Mastodon for the first time.

You can launch Mastodon with:

docker-compose up

If everything seems to run fine, you can start the containers with the -d flag (for detach, so the containers will run in the background):

docker-compose up -d

Important: You can now go to the last step, setting-up Nginx as your reverse proxy. See below "Nginx Setup".

PostgreSQL

As discussed earlier. We will use the default Docker PostgreSQL configuration. Meaning the .env.production config file should contain the following database settings:

DB_HOST=db
DB_USER=postgres
DB_NAME=postgres
DB_PASS=
DB_PORT=5432

Redis

As discussed earlier, Redis can be set to redis when using Docker compose:

REDIS_HOST=redis

Email (SMTP)

As described earlier, here is an example of the Gmail config in .env.production as the SMTP service provider, used for outgoing emails:

SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_LOGIN=your_email@gmail.com
SMTP_PASSWORD=your_password
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
SMTP_FROM_ADDRESS=Mastodon <notifications@example.com>

Generate an Application password at your Google Account settings. Try to avoid using your account password directly.

Docker network

Mastodon docker-compose.yml configuration file by default configures two Docker bridge networks. An external_network and an internal_network. The db and redis containers will not be part of the external network, however web, streaming and the sidekiq containers are part of the external_network bridge network and thus available on your host machine. After all that is what the Docker external bridge does as long as you publish the ports (which is also part of the docker-compose file).

The internal_network allows all different Mastodon containers (web, db, redis,..) to contact each other, while still be in an isolated Docker network.

The containers that are part of the external_network should now be accessible to you from the host machine.

I created a simple high-level network overview of the self-defined Docker bridge networks that will be created for you:

docker_network_design

If you didn't understand a word of what I told you, don't panic, everything is done automatically for you when starting the containers.

Nginx Setup

You need to configure Nginx to serve your Mastodon instance. We assume you already have installed Nginx.

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

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

We adapted the official Nginx configuration slightly to fit our needs.

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

Reminder #2: Depending on your public volume folder for the web instance, you want to change /var/www/mastodon/public in the Nginx example below to the location you setup earlier in this guide. Currently, we assume you changed the Docker compose web volume to: /var/www/mastodon/public/system:/mastodon/public/system.

Copy and paste the following and make additional edits where needed:

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

upstream backend {
    server 127.0.0.1:3000 fail_timeout=0;
}

upstream streaming {
    server 127.0.0.1:4000 fail_timeout=0;
}

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  root /var/www/mastodon/public;
  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;

  # intermediate SSL config (Mozilla Guideline)
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  # Uncomment these lines once you acquire a certificate:
  # ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
  ssl_session_tickets off;
    
  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;
  
  root /var/www/mastodon/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 image/svg+xml image/x-icon;

  location / {
    try_files $uri @proxy;
  }

  # If Docker is used for deployment and Rails serves static files,
  # then needed must replace line `try_files $uri =404;` with `try_files $uri @proxy;`.
  location = /sw.js {
    add_header Cache-Control "public, max-age=604800, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/assets/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/avatars/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/emoji/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/headers/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/packs/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/shortcuts/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/sounds/ {
    add_header Cache-Control "public, max-age=2419200, must-revalidate";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  location ~ ^/system/ {
    add_header Cache-Control "public, max-age=2419200, immutable";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    try_files $uri @proxy;
  }

  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 $scheme;
    proxy_set_header Proxy "";

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

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";

    tcp_nodelay on;
  }

  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 $scheme;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

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

    proxy_cache CACHE;
    proxy_cache_valid 200 7d;
    proxy_cache_valid 410 24h;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cached $upstream_cache_status;

    tcp_nodelay on;
  }

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

Activate your Nginx site configuration:

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

And finally restart the Nginx server:

sudo systemctl restart nginx

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.

Prerequisites: You need to have Certbot installed, follow these installation instructions.

Generation of the Certificate

Creating a Let's encrypt certificate easy. Just select the domain from the list after executing:

sudo certbot --nginx

When you are using the latest stable release of certbot your certificates will be renewed automatically.

Resources

@melroy89
Copy link
Author

melroy89 commented Nov 6, 2023

Ps. Certbot nginx plugin will insert if blocks in nginx server block on port 80.

@Daniel15
Copy link

Daniel15 commented Nov 6, 2023

Ps. Certbot nginx plugin will insert if blocks in nginx server block on port 80.

Ah, I didn't realise it did that. It probably does it because it's easiest to do, but it's not recommended. I use certbot certonly rather than the Nginx plugin which is why I was unaware of this behaviour.

@danielkrajnik
Copy link

danielkrajnik commented Nov 6, 2023

I haven't tried certonly, but python3-certbot-nginx messed up the site configuration quite a lot, so if (or when) the time comes to reinstall mastodon I will definitely try it instead.

@otrapersona
Copy link

otrapersona commented Feb 21, 2024

To install glitch-soc with this instructions:
I cloned glitch-soc's repo https://github.com/glitch-soc/mastodon instead of mastodon's.
And replaced ghcr.io/mastodon/mastodon with yakumosaki/glitch-soc-aarch64 (or yakumosaki/glitch-soc for amd64)

@melroy89
Copy link
Author

melroy89 commented Feb 22, 2024

https://github.com/glitch-soc/mastodon

good to know! All I need is markdown, so https://glitch-soc.github.io/docs/features/rich-text/ is a much welcoming feature with code blocks and alike. Too bad the code blocks doesn't really show like a code block (and without syntax highlighting) and it will also most likely be stripped by Mastodon software. I just really wish Mastodon implements markdown by default, how hard can it be?

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