Ich verwende einen Unix-Socket zwischen Caddy und Discourse statt TCP, aber ab und zu erhalte ich einen „unix:“-Fehler

@Falco @satonotdead

Ich habe es geschafft, es zu beheben. Ich habe eine gute Weile gewartet, um sicherzugehen, dass es wirklich behoben ist, und es scheint, dass es der Fall ist.

Die Konfiguration

Discourse läuft in Docker und stellt nginx über einen Unix-Socket (/var/discourse/shared/standalone/nginx.http.sock) bereit. Caddy sitzt davor als Reverse Proxy und leitet die echte IP des Clients über X-Real-IP weiter.

Zwei Dinge mussten geändert werden: die nginx-Konfiguration innerhalb des Containers und die Art und Weise, wie ich den Rebuild durchführe.

1. app.yml

## Plugins go here
## see https://meta.discourse.org/t/19157 for details
hooks:
  # This after_code block is just my own plugin list — it has nothing to do with
  # the fix. Keep whatever you already have here.
  after_code:
    - exec:
        [...]

  # This is the part that matters.
  #   1. Writes an http-level map that turns the literal "unix:" into 127.0.0.1.
  #      The 00- prefix makes nginx load it before discourse.conf.
  #   2. Rewrites discourse.conf so X-Forwarded-For uses the mapped variable
  #      instead of the raw $remote_addr.
  #   3. nginx -t fails the build if the result is not valid.
  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

Der entscheidende Code ist der, der im Körper von after_web_config steht.

2. Der Caddyfile-Block

Das folgende Skript steuert den Wartungsmodus über eine Flag-Datei, daher muss dein Site-Block darüber Bescheid wissen:

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

    root * /var/caddy/flags
    @maintenance file maintenance.flag
    handle @maintenance {
        respond "Maintenance in progress. We'll be back in a few minutes." 503
    }

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

Der Pfad zur Flag-Datei im Skript und die Datei, nach der der @maintenance-Matcher sucht, müssen dieselbe Datei sein. Wenn du eine umbenennst, benenne auch die andere um, sonst greift der Wartungsmodus stillschweigend nie.

Wenn dieselbe Caddy-Instanz auch andere Apps bedient, verwende eine Flag, die nur vom Block des Forums abgefangen wird, da ein Discourse-Rebuild sonst diese anderen Apps mit herunterreißt.

3. Das Rebuild-Skript

Ich benutze ./launcher rebuild app nicht mehr direkt. Ich verwende discourse-rebuild.sh:

#!/bin/bash
#
# discourse-rebuild.sh — Rebuilds the Discourse container without leaving orphaned
# requests behind and without the `invalid input syntax for type inet: "unix:"` error.
#
# CONTEXT
#   Discourse runs in Docker and exposes nginx on a Unix socket
#   (/var/discourse/shared/standalone/nginx.http.sock). Caddy acts as a reverse
#   proxy in front of it and passes the client's real IP through X-Real-IP.
#
#   A `rebuild` destroys the container and recreates the socket with a new inode.
#   During that transition there are two problematic windows:
#
#     1) Caddy keeps state from the previous socket until it is reloaded.
#     2) nginx starts accepting connections as soon as it boots, but Unicorn
#        takes ~15s longer before it can serve them.
#
#   A request landing in either window may arrive with no X-Real-IP. $remote_addr
#   is then left holding the literal "unix:", which PostgreSQL rejects when
#   inserting it into an inet column -> HTTP 500.
#
# WHAT IT DOES
#   1. Raises a flag file that puts the site into 503 (Caddy checks it on every
#      request, so it takes effect instantly and with no reload).
#   2. Waits until nothing but nginx itself is holding the socket open.
#   3. Rebuilds the container.
#   4. Polls /srv/status against the socket until Unicorn answers 200.
#   5. Reloads Caddy so it picks up the new socket, and clears the flag.
#   6. Starts the watcher that logs any leftover "unix:" hit.
#
#   If the rebuild fails, or Discourse never answers within the polling window,
#   the flag is NOT removed: the site stays in maintenance on purpose, so a broken
#   container is never exposed. Bring it back up by hand with:
#       rm -f /var/caddy/flags/maintenance.flag
#
# REQUIREMENTS
#   - lsof installed, and root privileges.
#   - FLAG below must point at the exact same file the Caddyfile @maintenance
#     matcher looks for.
#   - request_header X-Real-IP {remote_host} in that same Caddyfile block.
#
# USAGE
#   ./discourse-rebuild.sh
#
# TO SEE WHAT THE WATCHER CAUGHT
#   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 "✅ Rebuild finished. Site is online."
  else
    echo "⚠️  Rebuild failed (exit code $code). The site is still in maintenance."
    echo "    Check it, and once it's ready: rm -f $FLAG"
  fi
}
trap cleanup EXIT

mkdir -p "$(dirname "$FLAG")"
touch "$FLAG"
echo "🔧 Maintenance is on. Waiting for in-flight requests..."

# Rough heuristic: count the processes holding the socket open and wait until
# only the listener is left. Up to 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

# Up to 60 attempts: about 2 minutes of sleeps, more if any curl hits its own
# 5s timeout.
echo "⏳ Waiting for Discourse to answer..."
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 never answered within the polling window (last status code: $status)."
  exit 1
fi

echo "✅ Discourse ready after ~$((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"

Was das Skript tatsächlich tut

  1. Setzt die Site über die Flag-Datei von Caddy in den Wartungsmodus.
  2. Wartet, bis der Socket ruhig ist, und führt dann ./launcher rebuild app aus.
  3. Pollt /srv/status über den Socket, bis Unicorn mit 200 antwortet. Das ist der entscheidende Teil: Es verhindert, dass Anfragen nginx erreichen, während Unicorn noch startet.
  4. Lädt Caddy neu (Vorsichtshalber, es stellte sich heraus, dass dies nicht kritisch war).
  5. Entfernt die Wartungs-Flag nur, wenn dieser Poll erfolgreich war.

Nach mehreren Wochen und mehreren Rebuilds habe ich diesen Fehler nicht wieder gesehen.

PS: Ich weiß wirklich nicht, was ich da eigentlich gemacht habe.