Mastodon on Docker

I like a good adventure, and an adventure self-hosting something new is never a bad idea. This post will be about self-hosting Mastodon (and the resources it requires) as well as using nginx to control the front door. I'd like to thank Raphael Fleischlin (Raeffs Dev Site) for his post - it got me moving toward this project - with the big difference being that I wanted to use the consolidated docker container published by LinuxServer.io.

This post assumes you are familiar with (well, actually prefer) docker compose. You can run these various commands interactively, but everything described here will be based on docker compose.

Pre-requisites
  1. Linux box running Ubuntu 20.04.6 LTS (you more than likely can get away with a newer LTS version - this is what I specifically used).
  2. Docker installed on that box. I'm not super crazy about snap yet, so I do it manaully as described here. Additionally, I also follow the post-installation steps hightlighted here.
  3. Depending on the version of docker compose you install, the commands will either be "docker compose" (with no dash between them) or "docker-compose" (with a dash). I put an alias in my .bashrc file so that all of my shortcut macros work regardless - so on this box I've got this alias to smooth things out: alias docker-compose='docker compose'
Assumptions
  1. A data directory. Mine is off the root - so /data. All "permanent" data will be stored here for easy upgrading from docker container to docker container.
  2. A folder for my docker files and Let's encrypt script. My folder is off my personal directory so /home/chris/docker. I'll use ~/docker below to reference this directory. If you have any issues, replace ~ with /home/[youusername].
  3. I use nano as my text editor of choice. You do you - and then adjust the commands below.
  4. nginx is going to listen and manage port 80 and 443.
  5. I'm using the linuxserver.io docker image found here.
  6. Your DNS server is pointing to the IP address of this new box you are standing up.
The preliminary work
  1. Create the directories noted in the assumptions block. For reference here you go:
    sudo mkdir /data
    mkdir ~/docker
    
  2. Create and edit a docker compose file:
    nano ~/docker/docker-compose.yml
    
  3. Paste the following into the docker-compose.yml file, change the 11 instances marked "Change this", if desired change the 1 instance marked "Change this as needed", and then save and exit (the "Change this later" items will be changed later):
    version: '3.8'
    
    services:
      nginx:
        image: nginx:latest
        restart: unless-stopped
        container_name: nginx
        networks:
          - mastodon
        depends_on:
          - mastodon
        volumes:
          - /data/nginx/nginx.conf:/etc/nginx/nginx.conf
          - /data/nginx/certs:/etc/nginx/certs
        ports:
          - 80:80
          - 443:443
    
      redis:
        restart: unless-stopped
        image: redis:7-alpine
        container_name: redis
        networks:
          - mastodon
        healthcheck:
          test: ['CMD', 'redis-cli', 'ping']
        volumes:
          - /data/mastodon/redis:/data
        ports:
          - 6379:6379
    
      mastodon-db:
        image: postgres:alpine
        container_name: mastodon-db
        command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
        networks:
          - mastodon
        volumes:
          - /data/mastodon/postgres:/var/lib/postgresql/data
        environment:
          POSTGRES_USER: [db-user-goes-here]                          ## Change this
          POSTGRES_PASSWORD: [db-password-goes-here]                  ## Change this
          POSTGRES_DB: mastodon
    
      mastodon:
          image: lscr.io/linuxserver/mastodon:latest
          container_name: mastodon
          networks:
            - mastodon
          environment:
            - PUID=1000
            - PGID=1000
            - TZ=[your-timezone]                                      ## Change this
            - LOCAL_DOMAIN=[domain-name]                              ## Change this
            - REDIS_HOST=redis                                        ## This picks up the name from the above container
            - REDIS_PORT=6379
            - DB_HOST=mastodon-db                                     ## This picks up the name from the above container
            - DB_USER=[db-user-goes-here]                             ## Change this
            - DB_NAME=mastodon
            - DB_PASS=[db-password-goes-here]                         ## Change this
            - DB_PORT=5432
            - ES_ENABLED=false
            - SECRET_KEY_BASE=[mastodon-secret-goes-here]             ## Change this later
            - OTP_SECRET=[mastodon-otp-secret-goes-here]              ## Change this later
            - VAPID_PRIVATE_KEY=[vapid-private-key-goes-here]         ## Change this later
            - VAPID_PUBLIC_KEY=[vapid-public-key-goes-here]           ## Change this later
            - SMTP_SERVER=[smtp-server-goes-here]                     ## Change this
            - SMTP_PORT=587                                           ## Change this as needed
            - SMTP_LOGIN=[smtp-login-goes-here]                       ## Change this
            - SMTP_PASSWORD=[smtp-password-goes-here]                 ## Change this
            - SMTP_FROM_ADDRESS=[smtp-from-address-goes-here]         ## Change this
            - S3_ENABLED=false
            - WEB_DOMAIN=[domain-name]                                ## Change this (match the above value for local_domain)
          volumes:
            - /data/mastodon/mastodon/config:/config
          depends_on:
            - redis
            - mastodon-db
          ports:
            - 3000:3000
          restart: unless-stopped
    
    networks:
      mastodon:
        external: false
    
  4. Create and edit a nginx.conf file:
    sudo mkdir -p /data/nginx
    sudo nano /data/nginx/nginx.conf
    
  5. Paste the following into the nginx.conf file, change the domain name on the 5 indicated lines, then save and exit:
    events {
      worker_connections 1024;
    }
    
    http {
      ## myprivate.social :: Port 80
      server {
          listen 80;
          server_name [domain-name];                                                  ## Change this
    
          return 301 https://[domain-name];                                           ## Change this
      }
    
      ## myprivate.social :: Port 443
      server {
        listen 443 ssl http2;
        server_name [domain-name];                                                    ## Change this
    
        ssl_certificate           /etc/nginx/certs/[domain-name]/fullchain.pem;       ## Change this
        ssl_certificate_key       /etc/nginx/certs/[domain-name]/privkey.pem;         ## Change this
    
        location / {
          add_header 'Cache-Control'                    'no-cache, no-store, must-revalidate';
          add_header 'Content-Security-Policy'          'connect-src *';
          add_header 'Expires'                          '0';
          add_header 'Pragma'                           'no-cache';
          add_header 'Strict-Transport-Security'        'max-age=31536000; includeSubDomains';
          add_header 'X-Content-Type-Options'           'nosniff';
          add_header 'X-Frame-Options'                  'SAMEORIGIN';
          add_header 'X-XSS-Protection'                 '1; mode=block';
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header Host $http_host;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Real-Port $server_port;
          proxy_set_header X-Real-Scheme $scheme;
          proxy_set_header X-NginX-Proxy true;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Forwarded-Ssl on;
          client_max_body_size 5m;
          proxy_pass   http://mastodon;
        }
      }
    }
    
  6. Get an API key from your DNS provider. This example is assuming you are using NS1 (ns1.com). There are references to change 'nsone' throughout the rest of this script. If you are using a different provider, adjust accordingly. The provider list that this came from can be found here.
  7. Create and edit a file with the DNS API key
    sudo mkdir -p /data/LetsEncrypt/etc
    sudo nano /data/LetsEncrypt/etc/dns.ini
    
  8. Paste the following into the dns.ini file, adjust the values as indicated and save and exit - sometimes the "key" needs to change too (e.g. if you are using a different DNS provider like CloudFlare):
    dns_nsone_api_key = [ApiKey]                                                      ## Change the API Key and nsone word
    
  9. Create, make runnable, and edit a certbot-script.sh file:
    touch ~/docker/certbot-script.sh
    chmod 774 ~/docker/certbot-script.sh
    nano ~/docker/certbot-script.sh
    
  10. Paste the following into the certbot-script.sh file, change the 10 "Change this" lines (one has 2 changes), change the 8 dns plug-in lines from 'nsone' to the appropriate value, then save and exit.
    #!/bin/bash
    
    nginx_dir=/data/nginx/certs/[domain-name]                                         ## Change this
    
    certbot_chain=/data/LetsEncrypt/etc/live/[domain-name]/chain.pem                  ## Change this
    certbot_fullchain=/data/LetsEncrypt/etc/live/[domain-name]/fullchain.pem          ## Change this
    certbot_cert=/data/LetsEncrypt/etc/live/[domain-name]/cert.pem                    ## Change this
    certbot_key=/data/LetsEncrypt/etc/live/[domain-name]/privkey.pem                  ## Change this
    
    nginx_chain=/data/nginx/certs/[domain-name]/chain.pem                             ## Change this
    nginx_fullchain=/data/nginx/certs/[domain-name]/fullchain.pem                     ## Change this
    nginx_cert=/data/nginx/certs/[domain-name]/cert.pem                               ## Change this
    nginx_key=/data/nginx/certs/[domain-name]/privkey.pem                             ## Change this
    
    if [[ ! -f "$certbot_cert" ]]; then
        echo "Creating new certificate..."
        docker run --rm -it \
            --name certbot \
            -v "/data/LetsEncrypt/etc:/etc/letsencrypt" \
            -v "/data/LetsEncrypt/var/lib:/var/lib/letsencrypt" \
            certbot/dns-nsone certonly \
            --dns-nsone \
            --dns-nsone-credentials /etc/letsencrypt/dns.ini \
            --dns-nsone-propagation-seconds 10 \
            -d [domain-name] -d *.[domain-name] \
            --agree-tos
        echo "Creating new certificate...complete!"
    else
        echo "Renewing certificate..."
        docker run --rm -it \
            --name certbot \
            -v "/data/LetsEncrypt/etc:/etc/letsencrypt" \
            -v "/data/LetsEncrypt/var/lib:/var/lib/letsencrypt" \
            certbot/dns-nsone renew \
            --dns-nsone \
            --dns-nsone-credentials /etc/letsencrypt/dns.ini \
            --dns-nsone-propagation-seconds 10 \
            --agree-tos
        echo "Renewing certificate...complete!"
    fi
    
    if cmp -s "$certbot_cert" "$nginx_cert";
    then
        echo "Certificate is already up-to-date."
    else
        echo "Updating certificate..."
        rm -rf $nginx_dir
        mkdir -p $nginx_dir
        cp $certbot_chain $nginx_chain
        cp $certbot_cert $nginx_cert
        cp $certbot_key $nginx_key
        cp $certbot_fullchain $nginx_fullchain
        chown -R chris:chris /data/nginx
        chmod -R 755 $nginx_dir
        echo "Done!"
    fi
    
The execution

Now that all the preliminary work is done, lets stand the system up.

  1. Get a digital cert from Let's Encrypt. Ensure it completes successfully.
    sudo ~/docker/certbot-script.sh
    
  2. Start up the redis and mastodon-db portions of the system. Once they both indicate they are "ready for connections" you can ctrl-c and move to the next step (Postgres will emit: "database system is ready to accept connections" and Redis will emit "Ready to accept connections").
    docker-compose -f ~/docker/docker-compose.yml up redis mastodon-db
    
  3. Grab the secret keys needed for the mastodon server. Run this command twice:
    docker run --rm -it --entrypoint /bin/bash lscr.io/linuxserver/mastodon generate-secret
    
  4. Grab the Vapid keys needed for the mastodon server. Run this once:
    docker run --rm -it --entrypoint /bin/bash lscr.io/linuxserver/mastodon generate-vapid
    
  5. Take the secrets needed from the previous two commands and put them into the docker-compose.yml file created earlier into the "Change this later" values. The secret key base and OTP secret don't matter which one is which.
  6. Start all containers. The database objects will be created into the database. It'll take a few minutes for everything to settle down. Once you see the cessation of activity, it is safe to ctrl-c and exit the docker processes.
    docker-compose -f ~/docker/docker-compose.yml up
    
  7. If there were any errors encountered in the above, fix them and repeat the step until you aren't getting any errors.
  8. Once everything looks good, you can start the containers in "background mode":
    docker-compose -f ~/docker/docker-compose.yml up -d
    
  9. Create the "owner" of the mastodon system - save the temporary password to use in the next step.
    docker exec -it mastodon tootctl accounts create [name] --email [email-address] --confirmed --role Owner	    ## Replace [name] and [email-address]
    
  10. You should be able to navigate to the web site, log in with the user created above. Don't forget to change the temporary password.
Epilog

There are a couple of things I'll go back and tweak - there are some areas of "verboseness" that could be simplified - where I opted for quick cut & paste, instead of using variables (like in the certbot script).

I hope you found this helpful and got you running quickly. If you want to buy me a coffee, that'd be great. As always, feel free to hit me up on Mastodon.



Tags: Mastodon, Docker, Postgres, nginx, Lets Encrypt

← Back home