No description
Find a file
2024-10-25 17:28:07 +01:00
img initial commit 2024-10-25 17:28:07 +01:00
README.md initial commit 2024-10-25 17:28:07 +01:00

Building my Mastodon Setup with Docker

So many admins of mastodon instances prefer to use a server and install the OS and mastodon straight onto it. I on the other hand prefer to containerize my setup with docker. I find it neater and easier to upgrade, plus if I want to run other services on the same server I'm free to do so without messing up dependancies or versions. Now there are some ok(ish) guides on how to do this out there but I wanted pull together a few guides in order to produce a complete setup, a traefik reverse proxy in front of mastodon with full text search and translation enabled using libretranslate, I also wanted to use object storage for my assets, in my case I'm running on AWS so I'm using S3. I also set myself the challenge of removing software with dubious licenses such as Redis and ElasticSearch and opting for the more FOSS friendly Valkey and OpenSearch componants. So here is my setup.

The Setup

A picture speaks a thousand words so let me show you the main companants of this setup. Each bit of software is running in it's own container:

flowchart TD
    Users --> |HTTPS port: 80/443| A
    A[Traefik] -->|Web Interface port: 3000| B(mastodon)
    A -->|Streaming port: 4000| C(mastodon-streaming)
    B --> D(sidekiq)
    C --> D
    B --> E[postgres]
    C --> E
    B --> F[valkey]
    C --> F
    B --> G[opensearch]
    B --> H[S3]
    B --> I[libretranslate]
    Users --> | assets | H

Now the easiest way to run all these companants and have them interconnected is to set up the entire system with docker compose but before we get going we are going to have to set up the basics. Now I built this on a debian 12 system but most of the compose setup will be identical, you'll just need to tweak the install commands for docker and compose.

Install Docker and Compose

  1. First lets make sure theres no unoffical packages in place that will conflict by removing them.
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done
  1. Now lets set up dockers official apt repository:
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
  1. Now install the tool chain:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin vim
  1. Verify that the installation is successful by running the hello-world image:
sudo docker run hello-world

👉 Note

If you want to run docker as a none root user you'll need to add that user to the docker group by running the following command: sudo usermod -aG docker $USER

Setup storage directories on the host

I like to try and keep the persistant data neat and tidy on my system so I make sure group my configs and data directories together. For this I store them in /opt/containers/<application_name> in this case mastodon

  • Let's start by creating that folder structure, for ease we are going to use sudo to become root for these commands(you could just prefix all instructions with sudo if you really wanted):
sudo su -p
mkdir -p /opt/docker/traefik
mkdir -p /opt/docker/mastodon
cd /opt/docker/mastodon
  • Now we are in the main directory lets create the directories where the conatiners are going to write their persistant data:
cd /opt/docker/traefik
mkdir data
mkdir logs
cd /opt/docker/mastodon
mkdir -p public/system
mkdir postgres
mkdir -p opensearch/data
mkdir valkey
mkdir -p lt/data/{key,local}
mkdir lt/api_keys
  • Now lets set the permissions on those directories so the containers can write their data to them without having to tweak the containers:
chown -R 991:991 public
chown -R 70:root postgres
chown -R 1000:root opensearch
chown -R 999:root valkey
chown -R 1032:1032 lt

Setting up traefik

To access your site and ensure you have a valid SSL certificate we are going to run traefik as a reverse proxy, this means we can also force all traffic to be HTTPS which is great for security. Traefik will automtically take care of getting a cert from LetsEncrypt and rotating it when its due to expire. All you have to do is provide an email address for registration.

Ensure your in the /opt/docker/traefik directory and create a new file call compose.yml

vi compose.yml

now add the follwoing to the file:

💡 Tip

To enter insert mode in vim press i

services:
  traefik:
    image: "traefik:latest"
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.traefik.entrypoints=web"
      - "traefik.http.routers.traefik.rule=Host(`${HOSTNAME}`)"
      - "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_PASSWORD}"
      - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
      - "traefik.http.routers.traefik-secure.entrypoints=websecure"
      - "traefik.http.routers.traefik-secure.rule=Host(`${HOSTNAME}`)"
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=myresolver"
      - "traefik.http.routers.traefik-secure.service=api@internal"
      # Define the port inside of the Docker service to use
      - "traefik.http.services.traefik-secure.loadbalancer.server.port=8080"
    env_file: .env
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /opt/docker/traefik/logs:/logs:rw
      - /opt/docker/traefik/data/acme.json:/acme.json:rw
      - /opt/docker/traefik/data/traefik.yml:/traefik.yml:rw
      - /opt/docker/traefik/data/config.yml:/config.yml:rw
    networks:
      - proxy

networks:
  proxy:
    external: false

Save and exit the file.

💡 Tip

To get out of vim just type :wq!

You're also going to need to set up some environment variables for this to work so once you've saved the file above you'll need to create a new file called .env

vi .env

Populate it with these details, and remember to update the domain:

HOSTNAME=traefik.example.com
TRAEFIK_PASSWORD=admin:<GENERATED_PASSWORD>

You'll need to generate a password for traefik basic_auth to be able to login to the dashboard API. you can do that with the following command:

echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g

Paste the output user:password keypair into your .env file and save and exit.

Theres a couple of other files (4 actually) we now need to prep. 3 are going to be empty files that the container will need and one will be the initial config startup for traefik.

cd /opt/docker/traefik/data
touch dynamic-config.yml
touch acme.json
cd /opt/docker/traefik/logs
touch traefik.log

Now for the important file to pull all this together and get traefik working.

vi traefik.yml

Enter the following information and update your email address:

api:
  dashboard: true
  debug: true

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

log:
  level: ERROR
  filePath: "/logs/traefik.log"
  format: common

serversTransport:
  insecureSkipVerify: true

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedbydefault: false
  file:
    filename: dynamic-config.yml

certificatesResolvers:
  myresolver:
    acme:
      email: <YOUR_EMAIL_ADDRESS>
      storage: acme.json
      httpChallenge:
        # used during the challenge
        entryPoint: web

Now traefik is ready to run and accept HTTP and HTTPS connections. To get it up and running is pretty simple with docker compose, run the following command:

cd /opt/docker/traefik
docker compose up -d

Test Traefik

To test traefik you can browse direction to https://traefik.example.com where you will be prompted to enter your basic_auth details in your browser.

ScreenSHot of Basic Auth Prompt

Setup object storage

Create a S3 Bucket
Generate an IAM key
Setup Cloudfront

Setting up email with SES

Setup + Verify Domain
Generate SMTP credentials

Create configuration files

This first file is the all important docker compose file, so lets go ahead and create compose.yml and paste in the following content.

cd /opt/docker/mastodon
touch .env.production
touch .env.translate
vi compose.yml

Now lets paste in the following content:

services:
  db:
    restart: unless-stopped
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - mastodon
    healthcheck:
      test:
        - CMD
        - pg_isready
        - -U
        - postgres
    volumes:
      - /opt/docker/mastodon/postgres:/var/lib/postgresql/data
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
  valkey:
    restart: unless-stopped
    image: valkey/valkey:7-alpine
    networks:
      - mastodon
    healthcheck:
      test:
        - CMD
        - valkey-cli
        - ping
    volumes:
      - /opt/docker/mastodon/valkey:/data
  web:
    image: richarvey/mastodon-bird-ui:latest
    restart: unless-stopped
    env_file: .env
    #command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
    command: bundle exec puma -C config/puma.rb
    networks:
      - traefik_proxy
      - mastodon
    healthcheck:
      test:
        - CMD-SHELL
        - wget -q --spider --proxy=off localhost:3000/health || exit 1
    depends_on:
      - db
      - valkey
      - os
    volumes:
      - /opt/docker/mastodon/public/system:/mastodon/public/system
    labels:
      - traefik.enable=true
      - traefik.http.routers.mastodon.entrypoints=web
      - traefik.http.routers.mastodon.rule=Host(`mastodon.squarecows.com`)
      - traefik.http.middlewares.mastodon-https-redirect.redirectscheme.scheme=https
      - traefik.http.routers.mastodon.middlewares=mastodon-https-redirect
      - traefik.http.routers.mastodon-secure.entrypoints=websecure
      - traefik.http.routers.mastodon-secure.rule=Host(`mastodon.squarecows.com`)
      - traefik.http.routers.mastodon-secure.tls=true
      - traefik.http.routers.mastodon-secure.tls.certresolver=myresolver
      - traefik.http.routers.mastodon-secure.service=mastodon
      - traefik.http.services.mastodon.loadbalancer.server.port=3000
      - traefik.docker.network=traefik_proxy
  streaming:
    image: ghcr.io/mastodon/mastodon-streaming:v4.3.1
    restart: unless-stopped
    env_file: .env
    command: node ./streaming/index.js
    networks:
      - traefik_proxy
      - mastodon
    healthcheck:
      test:
        - CMD-SHELL
        - wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health ||
          exit 1
    depends_on:
      - db
      - valkey
    labels:
      - traefik.enable=true
      - traefik.http.routers.mastodon-api.entrypoints=web
      - traefik.http.routers.mastodon-api.rule=(Host(`mastodon.squarecows.com`)
        && PathPrefix(`/api/v1/streaming`))
      - traefik.http.middlewares.mastodon-api-https-redirect.redirectscheme.scheme=https
      - traefik.http.routers.mastodon-api.middlewares=mastodon-api-https-redirect
      - traefik.http.routers.mastodon-api-secure.entrypoints=websecure
      - traefik.http.routers.mastodon-api-secure.rule=(Host(`mastodon.squarecows.com`)
        && PathPrefix(`/api/v1/streaming`))
      - traefik.http.routers.mastodon-api-secure.tls=true
      - traefik.http.routers.mastodon-api-secure.tls.certresolver=myresolver
      - traefik.http.routers.mastodon-api-secure.service=mastodon-api
      - traefik.http.services.mastodon-api.loadbalancer.server.port=4000
      - traefik.docker.network=traefik_proxy
  sidekiq:
    #image: richarvey/mastodon-bird-ui:latest
    image: ghcr.io/mastodon/mastodon:v4.3.1
    restart: always
    env_file: .env
    command: bundle exec sidekiq
    depends_on:
      - db
      - valkey
      - os
    networks:
      - mastodon
    volumes:
      - /opt/docker/mastodon/public/system:/mastodon/public/system
    healthcheck:
      test:
        - CMD-SHELL
        - ps aux | grep '[s]idekiq 6' || false
  os:
    restart: always
    image: opensearchproject/opensearch:latest
    container_name: opensearch-node1
    environment:
      - node.name=opensearch-node1 # Name the node that will run in this container
      - discovery.type=single-node
      - bootstrap.memory_lock=true # Disable JVM heap memory swapping
      - DISABLE_SECURITY_PLUGIN=true
      - OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m # Set min and max JVM heap sizes to at least 50% of system RAM
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=<OPENSEARCH_PASSWORD>
    networks:
      - traefik_proxy
      - mastodon
        #healthcheck:
        #test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
    volumes:
      - /opt/docker/mastodon/opensearch/data:/usr/share/opensearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    ports:
      - 9200:9200
      - 9600:9600
  libretranslate:
    image: libretranslate/libretranslate:v1.6.1
    restart: unless-stopped
    healthcheck:
      test:
        - CMD-SHELL
        - ./venv/bin/python scripts/healthcheck.py
    env_file:
      - .env
    volumes:
      - /opt/docker/mastodon/lt/data:/home/libretranslate/.local
      - /opt/docker/mastodon/lt/api_keys:/app/db
    networks:
      - traefik_proxy
      - mastodon
    ports:
      - 5000:5000
networks:
  mastodon:
    external: false
  traefik_proxy:
    external: true

👉 Note

There are a few variables you'll need to setup in the above script to match your environment and secure it, make sure you change these values.

  • <POSTGRES_PASSWD> *1
  • <URL_MASTODON_URL> *4
  • <OPENSEARCH_PASSWORD> *1

Once you've changed these variables you'll need to save and exit vim.

Setting up the environment variables

Now to make this easy I've included a skeleton .env.production file for you to copy and populate with your own variables. But first we need to generate some keys and setup the DB.

Generate keys for mastodon

First we'll need to generate some keys you'll need to secure mastodon and you'll need to run the following command twice once for the SECRET_BASE_KEY and once for OTP_SECRET.

👉 Note

make a note of them you'll need them for the config file

docker compose run --rm -- web bundle exec rails secret

Now we need to generate the VAPID_PRIVATE_KEY.

👉 Note

yet again make a note of this output

docker compose run --rm -- web bundle exec rails mastodon:webpush:generate_vapid_key

Initialize the database

Now we need to start setting up the database ready for mastodon to use. Run the following command which will create the DB and populate the initial tables:

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

Generate DB AR Encryption Values

Since mastodon 4.3.0 you'll also need to set up Active Record (AR) encryption keys. The following command will generate you the required keys.

docker compose run --rm -- web bundle exec RAILS_ENV=production rails db:encryption:init

👉 Note

Make a note of these values as we'll need them for the configuration file.

Setup Libre Translate

Mastodon allows you to translate posts into your native language by using libretranslate. You can run this service locally in your stack with very little setup. Let's look at the steps

Create the environments file
vi libretranslate.env

and enter the following details, you don't need to edit anything in this file:

LT_DEBUG=true
LT_UPDATE_MODELS=true
LT_SSL=true
LT_SUGGESTIONS=false
LT_METRICS=true

LT_API_KEYS=true

LT_THREADS=12
LT_FRONTEND_TIMEOUT=2000

#LT_REQ_LIMIT=400
#LT_CHAR_LIMIT=1200

LT_API_KEYS_DB_PATH=/app/db/api_keys.db
Run the container

Now lets start the container ready to set up the required files and generate an API key.

docker compose exec -it lt bash
Install all the language packs

We now need to download the language packs for libretranslate to use.

for i in `/app/venv/bin/argospm list`;do /app/venv/bin/argospm install $i;done

👉 Note

Note: this will take a fair bit of time so please do be patient

Generate an API key

Mastodon will need an API key to access libretranslate. Still within the container lets create an API key that can make 120 requests per min:

/app/venv/bin/ltmanage keys add 120

👉 Note

Make a note of the output as this is the API key

Create the mastodon environment variables

We now need to gather those credentials we've just created and edit our .env.production file. Open that file and copy the below contents into that file and change the values in the < >.

vim .env.production

Contents:

LOCAL_DOMAIN=<URL_MASTODON_URL>
SINGLE_USER_MODE=false
SECRET_KEY_BASE=<GENERATED_SECRET_KEY_BASE>
OTP_SECRET=<GENERATED_OTP_SECRET>
VAPID_PRIVATE_KEY=<GENERATED_VAPID_PRIVATE_KEY>
VAPID_PUBLIC_KEY=<GENERATED_VAPID_PUBLIC_KEY>
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=<DB_PASSWORD>
REDIS_HOST=valkey
REDIS_PORT=6379
# This is blank on purpose
REDIS_PASSWORD=
S3_ENABLED=true
S3_PROTOCOL=https
S3_BUCKET=<AWS_S3_BUCKET>
S3_REGION=<AWS_REGION>
S3_HOSTNAME=<s3.AWS-REGION-1.amazonaws.com>
AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY>
AWS_SECRET_ACCESS_KEY=<AWS_SECRET_KEY>
# Only needed if you use cloudfront
S3_ALIAS_HOST=<CLOUDFRONT_URL>
SMTP_SERVER=<EMAIL_SERVER>
SMTP_PORT=587
SMTP_LOGIN=<EMAIL_USERNAME>
SMTP_PASSWORD=<EMAIL_PASSWORD>
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
SMTP_FROM_ADDRESS=<EMAIL_ADDRESS>
ES_ENABLED=true
ES_HOST=http://os
ES_PORT=9200
ES_PRESET=single_node_cluster
ES_USER=admin
ES_PASS=<OPENSEARCH_PASSWORD>
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<GENERATED_AR_KEY>
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<GENERATED_AR_SALT>
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<GENERATED_AR_PRIMARY_KEY>
LIBRE_TRANSLATE_ENDPOINT=http://libretranslate:5000
LIBRE_TRANSLATE_API_KEY=<LT_API_KEY>

enable admin

docker compose exec web /bin/bash
tootctl accounts deploy -approve <ADMIN_NAME>

enable opensearch

docker compose exec web /bin/bash
tootctl search deploy

get rid of yellow cluster warning

docker compose exec os /bin/bash
curl -X PUT -u admin:<OPENSEARCH_PASSWORD> "http://os:9200/_settings" -H 'Content-Type: application/json' -d'{
    "index" : {
        "number_of_replicas" : 0
    }
}'

libretranslate

https://blog.gcn.sh/howtos/integrating-mastodon-and-libretranslate