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.
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)Open the docker-compose.yml file in your favorite text editor.
- Comment out the
build: .lines for all images (web, streaming, sidekiq). You can do this by adding:#before those lines. - Edit the
image: tootsuite/mastodonlines for all images to include the release you want. The default islatestwhich 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 - Do maybe want to change the default volume mount location in the Docker compose file, see the
websection. Default is./public/system. If you change this to another location (let's say:/var/www/mastodon/public/system). Update thevolumessection of thewebsection to:
volumes:
- /var/www/mastodon/public/system:/mastodon/public/system- Save the
docker-compose.ymlfile and exit your text editor.
We will now create the web folder on your host machine, we specified earlier in the docker-compose.yml file.
- To create this folder, execute:
mkdir -p /var/www/mastodon/public/systemOr when using the default settings, create a local public/system folder hierarchy:
mkdir -p public/system- 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- 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
- You could use the
mastodon:setuptask to help you generate several important configuration variables for the.env.productionfile.Hint: If it asks you to "Save configuration?" You can say: No/N.docker run -it --rm tootsuite/mastodon bundle exec rake mastodon:setup - Open your local
.env.productionfile and apply the generated settings. Pay extra attention to the following settings:LOCAL_DOMAIN,DB_*,ES_ENABLED,S3_ENABLED,SECRET_KEY_BASEandOTP_SECRET,VAPID_PRIVATE_KEYandVAPID_PUBLIC_KEY.LOCAL_DOMAINshould point to your public (sub)domain you want to use to serve Mastodon on.- PostgreSQL config for Docker should be:
DB_HOST=db DB_USER=postgres DB_NAME=postgres DB_PASS= DB_PORT=5432
- Redis can be set to
redisfor Docker:REDIS_HOST=redis - Set both
ES_ENABLEDandS3_ENABLEDto false, if you aren't using Elasticsearch or Amazon. - The
SECRET_KEY,OPT_SECRET,VAPID_*_KEY's are generated by the setup, which you can copy one-by-one. - 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[email protected] SMTP_PASSWORD=your_password SMTP_AUTH_METHOD=plain SMTP_OPENSSL_VERIFY_MODE=none SMTP_ENABLE_STARTTLS=auto SMTP_FROM_ADDRESS=Mastodon <[email protected]>
- When running in production it's advised to log level to
warn(defaultinfo):RAILS_LOG_LEVEL=warn - 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
- Save all the changes to
.env.productionand exit your text editor.
More info about the configuration environment settings.
Continue with the Database setup, chapter below.
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:setupIf the database setup went successfully; well done! We can now start Mastodon for the first time.
You can launch Mastodon with:
docker-compose upIf 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 -dImportant: You can now go to the last step, setting-up Nginx as your reverse proxy. See below "Nginx Setup".
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=5432As discussed earlier, Redis can be set to redis when using Docker compose:
REDIS_HOST=redisAs 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[email protected]
SMTP_PASSWORD=your_password
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS=auto
SMTP_FROM_ADDRESS=Mastodon <[email protected]>Generate an Application password at your Google Account settings. Try to avoid using your account password directly.
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:
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.
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.confAnd finally restart the Nginx server:
sudo systemctl restart nginxThis 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.
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.
Creating a Let's encrypt certificate easy. Just select the domain from the list after executing:
sudo certbot --nginxWhen you are using the latest stable release of certbot your certificates will be renewed automatically.
- https://github.com/mastodon/mastodon
- Unmodified Nginx config example: https://github.com/mastodon/mastodon/blob/main/dist/nginx.conf
- https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Docker-Guide.md
- https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md

Worked well, but:
I had to use the configuration there as certbot destroyed it:
So thank you for posting it. During the installation, the admin user could not be created, so I had to manually do it via tootctl:
docker compose exec web bin/tootctl accounts create USERNAME --email EMAIL --confirmed --role Ownerand
docker compose exec web bin/tootctl accounts approve USERNAMEThank you a million times for the thread starter for writing this and the other commenters, all other guides on this are really bad and don't work.