# Produktivbetrieb mit Gitea Container Registry und Docker Compose Es gibt genau ein Git-Repository für Quellcode, Compose-Dateien und Dokumentation: ```text https://git.j-mahler.de/admin/hetzner-api-dyndns-records-webinterface.git ``` Releases werden nicht in ein zweites Git-Repository kopiert. Die Gitea Action veröffentlicht stattdessen versionierte Container-Pakete unter: ```text 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. `domain-control-webinterface-prod` ist dabei ausschließlich der Name des Container-Pakets in der Gitea Registry, kein zweites Git-Repository. Das Paket wird beim ersten erfolgreichen Push der Release-Action automatisch angelegt. ## 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: ```bash 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: ```bash 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: ```bash sudo mkdir -p /opt/domain-control sudo chown "$USER":"$USER" /opt/domain-control git clone https://git.j-mahler.de/admin/hetzner-api-dyndns-records-webinterface.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: ```bash echo "$GITEA_PULL_TOKEN" | docker login git.j-mahler.de --username deploy-bot --password-stdin ``` In `.env.production` mindestens `IMAGE_TAG` kontrollieren: ```dotenv 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: ```bash 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: ```caddyfile 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: ```bash 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: ```dotenv IMAGE_TAG=v1.1.0 ``` 3. Neues Image laden und Container ersetzen: ```bash 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 ```bash # 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 ```