Mastodon Setup with Docker and nginx-proxy
Learn how to setup a Mastodon server using Docker and nginx-proxy. This setup comes with automated SSL.
I have been working on a setup with Mastodon that is easy to repeat and share. A setup with very few steps. Please consider that this setup is not enough for a production environment. It requires additional security measures. Please put your recommendations in the comments!
Our starting point is the docker-compose.yml shipped with the Mastodon code. Why is it not enough? It assumes you setup up proxy with HTTPS endpoints yourself. So let’s integrate this as well in Docker.
Consider also the compact setup with the Caddy webserver.
Setup
Few remarks to start with:
- my testing system: Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-97-generic x86_64)
- install first some software with
apt install docker docker.io jq git
-
create an unprivileged user account, e.g.
mastodon
adduser --disabled-login mastodon adduser mastodon docker adduser mastodon sudo # optional, remove later su mastodon # switch to that user
-
my docker compose: Docker Compose version v2.2.3 (based on go)
install docker compose in 3 lines:
mkdir -p ~/.docker/cli-plugins curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose chmod +x ~/.docker/cli-plugins/docker-compose
-
my testing domain (for this example): social.host
-
my dot-env file
.env
for docker compose:LETS_ENCRYPT_EMAIL=admin-mail@social.host MASTODON_DOMAIN=social.host
-
I have commented out
build: .
, because I prefer to rely on the official images from Docker Hub. -
With little effort, I enable as well full-text search with elasticsearch.
-
The support of
VIRTUAL_PATH
is brand-new in nginx-proxy. It is not yet in the main branch, so that we rely onnginxproxy/nginx-proxy:dev-alpine
. -
The Mastodon code also ships an nginx configuration. However, nginx-proxy creates much of it as well, so that I currently believe no further configuration is required here. However, nginx-proxy allows to add custom elements to the generated configuration.
- The setup places all databases and uploaded files in the folder
mastodon
# file: 'docker-compose.yml'
version: "3.7"
services:
nginx-proxy:
image: nginxproxy/nginx-proxy:dev-alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- ./nginx/vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- ./nginx/certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx/logs:/var/log/nginx
networks:
- external_network
- internal_network
acme-companion:
image: nginxproxy/acme-companion
container_name: nginx-proxy-acme
volumes_from:
- nginx-proxy
volumes:
- ./nginx/certs:/etc/nginx/certs:rw
- ./nginx/acme:/etc/acme.sh
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
DEFAULT_EMAIL: "${LETS_ENCRYPT_EMAIL}"
networks:
- external_network
db:
restart: always
image: postgres:14-alpine
shm_size: 256mb
networks:
- internal_network
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
volumes:
- .mastodon//postgres14:/var/lib/postgresql/data
environment:
POSTGRES_HOST_AUTH_METHOD: trust
redis:
restart: always
image: redis:6-alpine
networks:
- internal_network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
volumes:
- ./mastodon/redis:/data
# elasticsearch
es:
restart: always
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.10
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- "cluster.name=es-mastodon"
- "discovery.type=single-node"
- "bootstrap.memory_lock=true"
networks:
- internal_network
healthcheck:
test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]
volumes:
- ./mastodon/elasticsearch:/usr/share/elasticsearch/data
ulimits:
memlock:
soft: -1
hard: -1
web:
# build: .
image: tootsuite/mastodon:v3.4.6
restart: always
env_file: mastodon.env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
networks:
- external_network
- internal_network
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:3000/health || exit 1"]
ports:
- "127.0.0.1:3000:3000"
depends_on:
- db
- redis
- es
volumes:
- ./mastodon/public/system:/mastodon/public/system
environment:
VIRTUAL_HOST: "${MASTODON_DOMAIN}"
VIRTUAL_PATH: "/"
VIRTUAL_PORT: 3000
LETSENCRYPT_HOST: "${MASTODON_DOMAIN}"
ES_HOST: mastodon-elastic
ES_ENABLED: true
streaming:
# build: .
image: tootsuite/mastodon:v3.4.6
restart: always
env_file: mastodon.env.production
command: node ./streaming
networks:
- external_network
- internal_network
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1"]
ports:
- "127.0.0.1:4000:4000"
depends_on:
- db
- redis
environment:
VIRTUAL_HOST: "${MASTODON_DOMAIN}"
VIRTUAL_PATH: "/api/v1/streaming"
VIRTUAL_PORT: 4000
sidekiq:
# build: .
image: tootsuite/mastodon:v3.4.6
restart: always
env_file: mastodon.env.production
command: bundle exec sidekiq
depends_on:
- db
- redis
networks:
# - external_network
- internal_network
volumes:
- ./mastodon/public/system:/mastodon/public/system
volumes:
html:
networks:
external_network:
internal_network:
internal: true
With this file in place, create a few more folders and launch the setup of the instance. If the instance has been setup before, a database setup may be enough.
# mastodon
touch mastodon.env.production
sudo chown 991:991 mastodon.env.production
mkdir -p mastodon/public
sudo chown -R 991:991 mastodon/public
mkdir -p mastodon/elasticsearch
sudo chmod g+rwx mastodon/elasticsearch
sudo chgrp 0 mastodon/elasticsearch
# first time: setup mastodon
# https://github.com/mastodon/mastodon/issues/16353 (on RUBYOPT)
docker compose run --rm -v $(pwd)/mastodon.env.production:/opt/mastodon/.env.production -e RUBYOPT=-W0 web bundle exec rake mastodon:setup
# subsequent times: skip generation of config and only setup database
docker compose run --rm -v $(pwd)/mastodon.env.production:/opt/mastodon/.env.production web bundle exec rake db:setup
# launch mastodon
docker compose run -d
# look into the logs, -f for live logs
docker compose logs -f
Mastodon Twitter Crossposter
To setup the Mastodon Twitter Poster for crossposting, add the following services to the docker-compose.yml
- create a
crossposter.env.production
with content adapted from https://github.com/renatolond/mastodon-twitter-poster/blob/main/.env.example -
create a directory
crossposter
- if you prefer to have logs handled by Docker, add to the
crossposter.env.production
alsoRAILS_LOG_TO_STDOUT=enabled
(Github issue)
crossposter-db:
restart: always
image: postgres:14-alpine
container_name: "crossposter-db"
healthcheck:
test: pg_isready -U postgres
environment:
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- ./crossposter/postgres:/var/lib/postgresql/data
networks:
- internal_network
crossposter-redis:
restart: always
image: redis:6.0-alpine
container_name: "crossposter-redis"
healthcheck:
test: redis-cli ping
volumes:
- ./crossposter/redis:/data
networks:
- internal_network
crossposter-web:
restart: always
build: https://github.com/renatolond/mastodon-twitter-poster.git#main
image: mastodon-twitter-poster
container_name: "crossposter-web"
env_file: crossposter.env.production
environment:
ALLOWED_DOMAIN: "${MASTODON_DOMAIN}"
DB_HOST: crossposter-db
REDIS_URL: "redis://crossposter-redis"
networks:
- internal_network
- external_network
expose:
- "3000"
depends_on:
- crossposter-db
crossposter-sidekiq:
restart: always
build: https://github.com/renatolond/mastodon-twitter-poster.git#main
image: mastodon-twitter-poster
container_name: "crossposter-sidekiq"
env_file: crossposter.env.production
environment:
ALLOWED_DOMAIN: "${MASTODON_DOMAIN}"
REDIS_URL: "redis://crossposter-redis"
DB_HOST: crossposter-db
command: bundle exec sidekiq -c 5 -q default
healthcheck:
test: ps aux | grep '[s]idekiq\ 6' || false
networks:
# - external_network
- internal_network
depends_on:
- crossposter-db
- crossposter-redis
The crossposter requires a database setup before the containers can be launched:
docker-compose run --rm crossposter-web bundle exec rake db:setup