From 38ab90f721765060ad01aee91a81ce3785471a33 Mon Sep 17 00:00:00 2001 From: Tronax Date: Tue, 25 Aug 2026 19:21:27 +0200 Subject: [PATCH] feat(docker): add single-image multi-stage Docker setup with compose - Multi-stage build: Node builds the frontend, Go builds a static CGO-free binary, runtime is Alpine with CA certificates - Add docker-compose.yml with persistent volume for the SQLite database - Add .dockerignore to keep the build context slim - Embed time/tzdata in the Go binary for OS-independent timezones - Document image build, startup, secrets, and reverse-proxy setup in README --- .dockerignore | 13 +++++++++++ Dockerfile | 45 ++++++++++++++++++++++++++++++++++++++ README.md | 32 +++++++++++++++++++++++++++ backend/cmd/server/main.go | 2 ++ docker-compose.yml | 23 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b962a71 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Build-Kontext schlank halten +**/node_modules +frontend/dist +**/*.db +**/*.db-wal +**/*.db-shm +.env +**/.env +!**/.env.example +.git +.gitignore +*.md +tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d132cd9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# ────────────────────────────────────────────────────────────────────── +# WannPassts – Multi-Stage-Build: Frontend + Backend in einem Image +# Bauen: docker build -t wannpassts . +# Starten: docker run -p 8080:8080 -v wannpassts-data:/data wannpassts +# ────────────────────────────────────────────────────────────────────── + +# ── Stage 1: Frontend bauen ─────────────────────────────────────────── +FROM node:22-alpine AS frontend +WORKDIR /build +# Dependencies zuerst (besserer Layer-Cache) +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# ── Stage 2: Backend bauen (CGO-frei → statisches Binary) ──────────── +# go.mod verlangt Go >= 1.25 (modernc.org/sqlite) +FROM golang:1.25-alpine AS backend +WORKDIR /build +COPY backend/go.mod backend/go.sum ./ +RUN go mod download +COPY backend/ ./ +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/wannpassts ./cmd/server + +# ── Stage 3: Laufzeit ───────────────────────────────────────────────── +FROM alpine:3.20 +# ca-certificates: nötig für HTTPS zu Google/CalDAV/ICS-Servern +RUN apk add --no-cache ca-certificates tzdata \ + && adduser -D -H -u 1000 app \ + && mkdir -p /data && chown -R app:app /data +WORKDIR /app +COPY --from=backend /out/wannpassts ./wannpassts +COPY --from=frontend /build/dist ./dist + +USER app +ENV PORT=8080 \ + DB_PATH=/data/wannpassts.db \ + STATIC_DIR=/app/dist \ + APP_URL=http://localhost:8080 \ + FRONTEND_URL=http://localhost:8080 +VOLUME /data +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- http://127.0.0.1:8080/api/health || exit 1 +ENTRYPOINT ["/app/wannpassts"] diff --git a/README.md b/README.md index 32130dc..1faa6c6 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,38 @@ go run ./cmd/server Der Go-Server liefert dann das Frontend (`dist/`) **und** die API über einen Port. +## Docker (alles in einem Image) + +Multi-Stage-Build: Node baut das Frontend, Go ein statisches Binary (CGO-frei dank modernc-SQLite), Laufzeit ist Alpine mit CA-Zertifikaten (für Google-/CalDAV-/ICS-HTTPS-Aufrufe). Frontend liegt im Image unter `/app/dist` und wird vom Go-Server mit ausgeliefert — **ein Port, ein Container, eine URL.** + +```bash +# Image bauen +docker build -t wannpassts . + +# Starten (Datenbank im Volume, damit sie Updates überlebt) +docker run -d --name wannpassts -p 8080:8080 \ + -e JWT_SECRET=$(openssl rand -hex 32) \ + -e ENCRYPTION_KEY=$(openssl rand -hex 32) \ + -v wannpassts-data:/data \ + wannpassts +``` + +Oder mit Compose (liest Secrets aus `.env` im Projektroot bzw. `backend/.env`): + +```bash +cp backend/.env.example backend/.env # Secrets & Google-Zugänge eintragen +docker compose up -d --build +``` + +Hinter einem Reverse-Proxy / mit Domain zusätzlich setzen: + +```bash +-e APP_URL=https://termine.deinedomain.de \ +-e FRONTEND_URL=https://termine.deinedomain.de +``` + +Wichtig: `JWT_SECRET` und `ENCRYPTION_KEY` dauerhaft setzen (Container-Neustarts sonst neue Secrets → Logins/Token ungültig), und die Google-Redirect-URI auf `{APP_URL}/api/calendars/google/callback` konfigurieren. Das SQLite-File liegt im Volume `/data`. + ### PWA-Hinweise - Der Service Worker wird nur im Production-Build registriert (`npm run build` + Auslieferung über `STATIC_DIR`), nicht im Vite-Dev-Modus. diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index de3be6f..bd94769 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -3,6 +3,8 @@ package main import ( "log" "net/http" + // Zeitzonen-Datenbank einbetten – macht das Binary unabhängig vom OS (Docker/Distroless) + _ "time/tzdata" "wannpassts/internal/calendar" "wannpassts/internal/config" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9e95fba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + wannpassts: + build: . + image: wannpassts + container_name: wannpassts + restart: unless-stopped + ports: + - "8080:8080" + # Secrets und Google-Zugangsdaten: backend/.env anlegen + # (cp backend/.env.example backend/.env) und Kommentar entfernen: + # env_file: backend/.env + environment: + APP_URL: ${APP_URL:-http://localhost:8080} + FRONTEND_URL: ${FRONTEND_URL:-http://localhost:8080} + JWT_SECRET: ${JWT_SECRET:-} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} + GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} + GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} + volumes: + - wannpassts-data:/data + +volumes: + wannpassts-data: