Files
hetzner-api-dyndns-records-…/PRODUCTION.md
T
admin df7516e7ab
Container-Image veröffentlichen / container (push) Has been cancelled
feat: production-ready domain control dashboard
2026-07-12 20:56:45 +02:00

5.8 KiB

Produktivbetrieb mit Gitea und Docker Compose

Dieses Repository veröffentlicht versionierte Container-Images unter:

git.j-mahler.de/admin/domain-control-webinterface-prod:vX.Y.Z

Das Image enthält ausschließlich die Anwendung. SQLite-Datenbank und Master-Key liegen dauerhaft auf dem Produktionsserver und bleiben bei Image-Updates erhalten.

1. Gitea für Releases vorbereiten

In den Repository-Einstellungen unter Actions → Secrets diese zwei Secrets anlegen:

  • REGISTRY_USERNAME: Gitea-Benutzer oder eigener Release-Bot
  • REGISTRY_TOKEN: persönlicher Gitea-Token mit Schreibrecht für Container-Pakete

Auf der Gitea-Instanz muss ein Actions-Runner mit dem Label ubuntu-latest registriert sein. Jeder Git-Tag, der mit v beginnt, startet anschließend .gitea/workflows/release.yml und baut Images für AMD64 und ARM64.

Erstes Release veröffentlichen:

git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

Nach erfolgreicher Action stehen v1.0.0 und der bewegliche Tag stable in der Gitea Container Registry. Auf Produktion immer den unveränderlichen Versionstag verwenden.

Image alternativ manuell veröffentlichen

Falls Gitea Actions noch nicht eingerichtet ist:

echo "$GITEA_TOKEN" | docker login git.j-mahler.de --username DEIN_BENUTZER --password-stdin

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t git.j-mahler.de/admin/domain-control-webinterface-prod:v1.0.0 \
  -t git.j-mahler.de/admin/domain-control-webinterface-prod:stable \
  --push .

2. Produktionsserver vorbereiten

Voraussetzungen:

  • Linux-Server mit Docker Engine und Docker-Compose-Plugin
  • HTTPS-Reverse-Proxy wie Caddy, nginx oder Traefik
  • DNS-Name, der auf den Server zeigt

Repository installieren:

sudo mkdir -p /opt/domain-control
sudo chown "$USER":"$USER" /opt/domain-control
git clone https://git.j-mahler.de/admin/domain-control-webinterface-prod.git /opt/domain-control
cd /opt/domain-control

cp .env.production.example .env.production
mkdir -p secrets backups
chmod 700 secrets backups
openssl rand -base64 32 > secrets/dyndns_master_key
chmod 644 secrets/dyndns_master_key

Der Master-Key wird vom unprivilegierten Benutzer im Container gelesen. Deshalb ist die Datei lesbar, während das übergeordnete Verzeichnis auf dem Host nur für seinen Besitzer zugänglich bleibt. Den Schlüssel niemals nachträglich ersetzen: Ohne ihn lassen sich gespeicherte Hetzner-Tokens nicht mehr entschlüsseln.

Bei einer privaten Registry auf dem Produktionsserver einmalig mit einem Token anmelden, der nur Leserechte auf Container-Pakete besitzt:

echo "$GITEA_PULL_TOKEN" | docker login git.j-mahler.de --username deploy-bot --password-stdin

In .env.production mindestens IMAGE_TAG kontrollieren:

DYNDNS_IMAGE=git.j-mahler.de/admin/domain-control-webinterface-prod
IMAGE_TAG=v1.0.0
BIND_ADDRESS=127.0.0.1
WEB_PORT=8080
APP_MODE=https
MASTER_KEY_FILE=./secrets/dyndns_master_key

Anwendung laden und starten:

docker compose --env-file .env.production -f compose.production.yml pull
docker compose --env-file .env.production -f compose.production.yml up -d
docker compose --env-file .env.production -f compose.production.yml ps
curl --fail http://127.0.0.1:8080/healthz

3. HTTPS-Reverse-Proxy

Port 8080 bleibt mit BIND_ADDRESS=127.0.0.1 ausschließlich lokal erreichbar. Ein minimales Caddy-Beispiel:

dns.example.de {
    reverse_proxy 127.0.0.1:8080
}

Danach https://dns.example.de öffnen und den ersten Administrator anlegen. APP_MODE=https sorgt dabei für HTTPS-gebundene Session-Cookies.

Nur in einem isolierten, vertrauenswürdigen LAN darf stattdessen eine private Server-IP und APP_MODE=lan verwendet werden. Den Dienst nicht unverschlüsselt über 0.0.0.0 ins Internet stellen.

4. Backup vor jedem Update

Für eine konsistente SQLite-Kopie wird der Container kurz gestoppt:

cd /opt/domain-control
docker compose --env-file .env.production -f compose.production.yml stop dyndns

docker run --rm \
  -v domain-control-data:/data:ro \
  -v "$PWD/backups:/backup" \
  alpine:3.23.2 \
  cp /data/dyndns.db /backup/dyndns-before-update.db

docker compose --env-file .env.production -f compose.production.yml start dyndns

Zusätzlich secrets/dyndns_master_key verschlüsselt und außerhalb dieses Servers sichern. Eine Datenbanksicherung ohne den dazugehörigen Master-Key ist für gespeicherte API-Tokens unbrauchbar.

5. Update

Nach Veröffentlichung beispielsweise von v1.1.0:

  1. Backup wie oben erstellen.

  2. In .env.production setzen:

    IMAGE_TAG=v1.1.0
    
  3. Neues Image laden und Container ersetzen:

    docker compose --env-file .env.production -f compose.production.yml pull
    docker compose --env-file .env.production -f compose.production.yml up -d
    docker compose --env-file .env.production -f compose.production.yml ps
    docker compose --env-file .env.production -f compose.production.yml logs --tail=100
    curl --fail http://127.0.0.1:8080/healthz
    

Das benannte Volume domain-control-data wird dabei nicht ersetzt.

6. Rollback

Ohne Datenbankmigration reicht es, in .env.production den vorherigen IMAGE_TAG einzutragen und erneut pull sowie up -d auszuführen.

Wenn die neue Version eine Datenbankmigration ausgeführt hat, gemeinsam auf das vorherige Image und die unmittelbar davor erstellte Datenbanksicherung zurückgehen. Automatische, unbeaufsichtigte Updates sind deshalb nicht empfohlen.

7. Nützliche Betriebsbefehle

# Status
docker compose --env-file .env.production -f compose.production.yml ps

# Logs
docker compose --env-file .env.production -f compose.production.yml logs -f --tail=100

# Neustart
docker compose --env-file .env.production -f compose.production.yml restart dyndns

# Bereits heruntergeladene, ungenutzte Images aufräumen
docker image prune