Skip to content

Instantly share code, notes, and snippets.

@gvisoc
Last active May 15, 2024 07:48
Show Gist options
  • Save gvisoc/ca2fc29f3050b7d611f4f232bc244a62 to your computer and use it in GitHub Desktop.
Save gvisoc/ca2fc29f3050b7d611f4f232bc244a62 to your computer and use it in GitHub Desktop.

Mastodon en Docker

Este Gist es un resumen de los pasos para:

  1. Preparar una máquina con Ubuntu 22.04 LTS como servidor Docker, con NGINX
  2. Instalar mastodon en docker, mediante el Docker compose oficial
  3. Si estamos migrando desde una VM o desde una máquina física, copiar los ficheros y contenido de las bases de datos de otra instancia de mastodon
  4. Configuración de NGINX (cambios sobre la configuración oficial)
  5. Obtener un certificado
  6. ✨🎉✨
  7. Comandos de mantenimiento modificados para funcionar con docker

Convenios tipográficos:

Los comandos ejecutados en la máquina local se marcan con el prompt 👾, mientras que en el servidor, con $ para un usuario normal, y con % para el usuario root.

Preparar la máquina

En nuestro ordenador personal, de trabajo, generamos una par de claves para hacer login por SSH:

👾 ssh-keygen 
  1. Copiar la clave pública generada en el paso anterior al servidor, una máquina con Ubuntu Server 22.04 LTS recién instalada, con el software completamente actualizado y a la que podemos acceder por ssh:
# cambiar el usuario `ubuntu` y el nombre del servidor `servidor` por 
# los valores que correspondan
👾 scp .ssh/id_rsa.pub ubuntu@servidor:public_key.pub
  1. En el servidor, añadirla al fichero .ssh/authorized_keys
ubuntu@servidor: ~$ mkdir -p .ssh
$ cat public_key.pub >> .ssh/authorized_keys
  1. Comprobar que podemos hacer ssh al servidor sin que nos pida contraseña.
👾 ssh ubuntu@servidor
ubuntu@servidor: ~$ 
  1. Restringir el acceso por ssh usando contraseña, editando el fichero /etc/ssh/sshd_config. Buscar la línea que contiene PasswordAuthentication, y asegurarnos de que se queda descomentada y con valor no:
# (contenido previo del fichero)
# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
#PermitEmptyPasswords no
# (resto del fichero)
  1. Reiniciar el servicio sshd:
$ sudo systemctl daemon-reload
$ sudo systemctl restart ssd.service

En este punto, si intentamos entrar por SSH en el servidor desde una máquina o un usuario que no tenga la clave privada del par generado en el paso 1, debería rechazar la conexión, sin dejarnos siquiera introducir la contraseña:

👽 ssh usuario@ubuntu
ubuntu@servidor: Permission denied (publickey).
  1. Instalar fail2ban y configurarlo de acuerdo a la guía de mastodon
  2. Instalar Docker CE siguiendo las instrucciones de la página oficial. El paquete de docker que viene en los repositorios de Ubuntu es, en realidad, un paquete Snap (un contenedor), y no queremos instalar el entorno de contenedores dentro de otro contenedor.
  3. En lugar de usar ip_tables, vamos a usar firewalld, ya que al parecer simplifica bastante las cosas cuando usamos contenedores docker. Seguir este artículo, que resumidamente efectúa estos pasos:
$ sudo su -
% ufw disable
% apt update && apt -y install firewalld
% systemctl enable firewalld --now
% firewall-cmd --state
running
% vim /etc/docker/daemon.json
# Introducir el contenido siguiente
{
"iptables": false
}
% systemctl restart docker
% firewall-cmd --zone=public --add-masquerade --permanent
% firewall-cmd reload

# Ejecutar el siguiente comando y anotar el nombre del interfaz de red de docker
% ip link show

# Asumiendo que es docker0
% firewall-cmd --permanent --zone=trusted --add-interface=docker0
% firewall-cmd --reload
% systemctl restart docker

# Ejecutar el siguiente comando y anotar el nombre del interfaz de red de docker
% ip link show
# Añadir el interfaz de red principal a la zona pública, asumiendo que es eth0:
% firewall-cmd --permanent --zone=public --add-interface=eth0
% firewall-cmd --reload
# Abrir los puertos 80 y 443, puesto que esta máquina va a alojar el proxy 
# inverso
% firewall-cmd --permanent --zone=public --add-port=80/tcp
% firewall-cmd --permanent --zone=public --add-port=443/tcp
% firewall-cmd --reload

# Salimos del usuario root
% exit
$
  1. Configurar unas DNS para que los contenedores Docker puedan acceder a internet. Para esto volvemos a editar el fichero /etc/docker/daemon.json y añadimos los servidores que queramos en formato lista:
{
"iptables": false,
"dns": ["1.1.1.1"]
}
  1. Instalamos NGINX con apt y certbot como Snap.
$ sudo su -
% apt update && apt-install -y nginx
% systemctl enable nginx --now
% snap install core; snap refresh core
% snap install --classic certbot
% ln -s /snap/bin/certbot /usr/bin/certbot
% exit
ubuntu@servidor: ~$

Instalar Mastodon en Docker

Todos los pasos se llevan a cabo en un usuario normal, que puede hacer sudo. Todos estos pasos están explicados en este artículo: "Mastodon with docker rootless, compose, and nginx reverse proxy".

⚠️ 🚨 No he llevado a cabo los pasos para ejecutarlo todo en modo docker rootless, por eso el enlace nos sitúa directamente en la sección Mastodon setup.

⚠️ 🚨 Si vas a migrar una instancia existente, no ejecutes el paso docker compose run --rm web rake mastodon:setup

  1. Crear una estructura de directorios
ubuntu@servidor: ~$ mkdir -p docker/mastodon
$ cd docker/mastodon
$ mkdir -p postgres14
$ mkdir -p redis
$ mkdir -p public/system
  1. Descargar el fichero docker_compose.yml del repositorio oficial de mastodon, y el ejemplo de .env.production:
$ pwd
/home/ubuntu/docker/mastodon
$ wget https://raw.githubusercontent.com/mastodon/mastodon/main/.env.production.sample
$ cp .env.production.sample .env.production
$ wget https://raw.githubusercontent.com/mastodon/mastodon/main/docker-compose.yml
  1. Crear un fichero .env.production de acuerdo con el proceso de configuración de mastodon, y de acuerdo con nuestra instancia. Probablemente esta sección del artículo de referencia también te sea de utilidad. Lo importante es:
    1. cambiar las ocurrencias de 127.0.0.1 o localhost a los nombres del contenedor de la base de datos.1
    2. el nombre de usuario de la base de datos. En mi ejemplo, a continuación, es postgres.2
Fichero .env.production completo de mi instancia
# Generated with mastodon:setup on 2022-11-05 14:12:35 UTC
LOCAL_DOMAIN=fedi.gvisoc.com
SECRET_KEY_BASE=...
OTP_SECRET=...
VAPID_PRIVATE_KEY=...=
VAPID_PUBLIC_KEY=...=
DB_HOST=mastodon-db
DB_PORT=5432
DB_NAME=mastodon_production
DB_USER=postgres
REDIS_HOST=mastodon-redis
REDIS_PORT=6379
SMTP_SERVER=smtp....com
SMTP_PORT=587
SMTP_LOGIN=g...m
SMTP_PASSWORD=...
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_FROM_ADDRESS=s...@e...e.com
  1. Modificar el fichero docker_compose.yml acorde con la estructura del directorios creada en el paso (1) de esta lista. Opcionalmente, exportar las versiones de las imágenes de Docker a un fichero .env independiente del fichero .env.production, para poder actualizar el software más fácilmente
Fichero .env
MASTODON_TAG=v4.2.8
REDIS_TAG=7-alpine
POSTGRES_TAG=14-alpine
Fichero docker_compose.yml completo He dejado comentadas las secciones propuestas por Mastodon que no estamos utilizando.
version: '3'
services:
db:
    build: .
    restart: always
    env_file: .env.db
    image: postgres:${POSTGRES_TAG}
    container_name: mastodon-db
    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:${REDIS_TAG}
    networks:
    - internal_network
    container_name: mastodon-redis
    healthcheck:
    test: ['CMD', 'redis-cli', 'ping']
    volumes:
    - ./redis:/data

# es:
#   restart: always
#   image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
#   environment:
#     - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true"
#     - "xpack.license.self_generated.type=basic"
#     - "xpack.security.enabled=false"
#     - "xpack.watcher.enabled=false"
#     - "xpack.graph.enabled=false"
#     - "xpack.ml.enabled=false"
#     - "bootstrap.memory_lock=true"
#     - "cluster.name=es-mastodon"
#     - "discovery.type=single-node"
#     - "thread_pool.write.queue_size=1000"
#   networks:
#      - external_network
#      - 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
#     nofile:
#       soft: 65536
#       hard: 65536
#   ports:
#     - '127.0.0.1:9200:9200'

web:
    build: .
    image: ghcr.io/mastodon/mastodon:${MASTODON_TAG}
    container_name: mastodon-web
    restart: always
    env_file: .env.production
    command: bundle exec puma -C config/puma.rb
    networks:
    - external_network
    - internal_network
    healthcheck:
    # prettier-ignore
    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:
    - ./public/system:/mastodon/public/system

streaming:
    build: .
    image: ghcr.io/mastodon/mastodon:${MASTODON_TAG}
    container_name: mastodon-streaming
    restart: always
    env_file: .env.production
    command: node ./streaming
    networks:
    - external_network
    - internal_network
    healthcheck:
    # prettier-ignore
    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

sidekiq:
    build: .
    image: ghcr.io/mastodon/mastodon:${MASTODON_TAG}
    container_name: mastodon-sidekiq
    restart: always
    env_file: .env.production
    command: bundle exec sidekiq
    depends_on:
    - db
    - redis
    networks:
    - external_network
    - internal_network
    volumes:
    - ./public/system:/mastodon/public/system
    healthcheck:
    test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]

## Uncomment to enable federation with tor instances along with adding the following ENV variables
## http_hidden_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

Si estás moviendo tu instancia desde una VM a docker

Inicializar y restaurar la base de datos

Este paso es completamente nuevo cuando migra una instancia existente a una instalación de mastodon en docker. Al no ejecutar docker compose run --rm web rake mastodon:setup, tenemos que inicializar la base de datos a mano.

Copiamos el fichero de volcado de nuestra vieja instancia de mastodon, siguiendo las indicaciones de las instrucciones oficiales de la migración.

$ sudo docker compose run -d db 
# esto crea el contenedor de la base de datos aisladamente y lo deja funcionando en segundo plano
$ sudo docker ps
# anotamos el nombre del contenedor, por ejemplo mastodon-db-run-27f0725f63a5,
# y su id, por ejemplo 006be69df010
# copiamos el fichero de volcado al contenedor
$ sudo docker cp mastodon_production.dump mastodon-db-run-27f0725f63a5:/mastodon_production.dump
# entramos en el contenedor
$ sudo docker exec -it mastodon-db-run-27f0725f63a5 bash
# dentro del contenedor, como root
% createdb -T template0 mastodon_production
% su - postgres
# dentro del contenedor, como postgres
$ pg_restore -Fc -n public --no-owner --role=postgres -d mastodon_production /mastodon_production.dump
# debería funcionar sin problemas
$ exit
# Borramos el fichero de volcado del volumen del contenedor
% rm /mastodon_production.dump 
% exit
# esto nos saca del contenedor. Paramos el contenedor.
$ sudo docker stop 006be69df010

Los comandos anteriores también sirven para restaurar una copia de seguridad (en forma de volcado) de la base de datos.

Copiar los ficheros de la instancia anterior

Utilizando rsync, copiamos los ficheros bajo live/publoc/system de la instancia anterior.

Desde la máquina antigua, antigua, de partida,a la nueva, servidor:

👽 rsync live/mastodon/public/system/ ubuntu@servidor:~/docker/mastodon/public/system/

Configuración de NGINX

  1. Realizar los cambios propuestos en la configuración de nginx en el artículo propuesto como referencia.
  2. Añadir las cabeceras siguientes al bloque location @proxy{...} de tu fichero de configuración de nginx, para evitar problemas con vídeos y GIFs
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_no_cache $http_range $http_if_range;
Configuración completa de NGINX
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

# upstream backend {
#     server 127.0.0.1:3000 fail_timeout=0;
# }

# upstream streaming {
    # Instruct nginx to send connections to the server with the least number of connections
    # to ensure load is distributed evenly.
#     least_conn;

#     server 127.0.0.1:4000 fail_timeout=0;
    # Uncomment these lines for load-balancing multiple instances of streaming for scaling,
    # this assumes your running the streaming server on ports 4000, 4001, and 4002:
    # server 127.0.0.1:4001 fail_timeout=0;
    # server 127.0.0.1:4002 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 fedi.gvisoc.com;
  # root /home/mastodon/live/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 fedi.gvisoc.com;

  ssl_protocols TLSv1.2 TLSv1.3;

  # You can use https://ssl-config.mozilla.org/ to generate your cipher set.
  # We recommend their "Intermediate" level.
  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:DHE-RSA-CHACHA20-POLY1305;

  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;

  # Uncomment these lines once you acquire a certificate:
  # managed by Certbot
  ssl_certificate     /etc/letsencrypt/live/fedi.gvisoc.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/fedi.gvisoc.com/privkey.pem;


  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 99m;

  # 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 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 =404;
    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 =404;
    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 =404;
    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 =404;
    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 =404;
    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 =404;
    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 =404;
    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 =404;
    try_files $uri @proxy;
  }

  location ~ ^/system/ {
    add_header Cache-Control "public, max-age=2419200, immutable";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
    add_header X-Content-Type-Options nosniff;
    add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
    # try_files $uri =404;
    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://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;

    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_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
    proxy_no_cache $http_range $http_if_range;

    proxy_pass http://127.0.0.1:3000;
    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;
}

Obtener un certificado con Certbot

Para obtener los certificados con nginx, yo he usado las indicaciones de la documentación oficial de mastodon.

En este apartado deberíamos haber cambiado ya nuestras DNS a la nueva máquina.

Arrancar

Si todo va bien, deberíamos poder:

  1. Habilitar nuestro sitio en nginx
  2. Arrancar nuestra instancia con sudo docker compose up -d
  3. Asegurarnos de que los permisos / propietarios de los ficheros bajo docker/mastodon/public/system/ son correctos desde dentro del contenedor, tal y como se explica al final de la sección "Mastodon setup" del artículo de referencia.
# entrar en el contenedor
$ sudo docker exec -u root -it mastodon-web /bin/bash
# Dentro del contenedor, como root.
% chown -R 991:991 /mastodon/public
% exit
$ 

Y usar mastodon 🎉

Comandos de mantenimiento

  1. Volcado de la base de datos
sudo docker exec -u postgres -it mastodon-db pg_dump -Fc -U postgres mastodon_production > $(date +%Y%m%d).dump
  1. Comprobación de volumen de ficheros
sudo docker compose run --rm web bundle exec bin/tootctl media usage
  1. Limpieza de ficheros
sudo docker compose run --rm web bundle exec bin/tootctl media remove
sudo docker compose run --rm web bundle exec bin/tootctl preview_cards remove
sudo docker compose run --rm web bundle exec bin/tootctl media remove-orphans
  • En general, para todo el mantenimiento posible con tootctl, debemos usar sudo docker compose run --rm web bundle exec bin/tootctl ...
  • Para restarurar un volcado de la base de datos:
# Paramos mastodon
$ sudo docker compose down
$ sudo docker compose run -d db 
# esto crea el contenedor de la base de datos aisladamente y lo deja funcionando en segundo plano
$ sudo docker ps
# anotamos el nombre del contenedor, por ejemplo mastodon-db-run-27f0725f63a5,
# y su id, por ejemplo 006be69df010
# copiamos el fichero de volcado al contenedor
$ sudo docker cp mastodon_production.dump mastodon-db-run-27f0725f63a5:/mastodon_production.dump
# entramos en el contenedor
$ sudo docker exec -it mastodon-db-run-27f0725f63a5 bash
% su - postgres
# dentro del contenedor, como postgres
$ pg_restore -Fc -n public --no-owner --role=postgres -d mastodon_production /mastodon_production.dump
# debería funcionar sin problemas
$ exit
# Borramos el fichero de volcado del volumen del contenedor
% rm /mastodon_production.dump 
% exit
# esto nos saca del contenedor. Paramos el contenedor.
$ sudo docker stop 006be69df010
$ sudo docker compose up -d

Footnotes

  1. Esto es porque, en un contenedor docker, localhost y 127.0.0.1 se refieren siempre al contenedor, y no a la máquina que los ejecuta. En docker compose, los contenedores son accesibles mediante el nombre que les damos en el fichero docker-compose.yml

  2. Esto difiere de la instalación en una máquina virtual o física, donde el usuario de la base de datos es mastodon. Por la razón que fuese, no he sido capaz de completar el proceso con el usuario mastodon, pero a lo mejor tú sí.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment