Self-host Mastodon with Docker: PostgreSQL, Redis, SSL, and Nginx
TABLE OF CONTENTS (Nhấn để xem)
Mastodon is federated, so this tutorial is more than one Docker container: it needs a stable domain, PostgreSQL, Redis, secrets, VAPID, streaming, Sidekiq, and an HTTPS reverse proxy. LOCAL_DOMAIN should not be changed casually after the instance is in use.1
1. Domain and Docker prerequisites
The Certbot host must be reachable from the Internet on port 80 or 443 for Let’s Encrypt validation. In a HomeLab I forward port 80 to the host during the handshake, verify DNS, and stop any web server already using port 80.
Install Docker with the source block:
# install prerequisites
sudo apt install apt-transport-https ca-certificates curl software-properties-common gnupg-agent -y
# add docker gpg key
curl -fsSL https://download.docker.com/linux/$(awk -F'=' '/^ID=/{ print $NF }' /etc/os-release)/gpg | sudo apt-key add -
# add docker software repository
sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/$(awk -F'=' '/^ID=/{ print $NF }' /etc/os-release) $(lsb_release -cs) stable"
# install docker
sudo apt install docker-ce docker-compose containerd.io -y
# enable and start docker service
sudo systemctl enable docker && sudo systemctl start docker
# add the current user to the docker group
sudo usermod -aG docker $USER
# reauthenticate for the new group membership to take effect
su - $USER
2. Obtain the SSL certificate
Remove the apt Certbot, install snapd/core/Certbot, create the symlink, and run standalone mode with the real domain:
# remove apt version of certbot if installed
sudo apt remove certbot -y
# install snapd
sudo apt install snapd -y
# install snap core and update
sudo snap install core; sudo snap refresh core
# install certbot snap
sudo snap install --classic certbot
# create certbot symbolic link
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# if a web server process is currently using port 80, stop it before proceeding
# generate a certificate
sudo certbot certonly --standalone --preferred-challenges http -d <%DNS NAME%>
When prompted, enter an email, accept the terms, and choose whether to receive Certbot email. Record the certificate file locations it prints.
Create the certificate-reading group as in the source:
# create ssl-certs group
sudo groupadd ssl-certs
# add $USER and root users to group
sudo usermod -aG ssl-certs $USER
sudo usermod -aG ssl-certs root
# verify the members of ssl-cert
getent group ssl-certs
# set owner group of /etc/letsencrypt
sudo chgrp -R ssl-certs /etc/letsencrypt
# set permissions on /etc/letsencrypt
sudo chmod -R g=rX /etc/letsencrypt
3. Prepare Mastodon and .env
Create PostgreSQL, Redis, public system, and Nginx directories; pull the image, run rake secret twice, generate VAPID keys, and open .env:
# create working directories
mkdir ~/docker/postgres -p && mkdir ~/docker/redis -p && mkdir ~/docker/mastodon/public/system -p && mkdir ~/docker/nginx/conf -p
# pull the mastodon web container
docker pull tootsuite/mastodon
# generate secrets, run this 2 times
docker run --rm -it tootsuite/mastodon bundle exec rake secret
# generate VAPID keys
docker run --rm -it tootsuite/mastodon bundle exec rake mastodon:webpush:generate_vapid_key
# create a mastodon .env file
# copy the generated secrets and keys into the .env file
# make sure to set the LOCAL_DOMAIN as this cannot be changed later
nano ~/docker/mastodon/.env
Paste the .env sample and edit LOCAL_DOMAIN, WEB_DOMAIN, PostgreSQL, secret, Web Push, and SMTP settings. Variable names remain intact, but source passwords are not committed; DB_PASS, database passwords, and any SMTP secrets must be local values:1
# This is a sample configuration file. You can generate your configuration
# with the `rake mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you'll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895
# Federation
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=i12bretro.local
# ----------
# Optional, if different than LOCAL_DOMAIN
# ----------
#WEB_DOMAIN=toots.webredirect.org
# Redis
# -----
REDIS_HOST=redis
REDIS_PORT=6379
# PostgreSQL
# ----------
DB_HOST=postgres
DB_USER=mastodon_rw
DB_NAME=mastodon
DB_PASS=[REDACTED]
DB_PORT=5432
# Secrets
# -------
# Make sure to use `rake secret` to generate secrets
# -------
SECRET_KEY_BASE=
OTP_SECRET=
# Web Push
# --------
# Generate with `rake mastodon:webpush:generate_vapid_key`
# --------
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=
# Sending mail
# ------------
SMTP_SERVER=smtp.example.com
SMTP_PORT=25
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=mastodon@example.com
# IP and session retention
# -----------------------
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800).
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
Press CTRL+O, Enter, CTRL+X to save .env. Mastodon’s full configuration documentation is an additional source linked by i12bretro; the compact sample is not a complete production policy.
4. Run PostgreSQL, Redis, and Mastodon
Create the network, start the database, run the migration, launch the frontend, create an owner/admin, then start streaming and Sidekiq. Replace the username/email placeholders and store the generated password safely; do not write it into Markdown:1
# set owner of docker directory
sudo chown "$USER":"$USER" ~/docker -R
# create containers
docker network create containers
# run the postgesql container
# Replace the source password value with your own local secret before running.
docker run -d --name postgres -e POSTGRES_USER=mastodon_rw -e POSTGRES_PASSWORD=[REDACTED] -e POSTGRES_DB=mastodon -v ~/docker/postgres:/var/lib/postgresql/data --network containers --restart=unless-stopped postgres:latest
# run the redis container
docker run -d --name redis -v ~/docker/redis:/data --network containers --restart=unless-stopped redis
# initialize the mastodon database
docker run --rm -it --network containers --env-file ~/docker/mastodon/.env tootsuite/mastodon rails db:migrate
# run the mastodon frontend container
docker run -d --name mastodon --env-file ~/docker/mastodon/.env -p 3000:3000 -v ~/docker/mastodon/public/system:/mastodon/public/system --network containers --restart=unless-stopped tootsuite/mastodon bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
# connect to shell inside mastodon container
docker exec -it mastodon /bin/bash
# set the RAILS_ENV variable
RAILS_ENV=production
# create an owner/admin account
# copy the password output for later
bin/tootctl accounts create <%username%> --email <%email address%> --confirmed --role Owner
# exit the container
exit
# run the mastodon streaming container
docker run -d --name mastodon-stream --env-file ~/docker/mastodon/.env -p 4000:4000 --network containers --restart=unless-stopped tootsuite/mastodon node ./streaming
# run the mastodon sidekiq container
docker run -d --name mastodon-sidekiq --env-file ~/docker/mastodon/.env --network containers -v ~/docker/mastodon/public/system:/mastodon/public/system --restart=unless-stopped tootsuite/mastodon bundle exec sidekiq
5. Nginx reverse proxy
Download Mastodon’s official Nginx configuration, update upstreams for the Docker network, replace the domain and certificate paths, and create the proxy container:
# download the default mastodon nginx configuration
wget -O ~/docker/nginx/conf/mastodon.conf https://raw.githubusercontent.com/mastodon/mastodon/main/dist/nginx.conf
# replace some options to work running in docker containers
sed -i "s/try_files \$uri =404;/try_files \$uri @proxy;/" ~/docker/nginx/conf/mastodon.conf
# update the server_name with the URL being used to reach mastodon
# make sure to replace WEB_DOMAIN
sed -i "s/server_name example.com;/\server_name <%WEB_DOMAIN%>;/" ~/docker/nginx/conf/mastodon.conf
# update mastodon frontend server
sed -i 's/server 127.0.0.1:3000/server mastodon:3000/' ~/docker/nginx/conf/mastodon.conf
# update mastodon stream server
sed -i 's/server 127.0.0.1:4000/server mastodon-stream:4000/' ~/docker/nginx/conf/mastodon.conf
# update the ssl certificate path
# make sure to replace DNS NAME
sed -i 's/# ssl_certificate\s*\/etc\/letsencrypt\/live\/example.com\/fullchain.pem;/ssl_certificate\t\/etc\/letsencrypt\/live\/<%DNS NAME%>\/fullchain.pem;/' ~/docker/nginx/conf/mastodon.conf
# update the ssl key path
# make sure to replace DNS NAME
sed -i 's/# ssl_certificate_key\s*\/etc\/letsencrypt\/live\/example.com\/privkey.pem;/ssl_certificate_key\t\/etc\/letsencrypt\/live\/<%DNS NAME%>\/privkey.pem;/' ~/docker/nginx/conf/mastodon.conf
# create nginx proxy container
docker run --name nginx -p 80:80 -p 443:443 --network containers -v ~/docker/nginx/conf:/etc/nginx/conf.d:ro -v /etc/letsencrypt:/etc/letsencrypt:ro -d nginx
Read mastodon.conf after each sed; replace <%WEB_DOMAIN%> and <%DNS NAME%> with the actual domain before running the container. Check that only the required proxy listens on ports 80/443.
6. Log in and change the password
Open https://<%WEB_DOMAIN%>, select Sign in, and use the owner email/password. Go to Preferences → Account, enter the current password, set and confirm a new password, click Save Changes, log out, and log in again.
Post-deployment checks
Check docker ps, each container’s logs, the certificate chain, HTTPS redirect, PostgreSQL/Redis health, streaming, and Sidekiq. Back up ~/docker/mastodon and .env outside the repository with restrictive permissions; do not expose ports 3000/4000 directly and never commit .env.
Sources
https://i12bretro.github.io/tutorials/0853.html — i12bretro tutorial 0853. ↩︎ ↩︎ ↩︎