Mastodon Glitch Soc
Scritto il: 2026-04-27
MASTODON GLITCH SOC
Conoscete tutti X aka ex TWITTER. Avrete anche sentito parlare del fediverso. Ma ditemi, bambini, sapete che cosa è il fediverso? Ebbene, sono sicuro che siete tranquillamente in grado di cercarvelo altrove, come per esempio su Wikipedia. Quello che mi preme far capire è che per entrare nel fediverso avete due modi principali:
- registrarvi su un'istanza
- crearvi la vostra istanza e parteciparvi. Va da sé che ciò che interessa me, e di conseguenza voi visto che siete qui, è crearvi la vostra.
Per entrare nel fediverso partiamo da Mastodon versione Glitch Soc. Il motivo per cui partiamo da questa e non dal Vanilla è perché oggettivamente è più versatile: permette di scrivere in linguaggio mark down, permette di scrivere di default post fino a 5k caratteri e non 500, e varie altre ed eventuali.
Ingredienti
Per creare l'istanza avrete bisogno di Docker, un dominio, un server di posta in uscita e Cloudflare in modo da non esporre il vostro indirizzo IP. E ovviamente una macchina accesa h24.
Se cercate soluzioni one-click come YunoHost, questo non è il posto giusto. Qui si lavora comprendendo ogni componente dello stack, perché quando qualcosa si rompe in produzione il pulsante magico non basta.
Cloudflare Tunnel: Prepariamo il terreno
Prima ancora di scrivere una riga di docker-compose, dobbiamo preparare il tunnel Cloudflare. Perché? Perché non vogliamo esporre nessuna porta della nostra macchina, nemmeno la 80 o la 443. Il tunnel si occuperà di tutto.
- Andate su Cloudflare Dashboard → Zero Trust → Networks → Tunnels.
- Create un nuovo tunnel (chiamiamolo
mastodon-tunnel). - Nella sezione "Public Hostname", aggiungete:
- Subdomain:
nebula(o quello che volete) - Domain: il vostro dominio (es.
dreadful.work) - Service:
http://localhost:80
- Subdomain:
- Cloudflare vi darà un token lungo e brutto. Salvatelo da qualche parte, servirà.
- Ora prendete quel token e create un file
.envnella directory dove lavoreremo (ancora non esiste, ma lo creiamo tra poco):
echo "TUNNEL_TOKEN=il_vostro_token" > .envQuesto file serve perché nel docker-compose c'è scritto ${TUNNEL_TOKEN}. Se non lo create, il container cloudflared non parte.
Nota sui DNS: Cloudflare Tunnel non richiede record A o CNAME particolari se usate il tunnel in modalità cloudflared. Il demone si occuperà di collegarsi automaticamente. Basta che il dominio sia gestito da Cloudflare.
Docker Compose
Creiamo la nostra bella working directory:
sudo mkdir /opt/dreadful.work/mastodon && sudo chown 1000:1000 /opt/dreadful.work/mastodon && cd /opt/dreadful.work/mastodoncreiamo il file .env.production: CREIAMOLO perché altrimenti se non esiste mezzi script potrebbero non andare.
touch .env.productionCreiamo anche il file .env per il token di Cloudflare (se non l'avete già fatto):
echo "TUNNEL_TOKEN=il_vostro_token_brutto" > .envbuttiamo giù il docker-compose.yml
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:7-alpine
networks:
- internal_network
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
volumes:
- ./redis:/data
es:
restart: always
image: docker.elastic.co/elasticsearch/elasticsearch:9.3.3
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
web:
image: ghcr.io/glitch-soc/mastodon:latest
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',"curl -s --noproxy localhost localhost:3000/health | grep -q 'OK' || exit 1"]
depends_on:
- db
- redis
- es
volumes:
- ./public/system:/mastodon/public/system
streaming:
image: ghcr.io/glitch-soc/mastodon-streaming:latest
restart: always
env_file: .env.production
command: node ./streaming/index.js
networks:
- external_network
- internal_network
healthcheck:
# prettier-ignore
test: ['CMD-SHELL', "curl -s --noproxy localhost localhost:4000/api/v1/streaming/health | grep -q 'OK' || exit 1"]
depends_on:
- db
- redis
sidekiq:
image: ghcr.io/glitch-soc/mastodon:latest
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\ 8' || false"]
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- external_network
- internal_network
depends_on:
- web
- streaming
restart: always
tunnel:
image: cloudflare/cloudflared:latest
networks:
- external_network
- internal_network
restart: "always"
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
networks:
external_network:
internal_network:
internal: trueCome noterete qui ci sono vari servizi tra cui elasticsearch per fare la ricerca full text e ... nessuna porta esposta. Già, perché usiamo cloudflared quindi nessuna porta esposta.
Creazione delle chiavi
Nel file .env.production dobbiamo scrivere alcune chiavi che otterremo con docker compose run --rm web bundle exec rails secret. Questo comando va eseguito SEI volte* perché abbiamo necessità di SEI chiavi. Dopodiché possiamo dare un docker compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key per creare le vapid keys. Quindi il procedimento corretto è
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rails secret
docker compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_keyI primi 6 comandi daranno 6 chiavi random: ciascuna dovrà essere messa in docker-compose.yml accanto al simbolo di =:
SECRET_KEY_BASE=
OTP_SECRET=
PAPERCLIP_SECRET=
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=L'ultimo comando invece vi restituirà le Vapid keys, e anche loro vogliono essere messe in .env.production:
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=Creazione del reverse proxy
Utilizzeremo nginx come reverse proxy (nel mio caso il server si chiamera nebula.dreadful.work):
mkdir nginx/
vim nginx/nginx.confIl suo interno sarà popolato con:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream mbackend {
server web:3000 fail_timeout=0;
}
upstream mstreaming {
server streaming:4000 fail_timeout=0;
}
# Cache per i media e gli asset
proxy_cache_path /var/cache/mnginx levels=1:2 keys_zone=MCACHE:10m inactive=7d max_size=1g;
# Redirect HTTP -> HTTPS
server {
listen 80;
server_name nebula.dreadful.work;
keepalive_timeout 70;
sendfile on;
client_max_body_size 80m;
root /var/www/mastodon/public;
# Compressione Gzip
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;
# Gestione file statici e routing Rails
location / {
try_files $uri @proxy;
}
# Cache Control per file statici (Avatar, Emoji, Packs)
location ~ ^/(assets|avatars|emoji|headers|packs|shortcuts|sounds|system)/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri @proxy;
}
location = /sw.js {
add_header Cache-Control "public, max-age=604800, must-revalidate";
try_files $uri @proxy;
}
# --- PROXY STREAMING (API Realtime) ---
location ^~ /api/v1/streaming {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # Ora passa l'IP reale
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://mstreaming;
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;
}
# --- PROXY PRINCIPALE (Web & API) ---
location @proxy {
# Usa il log di debug per monitorare il passaggio dell'IP reale
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # Fondamentale per far vedere l'IP vero a Mastodon
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://mbackend;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache configurata per le risposte 200
proxy_cache MCACHE;
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;
}.env.production
Riapriamo il file .env.production e compiliamolo con quanto manca.
Attenzione: qui ci sono due casi.
Caso 1: Split domain (consigliato) Se volete che il vostro username sia @[email protected] ma l'istanza sia su nebula.dreadful.work, usate questa configurazione:
Split domain: l'identità è su dreadful.work, il server su nebula.dreadful.work
WEB_DOMAIN=nebula.dreadful.work
LOCAL_DOMAIN=dreadful.workCaso 2: Domain unico Se preferite avere tutto su nebula.dreadful.work (username @[email protected]):
LOCAL_DOMAIN=nebula.dreadful.workIo uso split domain, quindi la mia configurazione prosegue così:
# Io uso lo split domain quindi il mio LOCAL_DOMAIN sarà dreadful.work e non nebula.dreadful.work
WEB_DOMAIN=nebula.dreadful.work
LOCAL_DOMAIN=dreadful.work
SINGLE_USER_MODE=false
SECRET_KEY_BASE= XXXXXXXXXX
OTP_SECRET= XXXXXXXXXX
PAPERCLIP_SECRET= XXXXXXXXXX
VAPID_PRIVATE_KEY= XXXXXXXXXX
VAPID_PUBLIC_KEY= XXXXXXXXXX
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY= XXXXXXXXXX
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT= XXXXXXXXXX
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY= XXXXXXXXXX
DB_HOST=db
DB_PORT=5432
DB_NAME=postgres
DB_USER=postgres
DB_PASS=
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
# Io uso Brevo, voi utilizzerete il vostro
SMTP_SERVER=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_LOGIN= XXXXXXXXXX
SMTP_PASSWORD= XXXXXXXXXX
SMTP_AUTH_METHOD=plain
SMTP_OPENSSL_VERIFY_MODE=peer
SMTP_ENABLE_STARTTLS=auto
[email protected]
RAILS_SERVE_STATIC_FILES=true
ES_ENABLED=true
ES_HOST=es
ES_PORT=9200
MAX_TOOT_CHARS=5000
# Se avete il vostro provider OIDC potete metterci queste direttive. Io utilizzo quello creato a suo tempo
OIDC_ENABLED=true
OIDC_DISPLAY_NAME=Authentik # O quello che vuoi che appaia sul bottone
OIDC_ISSUER=https://idp.dreadful.work/application/o/dreadful-mastodon/ # URL dell'Issuer di Authentik
OIDC_DISCOVERY=true
OIDC_CLIENT_ID= XXXXXXXXXX
OIDC_CLIENT_SECRET= XXXXXXXXXX
OIDC_REDIRECT_URI=https://nebula.dreadful.work/auth/auth/openid_connect/callback
OIDC_SCOPE=openid,email,profile
OIDC_INFO_FIELDS=email
OIDC_UID_FIELD=preferred_username
OIDC_AUTOLINK=true
OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# Qui va messo a true se volete abilitare le iscrizioni. Io non lo faccio perché lo voglio vincolato al mio IDP.
LOCAL_SIGNUP_ENABLED=false
TRUSTED_PROXY_IP="192.168.0.0/16,172.16.0.0/12,10.0.0.0/8,173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/12,172.64.0.0/13,131.0.72.0/22,100.127.11.83,172.16.171.1,172.16.0.0/12" Startup
Fatto questo possiamo proseguire:
docker compose run --rm -e DISABLE_DATABASE_ENVIRONMENT_CHECK=1 web bundle exec rake db:setupQuesto costruirà il database; dopodiché:
mkdir -p ./public/system
sudo chown 991:991 ./public/systemE per terminare
docker compose up -d && docker compose logs -f Qualora ci fossero problemi di permessi su elastic search:
mkdir elasticsearch; sudo chown 1000:1000 elasticsearch
touch .env.esE per finire
docker compose down; docker compose up -dCreazione dell'admin
docker compose run --rm web bin/tootctl accounts create errno0x0d --email [email protected] --confirmed --role OwnerSplit Domain
Se avete scelto lo split domain (come me), dovete aggiungere un pezzo di configurazione sul server che gestisce il dominio principale dreadful.work (quello senza il nebula.). Questo non è il server di Mastodon, ma il vostro web server principale (es. quello che serve il vostro sito personale).
Nel file di configurazione di nginx (o Apache, o Caddy) del vostro dominio principale, aggiungete:
location ~ ^/.well-known/(webfinger|nodeinfo|host-meta) {
# Non serve più il proxy_pass né gli header CORS qui,
# perché il client verrà mandato direttamente su nebula.
return 301 https://nebula.dreadful.work$request_uri;
}