deploy: add production Compose without Traefik
All checks were successful
check / verify (push) Successful in 3m2s
check / publish (push) Has been skipped

This commit is contained in:
Weapie 2026-10-08 13:42:27 +02:00
parent dd79dde532
commit 9317a6131f
3 changed files with 149 additions and 0 deletions

View File

@ -12,6 +12,9 @@ NEXTAUTH_SECRET=
POSTGRES_VOLUME=
UPLOADS_VOLUME=
TRAEFIK_NETWORK=dokploy-network
# Only docker-compose.production.direct.yml (no Traefik):
APP_BIND_ADDRESS=127.0.0.1
APP_PORT=3000
APP_TIMEZONE=Europe/Berlin
TRUST_PROXY=false
GROUP_UPLOAD_QUOTA_MB=1024

View File

@ -4,6 +4,31 @@
## Konfiguration
### Variante ohne Traefik
`docker-compose.production.direct.yml` ist ebenfalls eigenständig und benötigt weder Dokploy noch ein externes Proxy-Netz. Sie veröffentlicht die App standardmäßig auf `127.0.0.1:3000` des Docker-Hosts. Die übrigen Dienste, Daten-Volumes und Migrationen entsprechen der Traefik-Variante. Nur **eine** der beiden Compose-Dateien verwenden.
`APP_PORT` bestimmt den Host-Port. `APP_BIND_ADDRESS=127.0.0.1` eignet sich für einen auf demselben Host laufenden HTTPS-Proxy, beispielsweise Nginx oder Caddy. Bei einem separaten vorgeschalteten Proxy die erreichbare private Host-IP einstellen und den Port entsprechend auf diesen Proxy begrenzen. `0.0.0.0` bindet an alle IPv4-Schnittstellen. `TRAEFIK_NETWORK` wird in dieser Variante nicht verwendet.
Die App selbst stellt HTTP bereit. Die öffentliche Produktionsadresse in `NEXTAUTH_URL` bleibt HTTPS; TLS muss deshalb ein vorhandener HTTPS-Proxy oder Load Balancer übernehmen. Die Datei stellt keinen eigenen TLS-Dienst bereit und deaktiviert die HTTPS-Prüfung der Anwendung nicht. Eine Nginx-Vorlage liegt unter [deploy/nginx.conf.example](deploy/nginx.conf.example).
Für alle nachfolgenden CLI-Schritte und die Datenübernahme diese Funktion verwenden:
```bash
dc() { docker compose --env-file .env.production -p taskmanager-next -f docker-compose.production.direct.yml "$@"; }
dc config --quiet
```
Den Projektnamen konsistent auf den Namen der vorbereiteten Zielinstallation setzen. Die spätere Funktionsdefinition für die Dokploy-Variante überspringen. Nach Restore, Migration und Start lässt sich die App lokal prüfen:
```bash
curl --fail http://127.0.0.1:3000/api/health
```
Bei abweichendem `APP_PORT` oder `APP_BIND_ADDRESS` die Prüfadresse entsprechend anpassen.
### Variante mit Dokploy/Traefik
In Dokploy ein Compose-Projekt mit `docker-compose.production.yml` konfigurieren. Die Werte aus `.env.production.example` in die Compose-Umgebung übernehmen. Alternativ auf dem Server:
```bash

View File

@ -0,0 +1,121 @@
# Standalone production deployment with a configurable host port; no Traefik required.
# Read PRODUCTION.md before deploying an existing installation.
x-app-environment: &app-environment
NODE_ENV: production
DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL with host db and URL-encoded credentials}
NEXTAUTH_URL: ${NEXTAUTH_URL:?Set the public HTTPS URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?Set NEXTAUTH_SECRET}
ALLOW_LOCAL_HTTP: "false"
TRUST_PROXY: ${TRUST_PROXY:-false}
APP_TIMEZONE: ${APP_TIMEZONE:-Europe/Berlin}
UPLOAD_DIR: /app/private-uploads
GROUP_UPLOAD_QUOTA_MB: ${GROUP_UPLOAD_QUOTA_MB:-1024}
ARCHIVE_RETENTION_DAYS: ${ARCHIVE_RETENTION_DAYS:-0}
SMTP_URL: ${SMTP_URL:-}
MAIL_FROM: ${MAIL_FROM:-}
OIDC_ISSUER: ${OIDC_ISSUER:-}
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
OIDC_REQUIRED_ACR: ${OIDC_REQUIRED_ACR:-}
WEBHOOK_ALLOWED_HOSTS: ${WEBHOOK_ALLOWED_HOSTS:-}
x-logging: &logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-taskmanager}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-taskmanager}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\""]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
stop_grace_period: 60s
logging: *logging
migrate:
image: git.jfritzsche.de/jf/taskmanager-operations:${TASKMANAGER_VERSION:?Set a published release version}
restart: "no"
init: true
user: "1001:1001"
security_opt: ["no-new-privileges:true"]
environment:
<<: *app-environment
LEGACY_UPLOAD_DIR: /app/private-uploads
command: ["sh", "-c", "npx prisma migrate deploy && npx tsx scripts/migrate-access.ts && npx tsx scripts/migrate-uploads.ts"]
volumes:
- uploads:/app/private-uploads
depends_on:
db:
condition: service_healthy
logging: *logging
app:
image: git.jfritzsche.de/jf/taskmanager:${TASKMANAGER_VERSION:?Set a published release version}
restart: unless-stopped
init: true
user: "1001:1001"
security_opt: ["no-new-privileges:true"]
cap_drop: ["ALL"]
read_only: true
tmpfs: ["/tmp:rw,noexec,nosuid,size=64m"]
environment: *app-environment
ports:
- "${APP_BIND_ADDRESS:-127.0.0.1}:${APP_PORT:-3000}:3000"
volumes:
- uploads:/app/private-uploads
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health',{signal:AbortSignal.timeout(4000)}).then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3
depends_on:
migrate:
condition: service_completed_successfully
stop_grace_period: 30s
logging: *logging
worker:
image: git.jfritzsche.de/jf/taskmanager-operations:${TASKMANAGER_VERSION:?Set a published release version}
restart: unless-stopped
init: true
user: "1001:1001"
security_opt: ["no-new-privileges:true"]
cap_drop: ["ALL"]
environment:
<<: *app-environment
WORKER_HEARTBEAT_FILE: /tmp/taskmanager-worker-heartbeat
command: ["node", "--import", "tsx", "scripts/worker.ts"]
volumes:
- uploads:/app/private-uploads
healthcheck:
test: ["CMD", "node", "-e", "const fs=require('fs');process.exit(Date.now()-fs.statSync('/tmp/taskmanager-worker-heartbeat').mtimeMs<180000?0:1)"]
interval: 30s
timeout: 5s
start_period: 60s
retries: 3
depends_on:
app:
condition: service_healthy
stop_grace_period: 30s
logging: *logging
volumes:
postgres_data:
external: true
name: ${POSTGRES_VOLUME:?Set the prepared PostgreSQL target volume name}
uploads:
external: true
name: ${UPLOADS_VOLUME:?Set the prepared uploads target volume name}