Howto Mastodon

Mastodon est un réseau social libre et décentralisé, en alternative à X (anciennement Twitter).

Installation

Nous installons la version 4.5.8 sous Debian 12 (Bookworm).

Mastodon s’appuie sur Ruby, NodeJS, Yarn, Nginx, Redis et PostgreSQL.

On peut installer les dépendances pour Mastodon :

# apt install curl wget gnupg lsb-release ca-certificates nodejs imagemagick ffmpeg libvips-tools libpq-dev libxslt1-dev file git \
protobuf-compiler pkg-config autoconf bison build-essential libssl-dev libyaml-dev libreadline-dev zlib1g-dev libffi-dev \
libgdbm-dev nginx nodejs redis-server postgresql certbot python3-certbot-nginx libidn-dev libicu-dev libjemalloc-dev

Il faut activer corepack pour l’installation automatique de la bonne version de yarn.

# corepack enable

Compte UNIX

Créer un compte UNIX mastodon :

# adduser --disabled-login --gecos 'Mastodon App' mastodon

Note : Assurez-vous d’avoir DIR_MODE=0750 dans /etc/adduser.conf pour créer le home en 750.

Ruby

Mastodon 4.5.x nécessite Ruby (3.4.7) qui doit préférablement être compilé avec jemalloc. On le met en place via rbenv :

# sudo -iu mastodon
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ cd ~/.rbenv && src/configure && make -C src
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ echo 'export RAILS_ENV="production"' >> ~/.bash_profile
$ source ~/.bash_profile
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ cd
$ TMPDIR=~/tmp MAKE_OPTS=-j$(nproc) RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.4.7
$ rbenv local 3.4.7

Note : On met la variable d’environnement RAILS_ENV dans notre profile bash pour éviter de l’indiquer à chaque commande Ruby/Rails.

PostgreSQL

Mastodon utilise PostgreSQL. On utilise donc la version 15 de Debian 12 :

# apt install postgresql postgresql-client libpq-dev postgresql-contrib

Création de l’utilisateur PostgreSQL :

# sudo -u postgres createuser mastodon -d -P -R

Note : On donne les droits CREATEDB car Mastodon doit faire un DROP DATABASE puis CREATE DATABASE lors de l’installation…

Note : Pensez à conserver le mot de passe pour le mettre par la suite (ou pas si utilisation de l’authentification via ident).

Redis

Installation classique :

# apt install redis-server

Mastodon

On clone le repository et installe avec bundle et yarn :

# sudo -iu mastodon
$ git clone https://github.com/tootsuite/mastodon.git
$ cd mastodon
$ git checkout v4.5.8
$ bundle config deployment 'true'
$ bundle config without 'development test'
$ bundle install
$ yarn install
$ cp .env.production.sample .env.production

Attention, si vous avez un /home en noexec, il faudra le passer en exec pour les processus Ruby et NPM. Au risque d’obtenir ce genre d’erreurs :

npm[2990]: Error: Compilation of µWebSockets has failed and there is no pre-compiled binary available for your system. Please install a > supported C++11 compiler and reinstall the module 'uws'.
Failed at step EXEC spawning /home/mastodon/.rbenv/shims/bundle: Permission denied

Un assistant permet de paramétrer une nouvelle instance de Mastodon en intéractif :

$ bin/rails mastodon:setup

Ce petit programme va s’occuper de produire un fichier .env.production valide, précompiler les assets et initialiser la base de données. Si choisissez le mode interactif, vous pouvez passer à la section « Unités systemd » plus bas lorsque vous aurez terminé.

Il est aussi possible de le faire en mode manuel et dans ce cas on peut éditer le fichier .env.production à sa convenance. Voici un exemple :

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
DB_HOST=127.0.0.1
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=PASSWORD
DB_PORT=5432

LOCAL_DOMAIN=mastodon.example.com
LOCAL_HTTPS=true

EMAIL_DOMAIN_WHITELIST=example.com

DEFAULT_LOCALE=fr

SMTP_SERVER=127.0.0.1
SMTP_PORT=25
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=mastodon@mastodon.example.com
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
SMTP_AUTH_METHOD=none
SMTP_OPENSSL_VERIFY_MODE=none

Toujours pour le mode manuel, on génére des clés d’applications et webpush :

$ echo PAPERCLIP_SECRET=$(bundle exec rake secret) >> .env.production
$ echo SECRET_KEY_BASE=$(bundle exec rake secret) >> .env.production
$ echo OTP_SECRET=$(bundle exec rake secret) >> .env.production
$ bundle exec rake mastodon:webpush:generate_vapid_key >> .env.production

Initialisation de la base de données :

$ SAFETY_ASSURED=1 bundle exec rails db:setup

Précompilation des assets et ajustement des permissions pour le serveur Web (nginx) :

$ bundle exec rails assets:precompile
$ chmod -R u=rwX,g=rwX,o=rX /home/mastodon/mastodon/public

Unités systemd

Il y a trois unités systemd à mettre en place : web (puma), streaming et sidekiq. On peut reprendre les modèles proposés par le développeur ou s’en inspirer :

# cp /home/mastodon/mastodon/dist/mastodon-*.service /etc/systemd/system/

Il faut éditer les fichiers de configuration des trois unités, notamment pour changer le chemin /home/mastodon/live en /home/mastodon/mastodon/ :

mastodon-web.service

[Unit]
Description=mastodon-web
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/mastodon
Environment="RAILS_ENV=production"
Environment="PORT=3000"
Environment="LD_PRELOAD=libjemalloc.so"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -SIGUSR1 $MAINPID
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

mastodon-sidekiq.service

[Unit]
Description=mastodon-sidekiq
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/mastodon
Environment="RAILS_ENV=production"
Environment="DB_POOL=25"
Environment="MALLOC_ARENA_MAX=2"
Environment="LD_PRELOAD=libjemalloc.so"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 25
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

mastodon-streaming.service

[Unit]
Description=mastodon-streaming
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/mastodon
Environment="NODE_ENV=production"
Environment="PORT=4000"
Environment="STREAMING_CLUSTER_NUM=1"
ExecStart=/usr/bin/node ./streaming
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

Note : les ports 3000 et 4000 ci-haut devront être changés s’il y a plus d’une instance sur le serveur. On pourra par exemple incrémenter de 1, donc 3001, 4001.

On active et on démarre les unités :

# systemctl enable mastodon-{web,sidekiq,streaming}
# systemctl start mastodon-{web,sidekiq,streaming}

Note : Depuis la version 4.0.x, il est aussi possible de configurer la durée de vie du cache (médias, etc.) depuis l’interface d’administration (sous Administration -> Paramètres du serveur -> Rétention du contenu). Vous devrez baisser les valeurs par défaut si le stockage est un enjeu sur votre serveur.

Nginx

On utilise Nginx :

# apt install nginx-full

Exemple de vhost proposé par le développeur (avec quelques ajustements nécessaires à ce Howto) :

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 mastodon.example.com;
  root /home/mastodon/mastodon/public;
  # Useful for Let's Encrypt
  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 mastodon.example.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:
  # ssl_certificate     /etc/letsencrypt/live/mastodon.example.com/fullchain.pem;
  # ssl_certificate_key /etc/letsencrypt/live/mastodon.example.com/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 99m;

  root /home/mastodon/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/rss+xml text/javascript image/svg+xml image/x-icon;
  gzip_static on;

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
  }

  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;
}

Note : La partie SSL/TLS n’est pas évoquée. À vous de faire le nécessaire avec un certificat Let’s Encrypt par exemple. N’oubliez donc pas de modifier les directives ssl_ dans le vhost.

Mises à jour

Le principe des mises à jour est basé sur un git pull et un git checkout.

# sudo -iu mastodon
$ cd mastodon
$ git fetch
$ git checkout <VERSION>
$ bundle install
$ yarn install
$ bundle exec rails db:migrate
$ bundle exec rails assets:precompile
$ chmod -R u=rwX,g=rwX,o=rX /home/mastodon/mastodon/public/{assets,packs}
$ exit
# systemctl restart mastodon-*.service

Note : Ces commandes génériques ne sont parfois pas suffisantes. Vous devez systématiquement lire les notes de versions.

Configuration

Pour restreindre l’inscription à certains domaines de messagerie :

EMAIL_DOMAIN_WHITELIST=example.com|example.org

Utilisation

On peut utiliser différents clients :

FAQ

À propos des logs

Mastodon n’a aucun fichier de logs. Les logs sont gérés via la sortie standard et vont donc dans journald. On consultera les logs avec journalctl -u mastodon-web (ou mastodon-sidekiq ou mastodon-streaming).

Passer un utilisateur existant en admin

~/mastodon$ bin/tootctl accounts modify jdoe --role Admin
Congrats! jdoe is now an admin. \o/

Comment activer la recherche indexée (Elasticsearch ou Opensearch)

Il faut installer Elasticsearch. Voir notre HowtoElasticsearch

Une fois le logiciel Elasticsearch installé, vous devez ajouter un rôle et un utilisateur pour Mastodon :

# curl -X POST -u elastic:MOT_DE_PASSE_ES "localhost:9200/_security/role/mastodon_full_access?pretty" -H 'Content-Type: application/json' -d'
{
  "cluster": ["monitor"],
  "indices": [{
    "names": ["*"],
    "privileges": ["read", "monitor", "write", "manage"]
  }]
}
'
# apg -n1 -m15 # Pour générer le mot de passe de l'utilisateur mastodon
# curl -X POST -u elastic:MOT_DE_PASSE_ES "localhost:9200/_security/user/mastodon?pretty" -H 'Content-Type: application/json' -d'
{
  "password" : "MOT_DE_PASSE_POUR_MASTODON",
  "roles" : ["mastodon_full_access"]
}
'

Il faut ensuite ajuster la configuration de Mastodon en éditant le fichier .env.production :

ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
ES_PRESET=single_node_cluster
# ES_USER=mastodon
# ES_PASS=MOT_DE_PASSE_POUR_MASTODON

Cette configuration est pertinente si on installe Elasticsearch sur le même serveur que Mastodon, ce qui est recommandé, sauf si vous gérer une grosse instance.

Finalement, il faut redémarrer deux des trois unités systemd de Mastodon et lancer l’indexation :

# systemctl restart mastodon-sidekiq
# systemctl reload mastodon-web
# sudo -iu mastodon
$ cd mastodon
$ bin/tootctl search deploy