Este Gist es un resumen de los pasos para:
- Preparar una máquina con Ubuntu 22.04 LTS como servidor Docker, con NGINX
- Instalar mastodon en docker, mediante el Docker compose oficial
- 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
- Configuración de NGINX (cambios sobre la configuración oficial)
- Obtener un certificado
- ✨🎉✨
- Comandos de mantenimiento modificados para funcionar con docker
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
.
En nuestro ordenador personal, de trabajo, generamos una par de claves para hacer login por SSH:
👾 ssh-keygen
- 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
- En el servidor, añadirla al fichero
.ssh/authorized_keys
ubuntu@servidor: ~$ mkdir -p .ssh
$ cat public_key.pub >> .ssh/authorized_keys
- Comprobar que podemos hacer ssh al servidor sin que nos pida contraseña.
👾 ssh ubuntu@servidor
ubuntu@servidor: ~$
- Restringir el acceso por ssh usando contraseña, editando el fichero
/etc/ssh/sshd_config
. Buscar la línea que contienePasswordAuthentication
, y asegurarnos de que se queda descomentada y con valorno
:
# (contenido previo del fichero)
# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
#PermitEmptyPasswords no
# (resto del fichero)
- 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).
- Instalar
fail2ban
y configurarlo de acuerdo a la guía de mastodon - 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.
- En lugar de usar
ip_tables
, vamos a usarfirewalld
, 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
$
- 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"]
}
- 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: ~$
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 pasodocker compose run --rm web rake mastodon:setup
- Crear una estructura de directorios
ubuntu@servidor: ~$ mkdir -p docker/mastodon
$ cd docker/mastodon
$ mkdir -p postgres14
$ mkdir -p redis
$ mkdir -p public/system
- 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
- 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:
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
- 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
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.
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/
- Realizar los cambios propuestos en la configuración de nginx en el artículo propuesto como referencia.
- 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;
}
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.
Si todo va bien, deberíamos poder:
- Habilitar nuestro sitio en nginx
- Arrancar nuestra instancia con
sudo docker compose up -d
- 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 🎉
- 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
- Comprobación de volumen de ficheros
sudo docker compose run --rm web bundle exec bin/tootctl media usage
- 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 usarsudo 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
-
Esto es porque, en un contenedor docker,
localhost
y127.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 ficherodocker-compose.yml
↩ -
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 usuariomastodon
, pero a lo mejor tú sí. ↩