How to self host Mastodon on docker compose

Mastodon is Free and open source social media software and it's alternative for twitter.

It's easy to host it but before I start I will assume that: * You are running debian 11 * You already have a VPS: https://blog.esmailelbob.xyz/how-to-get-a-vps * Your linux distro is up-to-date (sudo apt update && sudo apt upgrade) * You have a domain name: https://blog.esmailelbob.xyz/how-to-get-a-domain-name * Have sudo access or root account * Already installed docker and docker-compose: https://blog.esmailelbob.xyz/how-to-install-docker-and-docker-compose * Already installed Nginx: https://blog.esmailelbob.xyz/how-to-install-and-configure-nginx-96yp * Already have a reverse proxy conf file: https://blog.esmailelbob.xyz/how-to-use-reverse-proxy-with-nginx * Already have certbot to issue cert: https://blog.esmailelbob.xyz/how-to-use-certbot-with-nginx-to-make-your-website-get

Changes in DNS (domain side)

You really do not need to add any dns entries except if you want to create subdomain for this container then you go in your domain's dns panel and add either CNAME entry that looks like subdomain.domain.com and make it's target the root domain domain.com or A entry with subdoamin.domain.com and make it's target the IP of your VPS

mastodon docker-compose file

After we finish with .env.docker file, We need a docker-compose.yml file so we can start mastodon, for me I use this file:

version: '3'
services:

  db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - internal_network
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
    volumes:
      - ./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:
      - ./redis:/data

#  es:
#    restart: always
#    image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
#    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:
#      - ./elasticsearch:/usr/share/elasticsearch/data
#    ulimits:
#      memlock:
#        soft: -1
#        hard: -1

  web:
#    build: .
    image: tootsuite/mastodon:latest
    restart: always
    env_file: .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 127.0.0.1:3000/health || exit 1"]
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      - db
      - redis
#      - es
    volumes:
      - ./MASTODON_DATA:/mastodon/public/system
      - ./MASTODON_DATA:/mastodon/public/assets
      - ./Mastomoji.tar.gz:/opt/mastodon/Mastomoji.tar.gz
  streaming:
#    build: .
    image: tootsuite/mastodon:latest
    restart: always
    env_file: .env.production
    command: node ./streaming
    networks:
      - external_network
      - internal_network
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy=off 127.0.0.1:4000/api/v1/streaming/health || exit 1"]
    ports:
      - "127.0.0.1:4000:4000"
    depends_on:
      - db
      - redis

  sidekiq:
#    build: .
    image: tootsuite/mastodon:latest
    restart: always
    env_file: .env.production
    command: bundle exec sidekiq
    depends_on:
      - db
      - redis
    networks:
      - external_network
      - internal_network
    volumes:
      - ./MASTODON_DATA:/mastodon/public/system
## Uncomment to enable federation with tor instances along with adding the following ENV variables
## http_proxy=http://privoxy:8118
## ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
#  tor:
#    image: sirboops/tor
#    networks:
#      - external_network
#      - internal_network
#
#  privoxy:
#    image: sirboops/privoxy
#    volumes:
#      - ./priv-config:/opt/config
#    networks:
#      - external_network
#      - internal_network

networks:
  external_network:
  internal_network:
    internal: true

Optional stuff to change: ./MASTODON_DATA: This is where our posts and photos will be saved at

NOTE: Do not change ports of mastodon, I tried to change them and it did not work so if you have another website running on same port, change port for other website not mastodon.

Prepare Mastodon

Now we need to prepare .env.production so we can edit it and configure mastodon:

cp .env.production.sample .env.production

Now let's pull image from dockerhub:

docker-compose build

And correct the impression of mastodon folders:

sudo chown -R 991:991 public/system

Spin it up!

Now we are done with docker-compose file and prepared it, We need to setup mastodon, to setup mastodon database and generate secret keys and other stuff

docker-compose run --rm web bundle exec rake mastodon:setup

Answer the questions and after you done, it will show you configuration for mastodon in terminal so copy it and paste it in the .env.production file

NOTE: You might want to keep database url and port and username and password same as it's (and redis data too) while you interact with the wizard

Now it's time to run mastodon! so:

docker-compose up -d

the -d option does not let docker post logs of running application but if you want to see logs you can run:

sudo docker-compose logs -f -t

To check if there any weird behaviors or errors.

Nginx

Now to the tricky part number 2! Now after we make sure it's running well. We need to serve it over the internet (called reverse proxy) so we need to first make a dummy nginx website that got same domain name as we will use for mastodon then generate https certificate using certbot and after it's all setup you may now paste this to your mastodon nginx server block:

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}
server {
  server_name [mastodon domain name] ;

#  ssl_protocols TLSv1.2;
#  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
#  ssl_prefer_server_ciphers on;

  #ssl_session_cache shared:SSL:10m;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;

  root /home/mastodon/live/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;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri @proxy;
  }
  
  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    try_files $uri @proxy;
  }

  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 https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  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 https;
    proxy_set_header Proxy "";

    proxy_pass http://127.0.0.1:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;


    add_header Onion-Location http://social.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd.onion$request_uri;
  #root /home/mastodon/live/public;
  # Useful for Let's Encrypt
  #location /.well-known/acme-challenge/ { allow all; }
  #location / { return 301 https://$host$request_uri; }



    listen [::]:443 ssl; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/[mastodon domain name]/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/[mastodon domain name]/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {
        listen 127.0.0.1:80 ;
        server_name social.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd.onion ;
#  ssl_protocols TLSv1.2;
#  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
#  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;

  root /home/mastodon/live/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;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri @proxy;
  }
  
  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    try_files $uri @proxy;
  }

  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 https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  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 https;
    proxy_set_header Proxy "";

    proxy_pass http://127.0.0.1:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}


server {
    if ($host = [mastodon domain name]) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


  server_name [mastodon domain name] ;

    listen [::]:80;
    listen 80;
    return 404; # managed by Certbot


}

[mastodon domain name]: Replace this with your mastodon's domain name

Update it

Of course after some time the image will be outdated and you need to update and what I love about docker that it's easy to update, really just to do it run:

docker-compose down && docker-compose pull && docker-compose up -d

What it does is: 1) Stops the container, 2) Pull last update (download last update) and 3) Re-run the container back!

And if you get notification about migrate your database, simply run:

docker-compose run --rm web bundle exec rake db:migrate

Firewall

If you use firewall (ufw for example) you really do not need any ports other than 443 and 80 as we use nginx reverse proxy

To add an onion link so you offer your instance on both tor and clear net, simply open .env.production file and add this line:

ALTERNATE_DOMAINS=[YOUR ONION DOMAIN HERE OR REALLY ANY OTHER DOMAIN]

save and restart docker and it should work

#howto #selfhost #docker