Kommentare auf einem statischen Blog sind ein echtes Problem. Kein Backend, keine Datenbank, kein Formular. Die Lösung: ein leichter Kommentarserver, den man selbst hostet. Isso ist genau das – Python, SQLite, ein paar Zeilen JavaScript. Aber der Weg dorthin war steiniger als erwartet.
Was ist Isso?
Isso ist ein Open-Source-Kommentarsystem, das als leichter Server läuft. Besucher schreiben Kommentare, die in einer SQLite-Datei gespeichert werden. Kein externer Dienst, keine Tracking-Skripte, keine Werbung. Alles bleibt auf dem eigenen Server.
Die Architektur ist simpel: Ein Python-Prozess lauscht auf einem Port, ein JavaScript-Client lädt die Kommentare und rendert ein Formular unter jedem Artikel. Fertig.
Das Setup-Script
Ich habe ein Shell-Script geschrieben, das die komplette Einrichtung automatisiert. Auf dem Incus-Host ausgeführt, legt es eine eigene Instanz an, installiert Isso, richtet einen systemd-Service ein und gibt die Konfiguration für den Reverse Proxy aus. Beliebig oft ausführbar, jedes Mal mit neuem Admin-Passwort.
Aber bis das Script funktionierte, musste ich ein paar Dinge herausfinden.
Problem 1: Instanz-Erkennung
Die erste Version hat geprüft, ob die Incus-Instanz existiert, mit:
if incus info "$INSTANZ" &>/dev/null; then
Das Problem: incus info gibt auch bei nicht-existierenden Instanzen Exit-Code 0 zurück. Das Script dachte, die Instanz existiert bereits, und übersprungen die Erstellung. Dann schlug alles weitere (die Paketinstallation, der User) fehl.
Lösung: Statt incus info die Ausgabe von incus list --format csv parsen und nach dem Instanznamen suchen. Zuverlässig, portabel.
if incus list "$INSTANZ" --format csv 2>/dev/null | grep -q "^$INSTANZ,"; then
Problem 2: Kein TTY für adduser
In Incus-Containern gibt es kein Terminal. Das Skript hat adduser --disabled-password --gecos "" isso aufgerufen – und der Aufruf ist einfach fehlgeschlagen, ohne Fehler. Der User wurde nie angelegt, sudo -u isso brach dann mit "unknown user" ab.
Lösung: useradd -m -s /bin/bash isso statt adduser. useradd ist nicht-interaktiv und braucht kein TTY.
Problem 3: Fehlender C-Compiler
Isso braucht misaka, ein Python-Paket mit C-Extension. Ohne gcc und die Python-Entwicklungspakete schlägt pip install fehl:
error: [Errno 2] No such file or directory: 'x86_64-linux-gnu-gcc'
Die Debian-Minimal-Image hat weder gcc noch python3-dev vorinstalliert.
Lösung: python3-dev gcc build-essential mitinstallieren. Das Skript installiert jetzt alle Abhängigkeiten, die pip für Kompilierung braucht.
Problem 4: dpkg-preconfigure meckert
dpkg-preconfigure: unable to re-open stdin: No such file or directory
apt-get install in Incus ohne TTY erzeugt diese Warnung. Kein echtes Problem, aber hässlich.
Lösung: DEBIAN_FRONTEND=noninteractive setzen und an apt-get übergeben. Die Warnung verschwindet.
Problem 5: incus file write gibt es nicht
In meiner Incus-Version gibt es den Befehl incus file write nicht. Das Skript ist mit "unknown command" abgebrochen.
Lösung: Dateien erst lokal in ein Temp-Verzeichnis schreiben, dann per incus file push in die Instanz übertragen. Funktioniert in allen Incus-Versionen:
cat > "$TMPDIR/isso.cfg" << EOF
...
EOF
incus file push "$TMPDIR/isso.cfg" "$INSTANZ"/home/isso/isso.cfg
Problem 6: Isso horcht nur auf localhost
Die Isso-Konfiguration hatte listen = http://127.0.0.1:8080. Der Caddy-Reverse-Proxy im Blog-Container konnte Isso nicht erreichen, weil 127.0.0.1 nur den Loopback der Isso-Instanz meint – nicht den des Blog-Servers.
Lösung: listen = http://0.0.0.0:8080 in der Config UND gunicorn -b 0.0.0.0:8080 im systemd-Service. Beides muss zusammenpassen.
Die Integration ins Blog
Das Einbinden in die Artikel war der einfachste Teil. Im Build-Skript (build-static.py) kommt unter jedem Artikel der Isso-Client:
<script data-isso="https://kommentare.frischux.de/"
data-isso-lang="de"
data-isso-sorting="newest"
data-isso-require-author="true"
data-isso-require-email="true"
data-isso-avatar="true"
data-isso-vote="false"
src="https://kommentare.frischux.de/js/embed.min.js"></script>
<section id="isso-thread"></section>
Das data-isso Attribut verweist auf die Isso-Instanz, das src lädt das JavaScript. Isso erkennt automatisch die URL des aktuellen Artikels und lädt die passenden Kommentare.
Problem 7: SPA-Zugriff zerstört Kommentare
Das Blog hat eine post.html, die Artikel per JavaScript lädt und rendert – ein kleiner SPA-Mechanismus. Beim Klick auf der Startseite wird post.html?slug=... aufgerufen, nicht die statische HTML-Seite. Das Problem: Isso läuft beim initialen Seitenaufruf durch, findet #isso-thread noch nicht (der Inhalt wird erst danach per JavaScript gerendert), und initialisiert sich nie.
Versuch 1: Isso-Script dynamisch einfügen, nachdem #isso-thread im DOM ist. Funktioniert nicht – Isso initialisiert sich nur einmal und scannt das DOM nicht erneut.
Versuch 2: MutationObserver, der auf DOM-Änderungen lauscht und Isso.init() aufruft. Funktioniert nicht – Isso.init() existiert nicht oder setzt den Thread nicht neu an.
Lösung: Die post.html einfach abschaffen. Statt post.html?slug=... verlinken die Artikel direkt auf artikel/{slug}.html. Die statischen Seiten haben Isso eingebaut, funktionieren zuverlässig. Der SPA-Komfort war es nicht wert – die Seiten laden eh schnell genug.
Was bleibt
Das Skript ist jetzt stabil und idempotent. Jeder Durchlauf:
- Überspringt die Instanz-Erstellung, wenn sie schon existiert
- Installiert Pakete nur, wenn sie fehlen
- Erstellt den User nur, wenn er noch nicht da ist
- Überschreibt Config und Service (mit neuem Admin-Passwort)
- Startet Isso neu
HTTPS ist über einen externen Caddy gelöst – der terminiert TLS und leitet auf die Isso-Instanz weiter. Kein extra Zertifikat nötig, kein Certbot, kein Port 443 im Container.
Der erste Login
Die Admin-Oberfläche ist unter https://kommentare.frischux.de/admin erreichbar. Ein User muss nicht extra angelegt werden – das generierte Passwort aus dem Setup-Script reicht direkt zum Einloggen. Danach Domain frischux.de in den Domains hinzufügen, und los geht's.
Benachrichtigungen über ntfy
Isso kann bei notify = stdout Kommentar-Benachrichtigungen an die Standard-Ausgabe senden. Theoretisch. In der Praxis puffert gunicorn stdout – die Notifications kommen nie in der Pipe an.
Lösung: Ein systemd-Timer, der jede Minute die SQLite-Datenbank abfragt. Das Skript vergleicht die Anzahl freigeschalteter (mode=1) und ausstehender (mode=2) Kommentare mit dem letzten Stand. Bei neuen Einträgen geht eine Nachricht an ntfy.
In der Isso-Konfiguration muss sqlite3 installiert sein – das Setup-Script erledigt das mit. Die Spalte mode in der Tabelle comments unterscheidet zwischen freigeschaltet (1), ausstehend (2) und Spam (0).
Status
Die Kommentarfunktion ist testweise eingebaut. Ob ich sie behalten werde, weiß ich noch nicht. Mal schauen, ob sich jemand meldet.
Das Setup-Script
Das komplette Script zum Einrichten der Isso-Instanz. Auf dem Incus-Host ausfuehren, beliebig oft ausfuehren.
#!/bin/bash
set -e
INSTANZ="isso"
DOMAIN="kommentare.frischux.de"
BLOG_HOST="https://frischux.de"
ADMIN_PASSWD=$(openssl rand -base64 16)
TMPDIR=$(mktemp -d)
echo "=== Isso-Setup fuer Incus ==="
echo "Instanz: $INSTANZ"
echo "Domain: $DOMAIN"
echo "Blog: $BLOG_HOST"
echo "Admin-PW: $ADMIN_PASSWD"
echo ""
# --- Instanz anlegen ---
if incus list "$INSTANZ" --format csv 2>/dev/null | grep -q "^$INSTANZ,"; then
echo "Instanz '$INSTANZ' existiert bereits."
else
echo "Erstelle Incus-Instanz..."
incus launch images:debian/13 "$INSTANZ"
sleep 3
fi
# --- Pakete installieren ---
echo "Installiere Abhaengigkeiten..."
incus exec "$INSTANZ" -- bash -c "
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq python3 python3-venv python3-dev gcc build-essential curl </dev/null
" >/dev/null
# --- Isso-User anlegen ---
if incus exec "$INSTANZ" -- bash -c "id isso" &>/dev/null; then
echo "User 'isso' existiert bereits."
else
echo "Erstelle User 'isso'..."
incus exec "$INSTANZ" -- useradd -m -s /bin/bash isso
fi
# --- Virtualenv + Isso installieren ---
echo "Installiere Isso + Gunicorn..."
incus exec "$INSTANZ" -- sudo -u isso bash -c '
if [ ! -d /home/isso/venv ]; then
python3 -m venv /home/isso/venv
fi
/home/isso/venv/bin/pip install --quiet isso gunicorn
'
# --- Konfiguration ---
echo "Schreibe Konfiguration..."
SALT=$(openssl rand -hex 16)
cat > "$TMPDIR/isso.cfg" << EOF
[general]
dbpath = /home/isso/comments.db
host = ${BLOG_HOST}
notify = stdout
reply-notifications = false
gravatar = true
[moderation]
enabled = true
purge-after = 30d
[server]
listen = http://0.0.0.0:8080
public-endpoint = https://${DOMAIN}
[guard]
enabled = true
ratelimit = 2
direct-reply = 3
reply-to-self = false
require-author = true
require-email = true
[hash]
salt = ${SALT}
algorithm = pbkdf2
[admin]
enabled = true
password = ${ADMIN_PASSWD}
[markup]
options = strikethrough, superscript, autolink
allowed-elements = a, blockquote, br, code, em, p, pre, strong, ul, ol, li
allowed-attributes = a href
EOF
incus file push "$TMPDIR/isso.cfg" "$INSTANZ"/home/isso/isso.cfg
incus exec "$INSTANZ" -- chown isso:isso /home/isso/isso.cfg
# --- Notify-Script ---
echo "Erstelle Notify-Script..."
cat > "$TMPDIR/isso-check-comments.sh" << 'NOTIFY'
#!/bin/bash
DB="/home/isso/comments.db"
STATE="/home/isso/.last_comment_state"
NTFY_SERVER="https://ntfy.frischux.de"
NTFY_TOPIC="blog"
if [ ! -f "$DB" ]; then exit 0; fi
APPROVED=$(sqlite3 "$DB" "SELECT COUNT(*) FROM comments WHERE mode=1;" 2>/dev/null || echo 0)
PENDING=$(sqlite3 "$DB" "SELECT COUNT(*) FROM comments WHERE mode=2;" 2>/dev/null || echo 0)
PREV_APPROVED=0
PREV_PENDING=0
[ -f "$STATE" ] && read -r PREV_APPROVED PREV_PENDING < "$STATE"
MSG=""
if [ "$APPROVED" -gt "$PREV_APPROVED" ]; then
NEW=$((APPROVED - PREV_APPROVED))
MSG="${MSG}${NEW} freigeschalteter Kommentar\n"
fi
if [ "$PENDING" -gt "$PREV_PENDING" ]; then
NEW=$((PENDING - PREV_PENDING))
MSG="${MSG}${NEW} Kommentar wartet auf Freischaltung\n"
fi
if [ -n "$MSG" ]; then
curl -s -d "$(echo -e "$MSG")" "$NTFY_SERVER/$NTFY_TOPIC" >/dev/null
fi
echo "$APPROVED $PENDING" > "$STATE"
NOTIFY
incus file push "$TMPDIR/isso-check-comments.sh" "$INSTANZ"/home/isso/check-comments.sh
incus exec "$INSTANZ" -- chmod +x /home/isso/check-comments.sh
incus exec "$INSTANZ" -- chown isso:isso /home/isso/check-comments.sh
incus exec "$INSTANZ" -- apt-get install -y -qq sqlite3 >/dev/null
# --- Systemd-Service ( Kommentar-Check ) ---
echo "Erstelle Kommentar-Check-Service..."
cat > "$TMPDIR/isso-comments-check.service" << 'EOF'
[Unit]
Description=Check for new Isso comments
[Service]
Type=oneshot
User=isso
ExecStart=/home/isso/check-comments.sh
EOF
incus file push "$TMPDIR/isso-comments-check.service" "$INSTANZ"/etc/systemd/system/isso-comments-check.service
cat > "$TMPDIR/isso-comments-check.timer" << 'EOF'
[Unit]
Description=Check for new Isso comments every minute
[Timer]
OnBootSec=60
OnUnitActiveSec=60
[Install]
WantedBy=timers.target
EOF
incus file push "$TMPDIR/isso-comments-check.timer" "$INSTANZ"/etc/systemd/system/isso-comments-check.timer
# --- Systemd-Service (Isso) ---
echo "Erstelle Systemd-Service..."
cat > "$TMPDIR/isso.service" << 'EOF'
[Unit]
Description=Isso commenting system
After=network.target
[Service]
Type=simple
User=isso
Environment=ISSO_SETTINGS=/home/isso/isso.cfg
ExecStart=/home/isso/venv/bin/gunicorn -b 0.0.0.0:8080 -w 4 --preload isso.run
Restart=on-failure
LimitNOFILE=16384
[Install]
WantedBy=multi-user.target
EOF
incus file push "$TMPDIR/isso.service" "$INSTANZ"/etc/systemd/system/isso.service
incus exec "$INSTANZ" -- systemctl daemon-reload
incus exec "$INSTANZ" -- systemctl enable --now isso
incus exec "$INSTANZ" -- systemctl enable --now isso-comments-check.timer
# --- Verifikation ---
sleep 2
if incus exec "$INSTANZ" -- curl -s http://0.0.0.0:8080/info | grep -q "version"; then
echo "Isso laeuft."
else
echo "FEHLER: Isso antwortet nicht!"
incus exec "$INSTANZ" -- systemctl status isso --no-pager || true
exit 1
fi
rm -rf "$TMPDIR"
echo ""
echo "=== Fertig ==="
echo ""
echo "Isso laeuft auf http://0.0.0.0:8080 in der Instanz '${INSTANZ}'"
echo ""
echo "Admin-Interface: https://${DOMAIN}/admin"
echo "Admin-Passwort: ${ADMIN_PASSWD}"
