J'utilise une socket Unix entre Caddy et Discourse au lieu de TCP, mais de temps en temps j'obtiens une erreur « unix: »

@Falco @satonotdead

J’ai réussi à le corriger. J’ai attendu un bon moment pour m’assurer que c’était vraiment réparé, et cela semble le cas.

La configuration

Discourse s’exécute dans Docker et expose nginx sur un socket Unix (/var/discourse/shared/standalone/nginx.http.sock). Caddy est placé devant en tant que proxy inverse et transmet l’IP réelle du client via l’en-tête X-Real-IP.

Deux éléments ont dû être modifiés : la configuration nginx à l’intérieur du conteneur et la manière dont je lance la reconstruction.

1. app.yml

## Les plugins vont ici
## voir https://meta.discourse.org/t/19157 pour les détails
hooks:
  # Ce bloc after_code n'est que ma propre liste de plugins — il n'a rien à voir avec
  # le correctif. Conservez ce que vous avez déjà ici.
  after_code:
    - exec:
        [...]

  # C'est la partie qui compte.
  #   1. Écrit une map au niveau http qui transforme la chaîne littérale "unix:" en 127.0.0.1.
  #      Le préfixe 00- fait en sorte que nginx la charge avant discourse.conf.
  #   2. Réécrit discourse.conf pour que X-Forwarded-For utilise la variable mappée
  #      au lieu du $remote_addr brut.
  #   3. nginx -t échoue la construction si le résultat n'est pas valide.
  after_web_config:
    - exec: >-
        printf 'map $remote_addr $safe_remote_addr {\n  "unix:" 127.0.0.1;\n  default $remote_addr;\n}\nreal_ip_header X-Real-IP;\n'
        > /etc/nginx/conf.d/00-safe-remote-addr.conf
    - exec: >-
        sed -i 's/X-Forwarded-For \$remote_addr;/X-Forwarded-For $safe_remote_addr;/g'
        /etc/nginx/conf.d/discourse.conf
    - exec: nginx -t

Le code critique est ce qui se trouve dans le corps de after_web_config.

2. Le bloc Caddyfile

Le script ci-dessous gère le mode maintenance via un fichier drapeau, votre bloc de site doit donc en tenir compte :

forum.example.com {
    request_header X-Real-IP {remote_host}

    root * /var/caddy/flags
    @maintenance file maintenance.flag
    handle @maintenance {
        respond "Maintenance en cours. Nous serons de retour dans quelques minutes." 503
    }

    handle {
        reverse_proxy unix//var/discourse/shared/standalone/nginx.http.sock
    }
}

Le chemin du drapeau dans le script et le fichier que le sélecteur @maintenance recherche doivent être le même fichier. Si vous renommez l’un, renommez l’autre, sinon le mode maintenance ne s’activera jamais silencieusement.

Si la même instance Caddy sert également d’autres applications, utilisez un drapeau que seul le bloc du forum correspond, sinon une reconstruction de Discourse entraînera la mise hors ligne de ces autres applications.

3. Le script de reconstruction

Je n’utilise plus ./launcher rebuild app directement. J’utilise discourse-rebuild.sh :

#!/bin/bash
#
# discourse-rebuild.sh — Reconstruit le conteneur Discourse sans laisser de
# requêtes orphelines et sans l'erreur `invalid input syntax for type inet: "unix:"`.
#
# CONTEXTE
#   Discourse s'exécute dans Docker et expose nginx sur un socket Unix
#   (/var/discourse/shared/standalone/nginx.http.sock). Caddy agit comme proxy
#   inverse devant et transmet l'IP réelle du client via X-Real-IP.
#
#   Une `rebuild` détruit le conteneur et recrée le socket avec un nouvel inode.
#   Pendant cette transition, il y a deux fenêtres problématiques :
#
#     1) Caddy conserve l'état du socket précédent jusqu'à ce qu'il soit rechargé.
#     2) nginx commence à accepter les connexions dès son démarrage, mais Unicorn
#        met environ 15s de plus avant de pouvoir les servir.
#
#   Une requête atterrissant dans l'une de ces fenêtres peut arriver sans X-Real-IP. $remote_addr
#   reste alors avec la chaîne littérale "unix:", que PostgreSQL rejette lors
#   de l'insertion dans une colonne inet -> HTTP 500.
#
# CE QU'IL FAIT
#   1. Lève un fichier drapeau qui met le site en 503 (Caddy le vérifie à chaque
#      requête, donc il prend effet instantanément et sans rechargement).
#   2. Attend que rien d'autre que nginx lui-même ne tienne le socket ouvert.
#   3. Reconstruit le conteneur.
#   4. Interroge /srv/status via le socket jusqu'à ce qu'Unicorn réponde 200.
#   5. Recharge Caddy pour qu'il prenne en compte le nouveau socket, et efface le drapeau.
#   6. Démarre le surveillant qui journalise tout hit "unix:" restant.
#
#   Si la reconstruction échoue, ou si Discourse ne répond pas dans la fenêtre d'interrogation,
#   le drapeau n'est PAS retiré : le site reste en maintenance volontairement, afin qu'un conteneur
#   cassé ne soit jamais exposé. Remettez-le en ligne manuellement avec :
#       rm -f /var/caddy/flags/maintenance.flag
#
# EXIGENCES
#   - lsof installé, et privilèges root.
#   - FLAG ci-dessous doit pointer vers exactement le même fichier que le sélecteur @maintenance
#     du Caddyfile recherche.
#   - request_header X-Real-IP {remote_host} dans ce même bloc Caddyfile.
#
# UTILISATION
#   ./discourse-rebuild.sh
#
# POUR VOIR CE QUE LE SURVEILLANT A CAPTÉ
#   cat /var/log/unixip-hits.log
#

set -e

FLAG=/var/caddy/flags/maintenance.flag
SOCK=/var/discourse/shared/standalone/nginx.http.sock

cleanup() {
  local code=$?
  systemctl reload caddy
  if [ $code -eq 0 ]; then
    rm -f "$FLAG"
    echo "✅ Reconstruction terminée. Le site est en ligne."
  else
    echo "⚠️  Échec de la reconstruction (code de sortie $code). Le site est toujours en maintenance."
    echo "    Vérifiez, et une fois prêt : rm -f $FLAG"
  fi
}
trap cleanup EXIT

mkdir -p "$(dirname "$FLAG")"
touch "$FLAG"
echo "🔧 Maintenance activée. En attente des requêtes en cours..."

# Heuristique approximative : compte les processus tenant le socket ouvert et attend jusqu'à
# ce que seul l'écouteur reste. Jusqu'à 30s.
for i in $(seq 30); do
  n=$(lsof -t "$SOCK" 2>/dev/null | wc -l || echo 0)
  [ "$n" -le 1 ] && break
  sleep 1
done

/var/discourse/launcher rebuild app

# Jusqu'à 60 tentatives : environ 2 minutes de pauses, plus si curl atteint son
# propre timeout de 5s.
echo "⏳ En attente de la réponse de Discourse..."
status=000
for i in $(seq 60); do
  status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
    --unix-socket "$SOCK" http://localhost/srv/status 2>/dev/null || echo 000)
  [ "$status" = "200" ] && break
  sleep 2
done

if [ "$status" != "200" ]; then
  echo "⚠️  Discourse n'a jamais répondu dans la fenêtre d'interrogation (dernier code de statut : $status)."
  exit 1
fi

echo "✅ Discourse prêt après ~$((i*2))s."

systemd-run --unit=unixip-watch --collect \
  /bin/bash -c "docker exec app tail -F /var/log/nginx/access.log | grep --line-buffered 'unix:' >> /var/log/unixip-hits.log"

Ce que fait réellement le script

  1. Met le site en maintenance via le fichier drapeau de Caddy.
  2. Attend que le socket soit calme, puis exécute ./launcher rebuild app.
  3. Interroge /srv/status via le socket jusqu’à ce qu’Unicorn réponde 200. C’est la partie critique : c’est ce qui empêche les requêtes d’atteindre nginx pendant qu’Unicorn est encore en cours de démarrage.
  4. Recharge Caddy (par sécurité, il s’est avéré que ce n’était pas critique).
  5. Efface le drapeau de maintenance uniquement si cette interrogation a réussi.

Plusieurs semaines et plusieurs reconstructions plus tard, je n’ai plus vu cette erreur.

P.-S. : Je ne sais vraiment pas ce que j’ai fait, honnêtement.