Betrieb mit Docker
Das Form Gateway bringt zwei Dockerfiles und eine docker-compose.yml als Beispiel mit:
| Datei | Ansatz | Wann verwenden |
|---|---|---|
| Dockerfile (Standard) | Kopiert das fertig auf dem Host gebaute Linux-Binary in ein minimales Alpine-Runtime-Image | Normalfall: Image-Build in Sekunden, keine Toolchain im Build-Kontext, exakt dasselbe Binary wie bei allen anderen Deployment-Wegen |
| Dockerfile.full | Baut Frontend + Backend komplett im Container (Multi-Stage: Node LTS → Go 1.x → Alpine) | Umgebungen ohne Go/Node auf dem Host; braucht Zugriff auf das private ORM++-Modul (Build-Arg/Token) |
Beide erzeugen dasselbe gehärtete Runtime-Image (Alpine, non-root, Healthcheck).
Schnellstart mit Docker Compose (empfohlen)
Abschnitt betitelt „Schnellstart mit Docker Compose (empfohlen)“make docker-build # Linux-Binary bauen (inkl. Audits) + Image erzeugendocker compose up -ddocker compose logs -f # Erststart: hier steht das generierte Admin-Passwort!make docker-build erledigt beides in einem Schritt: erst der normale,
sicherheitsgeprüfte Build (npm audit → Vite → govulncheck →
Cross-Compile für Linux, Architektur automatisch erkannt, überschreibbar
mit DOCKER_ARCH=arm64), dann der Sekunden-schnelle Image-Build.
Danach läuft die App auf http://localhost:8080. Beim ersten Start
entstehen im Host-Ordner ./data:
data/├── config.json Konfiguration (inkl. JWT-Secret und encryption_key — sichern!)└── gateway.db SQLite-Datenbank (+ -wal/-shm im Betrieb)Beide Dateien liegen durch den Bind-Mount auf dem Host — einsehen,
sichern, anpassen (nach Änderungen: docker compose restart).
Stoppen/Updaten:
docker compose down # stoppen (Daten bleiben in ./data)make docker-build # neue Version bauendocker compose up -d # startenSchema-Änderungen einer neuen Version wendet ORM++ beim Start selbst an (additiv automatisch, Umbauten über die versionierten Migrationsschritte — siehe database.md).
Nur mit dem Docker-Befehl (ohne Compose)
Abschnitt betitelt „Nur mit dem Docker-Befehl (ohne Compose)“make build-linux # bzw. make build-linux-arm64 auf ARM-Hostsdocker build -t form-gateway .Oder komplett im Container: docker build -f Dockerfile.full -t form-gateway .
Container starten — mit denselben Härtungs-Optionen wie im Compose-Beispiel:
docker run -d \ --name form-gateway \ -p 8080:8080 \ -v "$(pwd)/data:/data" \ --read-only \ --security-opt no-new-privileges \ --cap-drop ALL \ --restart unless-stopped \ form-gatewayAdmin-Passwort des Erststarts anzeigen, stoppen, entfernen:
docker logs form-gateway | grep -A3 Seedingdocker stop form-gatewaydocker rm form-gateway # ./data bleibt erhaltenWie das Image aufgebaut ist
Abschnitt betitelt „Wie das Image aufgebaut ist“Standard-Dockerfile: ein minimales alpine:3, in das nur das auf dem
Host gebaute Linux-Binary kopiert wird (bin/form-gateway-linux-<arch>;
ARG TARGETARCH wählt automatisch). Sämtliche Sicherheits-Gates und die
Versions-Injektion laufen im Makefile-Build — im Image landen nur
CA-Zertifikate und das Binary. Zeitzonendaten sind ins Go-Binary
eingebettet (time/tzdata).
Dockerfile.full: Stage 1 baut das Frontend (node:lts-alpine),
Stage 2 das Go-Binary (golang:1-alpine, govulncheck,
CGO_ENABLED=0), Stage 3 ist dasselbe minimale Runtime. Build-Werkzeuge
und Quellcode sind im fertigen Image nicht enthalten.
Sicherheits-Härtung im Detail
Abschnitt betitelt „Sicherheits-Härtung im Detail“Im Image (beide Dockerfiles):
| Maßnahme | Wirkung |
|---|---|
apk upgrade --no-cache | aktuelle Alpine-Security-Patches zum Build-Zeitpunkt |
Nur ca-certificates installiert | minimale Angriffsfläche (auch nötig für TLS zu SMTP-Servern und die Reputations-Downloads) |
USER app (UID 1000, nologin) | Prozess läuft nie als root |
CGO_ENABLED=0 | statisches Binary, keine libc-Schwachstellen |
npm audit + govulncheck im Build | verwundbare Abhängigkeiten brechen den Build ab |
HEALTHCHECK auf /api/v1/health | Orchestrierung erkennt hängende Container |
Zur Laufzeit (Compose/docker run-Flags):
| Maßnahme | Wirkung |
|---|---|
read_only: true | Root-Dateisystem unveränderlich; beschreibbar nur /data |
no-new-privileges | keine Privilegien-Eskalation |
cap_drop: ALL | sämtliche Linux-Capabilities entfernt |
Port-Mapping statt --network host | eigener Netzwerk-Namespace |
TLS: Der Container spricht HTTP. Für öffentliche Deployments einen
Reverse-Proxy (Caddy, Traefik, nginx) davorschalten — die Public API
(/v1) und das Admin-UI laufen über denselben Port. Ausgehend
braucht der Container Zugriff auf die SMTP-Server der Routen (Port
587/465) und — für den Reputations-Refresh — auf iptoasn.com und
check.torproject.org.
Berechtigungen des Daten-Volumes
Abschnitt betitelt „Berechtigungen des Daten-Volumes“Der Container schreibt als UID 1000. Auf den meisten Hosts
funktioniert der Bind-Mount ./data:/data direkt; bei abweichender
Host-UID:
mkdir -p data && sudo chown 1000 dataAlternativ ein Named Volume:
volumes: - formgw-data:/data# ...volumes: formgw-data:Konfiguration im Container
Abschnitt betitelt „Konfiguration im Container“Die normale Konfiguration passiert über data/config.json (entsteht
beim Erststart). Umgebungsvariablen erlauben Container-typische
Overrides:
| Variable | Bedeutung | Default im Image |
|---|---|---|
TZ | Zeitzone (Logs, Zeitstempel), z.B. Europe/Berlin | Etc/UTC |
FORMGW_HOST | Listen-Adresse | 0.0.0.0 (nötig fürs Port-Mapping) |
FORMGW_PORT | Listen-Port | Wert aus config.json (8080) |
FORMGW_DATA | Datenverzeichnis | /data |
Beispiel (Compose):
environment: TZ: Europe/Berlin FORMGW_PORT: "9000"ports: - "9000:9000"PostgreSQL statt SQLite: in der data/config.json database_dsn
setzen (z. B. postgres://formgw:…@db:5432/formgw) — dank ORM++ ohne
weitere Änderung; die Postgres-Instanz als weiteren Compose-Service
betreiben.
Das -debug-Flag funktioniert auch im Container:
docker run … form-gateway -debug (Argumente nach dem Image-Namen gehen
an das Binary, dank ENTRYPOINT).