Nur noch eine docker-compose.yml und eine .env

Der zweite Stack unter docker/ war ein Duplikat aus Februar, das nie
mitgepflegt wurde - und genau deshalb schwer zu finden und leicht falsch zu
bedienen. Statt ihn zu loeschen (Caddy ist fuer Betreiber ohne eigenen
Reverse-Proxy zurecht gewuenscht) ist er jetzt in den Hauptstack integriert:

- caddy als optionaler Dienst mit `profiles: ["caddy"]` in docker-compose.yml.
  Ohne `--profile caddy` wird er nicht einmal angelegt - empirisch geprueft mit
  einem separaten Testprojekt: `up -d` startete nur den ungeschuetzten Dienst.
  Der laufende Stack bleibt damit unveraendert.
- Caddyfile in den Projektstamm verschoben, proxy-Ziel auf den Dienstnamen
  `opencrm` angepasst, Zertifikate unter ./data/caddy wie alle anderen Daten.
- DOMAIN, CADDY_DIR und CADDY_CONFIG_DIR in die .env.example aufgenommen.
- docker/ entfernt (Dockerfile, entrypoint.sh, docker-compose.yml,
  .env.example, README.md). Das dortige Dockerfile basierte noch auf Alpine -
  genau die Variante, von der das Projekt wegen Prisma-/TLS-Problemen bewusst
  auf node:20-slim gewechselt ist. Das gepflegte backend/Dockerfile baut
  Frontend und Backend ebenso.
- README: Abschnitt "Docker (Produktion)" ersetzt durch "Betrieb mit eigenem
  SSL", inkl. Hinweisen zu Let's-Encrypt-Limits, DNS-Voraussetzung,
  HTTPS_ENABLED und dem weiterhin offenen App-Port. Projektbaum und
  .dockerignore nachgezogen.

Geprueft: docker-compose config gueltig, caddy validate gueltig, laufende
Container unberuehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 19:40:43 +02:00
co-authored by Claude Opus 5
parent ee83b09ed8
commit 22501f4650
10 changed files with 70 additions and 484 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ backend/dist
.vscode
.idea
*.md
!docker/README.md
# Environment files (use Docker environment instead)
.env
+8
View File
@@ -53,6 +53,14 @@ JWT_REFRESH_EXPIRES_IN=7d
# Generieren: openssl rand -hex 32
ENCRYPTION_KEY=change-this-to-64-hex-characters-please-rotate-before-production-xx
# ---------- Optionaler Reverse-Proxy (Caddy) ----------
# Nur nötig, wenn du KEINEN eigenen Reverse-Proxy hast und Caddy das SSL
# übernehmen soll. Starten dann mit: docker-compose --profile caddy up -d
# Ohne dieses Profil bleibt Caddy aus und ändert nichts am Setup.
DOMAIN=crm.example.com
CADDY_DIR=./data/caddy
CADDY_CONFIG_DIR=./data/caddy-config
# ---------- Siegel für das Audit-Log ----------
# OpenCRM protokolliert, wer wann welche Daten gesehen oder geändert hat die
# Beweisspur bei Streit oder Datenschutz-Nachfragen. Dieser Schlüssel versieht
+6 -3
View File
@@ -1,11 +1,14 @@
# OpenCRM Caddyfile
# ===================
# Replace {$DOMAIN} with your actual domain or use environment variable
# For local development without SSL, use localhost:80
# Wird NUR gebraucht, wenn du den optionalen Caddy-Dienst startest:
# docker-compose --profile caddy up -d
# Caddy holt und erneuert dann das SSL-Zertifikat automatisch fuer ${DOMAIN}.
# Betreibst du bereits einen eigenen Reverse-Proxy (nginx, NPM, Traefik),
# brauchst du diese Datei nicht - dann laeuft alles ueber deinen Proxy.
{$DOMAIN:localhost} {
# Reverse proxy to OpenCRM app
reverse_proxy app:3001
reverse_proxy opencrm:3001
# Logging
log {
+31 -36
View File
@@ -341,9 +341,8 @@ openssl rand -hex 32
| Betrieb | Datei |
|---|---|
| **Docker** (Standard, `docker-compose.yml` im Projektstamm) | **`.env` im Projektstamm** |
| **Docker** (Standard) | **`.env` im Projektstamm** |
| Backend direkt, ohne Container (Entwicklung) | `backend/.env` |
| Alternativer Caddy-Stack unter `docker/` | `docker/.env` |
Beim Docker-Betrieb reicht der Eintrag in der Stamm-`.env` `docker-compose.yml`
reicht die Variable an den Container weiter. Ein Eintrag in `backend/.env` wirkt
@@ -817,48 +816,44 @@ docker-compose down -v # Container stoppen + Daten löschen
docker-compose logs -f # Logs anzeigen
```
### Docker (Produktion)
### Betrieb mit eigenem SSL (optionaler Caddy-Dienst)
Im `docker/` Verzeichnis liegt ein komplettes Produktions-Setup:
Es gibt **eine** `docker-compose.yml` und **eine** `.env` im Projektstamm.
Standardmässig bringt der Stack **keinen** Reverse-Proxy mit die Annahme ist,
dass du bereits einen hast (nginx, Nginx Proxy Manager, Traefik) und OpenCRM
dahinter hängst.
Hast du **keinen**, kannst du Caddy als optionalen Dienst dazuschalten. Er holt
und erneuert das SSL-Zertifikat automatisch:
```bash
cd docker
# In der .env die Domain setzen
DOMAIN=crm.example.de
HTTPS_ENABLED=true
# Image bauen
docker-compose build
# Container starten
docker-compose up -d
# Logs anzeigen
docker-compose logs -f app
# Mit Caddy starten (Ports 80 + 443 müssen frei sein)
docker-compose --profile caddy up -d
```
**Komponenten:**
- **MariaDB 10.11**: Datenbank
- **App**: Backend + Frontend in einem Container
- **Caddy**: Reverse-Proxy mit automatischem SSL
Ohne `--profile caddy` bleibt Caddy aus und nichts ändert sich der Dienst
wird dann gar nicht erst angelegt.
> **Wann dieser Stack?** Wenn du **keinen** eigenen Reverse-Proxy betreibst
> Caddy holt und erneuert das SSL-Zertifikat hier selbst. Hast du bereits einen
> Proxy davor (nginx, Nginx Proxy Manager, Traefik), nimm den Stack im
> Projektstamm; er bringt bewusst keinen eigenen Proxy mit.
**Was dabei zu beachten ist:**
**Umgebungsvariablen (`docker/.env`):** vollständige Vorlage mit Erklärungen
in `docker/.env.example`
- Die Zertifikate liegen unter `./data/caddy`. Diesen Ordner **nicht löschen**,
sonst werden bei jedem Start neue Zertifikate angefordert und du läufst in
die Mengenbegrenzung von Let's Encrypt.
- Die Domain muss vorher per DNS auf den Server zeigen, sonst schlägt die
Zertifikatsausstellung fehl.
- `HTTPS_ENABLED=true` setzen das aktiviert das `Secure`-Attribut am
Refresh-Cookie und die korrekte Proxy-Erkennung.
- Der direkte App-Port (`OPENCRM_PORT`, Standard 3010) bleibt weiterhin offen.
Wenn nur Caddy von aussen erreichbar sein soll, schliesse ihn per Firewall
oder setze ihn in der `.env` auf einen Wert, den du nicht freigibst.
```env
DB_ROOT_PASSWORD=sicheres-root-passwort
DB_NAME=opencrm
DB_USER=opencrm
DB_PASSWORD=sicheres-passwort
JWT_SECRET=sehr-langer-zufaelliger-string
ENCRYPTION_KEY=64-zeichen-hex-string
DOMAIN=crm.example.com
HTTPS_ENABLED=true # Caddy terminiert TLS → Secure-Cookies an
AUDIT_HMAC_KEY= # Siegel fürs Audit-Log, siehe oben
RUN_SEED=true # Nur beim ersten Start
```
Die Konfiguration liegt in der `Caddyfile` im Projektstamm. Sie liefert
Sicherheits-Header mit und komprimiert bewusst **nicht** die API-Antworten
(BREACH-Schutz, siehe Abschnitt zum Reverse-Proxy oben).
## Projektstruktur
@@ -910,7 +905,7 @@ opencrm/
│ │ ├── types/ # TypeScript-Typen
│ │ └── App.tsx # Haupt-Komponente
│ └── package.json
├── docker/ # Docker-Deployment
├── Caddyfile # Optionaler Reverse-Proxy (nur mit --profile caddy)
│ ├── Dockerfile # Multi-Stage Build
│ ├── docker-compose.yml # Produktion (MariaDB, App, Caddy)
│ ├── Caddyfile # Reverse-Proxy mit SSL
+24
View File
@@ -90,6 +90,30 @@ services:
# Auto-aktualisierter BLZ-/Bankdaten-Datensatz (Bundesbank-Bankleitzahlen)
- ${BANKDATA_DIR:-./data/bankdata}:/app/bankdata
# Optionaler Reverse-Proxy mit automatischem SSL nur fuer Betreiber OHNE
# eigenen Proxy. Startet NICHT mit `docker-compose up -d`, sondern nur mit:
# docker-compose --profile caddy up -d
# Wer nginx / Nginx Proxy Manager / Traefik davor hat, laesst das weg.
caddy:
image: caddy:2-alpine
container_name: opencrm-caddy
restart: unless-stopped
profiles: ["caddy"]
depends_on:
- opencrm
environment:
DOMAIN: ${DOMAIN:-localhost}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
# Zertifikate liegen wie alle Daten unter ./data/ nicht loeschen,
# sonst werden bei jedem Start neue Zertifikate angefordert
# (Let's-Encrypt-Limits!).
- ${CADDY_DIR:-./data/caddy}:/data
- ${CADDY_CONFIG_DIR:-./data/caddy-config}:/config
adminer:
image: adminer:latest
container_name: opencrm-adminer
-61
View File
@@ -1,61 +0,0 @@
# OpenCRM Docker Environment
# ============================
# Copy this file to .env and adjust the values
# Domain (for Caddy SSL certificate)
# Use your actual domain, e.g., crm.example.com
DOMAIN=localhost
# Database
# HINWEIS: In diesem Stack wird die Verbindungs-URL direkt aus diesen Werten
# zusammengesetzt. Sonderzeichen im Passwort ($ ! # @ : / ?) koennen die URL
# zerlegen und den Start verhindern. Nutze hier ein langes Passwort aus
# Buchstaben und Ziffern - oder den Stack im Projektstamm, dessen Entrypoint
# die Zeichen korrekt kodiert.
DB_ROOT_PASSWORD=change-this-root-password
DB_NAME=opencrm
DB_USER=opencrm
DB_PASSWORD=change-this-password
# JWT Authentication
# Generate with: openssl rand -base64 32
JWT_SECRET=change-this-to-a-secure-random-string
# Access-Token kurz (XSS-Schutz, Token lebt nur im JS-Memory).
# Refresh-Token lang im httpOnly-Cookie.
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
# Encryption Key (for portal credentials)
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=change-this-to-a-32-byte-hex-key
# TLS: In diesem Stack terminiert Caddy das HTTPS. Das Flag setzt das
# Secure-Attribut am Refresh-Cookie, trust proxy = 1 Hop und
# upgrade-insecure-requests in der CSP.
# Nur auf false setzen, wenn der Stack bewusst ohne TLS laeuft (reiner Test).
HTTPS_ENABLED=true
# Nur noetig, wenn das Frontend unter einer ANDEREN Domain laeuft als das
# Backend. Beim Standard-Setup (alles hinter derselben Caddy-Domain) leer
# lassen.
CORS_ORIGINS=
# Cloud-Deploy: blockt zusaetzlich private IP-Ranges, damit ein eingeloggter
# Admin ueber die Provider-/SMTP-Testverbindung keine internen Dienste
# anpingen kann. On-Prem meist false, weil Mailserver oft lokal laufen.
SSRF_BLOCK_PRIVATE_IPS=false
# First Install: Set to "true" to seed database on first startup
# After first successful start, set back to "false"
RUN_SEED=true
# ---------- Siegel für das Audit-Log ----------
# Signiert jeden neuen Audit-Eintrag mit einem Schlüssel ausserhalb der
# Datenbank, damit sich die Beweisspur nicht nachtraeglich faelschen laesst.
# Generieren: openssl rand -hex 32 (pro Umgebung ein eigener Wert).
# Ohne Wert laeuft alles normal weiter, nur ungesiegelt.
# Einmal gesetzt nicht wieder entfernen - Details in der README.
AUDIT_HMAC_KEY=
# Nur beim Schluesselwechsel: frueherer Schluessel, mehrere kommagetrennt.
AUDIT_HMAC_KEY_OLD=
-97
View File
@@ -1,97 +0,0 @@
# ===================================
# Stage 1: Build Frontend
# ===================================
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
# Copy package files
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci
# Copy frontend source
COPY frontend/ ./
# Build frontend
RUN npm run build
# ===================================
# Stage 2: Build Backend
# ===================================
FROM node:20-alpine AS backend-builder
WORKDIR /app/backend
# Install OpenSSL for Prisma
RUN apk add --no-cache openssl
# Copy package files
COPY backend/package*.json ./
# Install dependencies (including dev for build)
RUN npm ci
# Copy backend source
COPY backend/ ./
# Generate Prisma client
RUN npx prisma generate
# Build TypeScript
RUN npm run build
# ===================================
# Stage 3: Production Runtime
# ===================================
FROM node:20-alpine AS production
WORKDIR /app
# Install OpenSSL for Prisma runtime and netcat for DB health check
RUN apk add --no-cache openssl netcat-openbsd
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S opencrm -u 1001 -G nodejs
# Copy backend package files
COPY backend/package*.json ./
# Install production dependencies + tsx for seed script
RUN npm ci --omit=dev && npm install tsx
# Copy Prisma schema and generate client
COPY backend/prisma ./prisma
RUN npx prisma generate
# Copy built backend
COPY --from=backend-builder /app/backend/dist ./dist
# Copy built frontend to public directory
COPY --from=frontend-builder /app/frontend/dist ./public
# Copy seed file for factory reset
COPY backend/prisma/seed.ts ./prisma/
# Copy entrypoint script
COPY docker/entrypoint.sh ./entrypoint.sh
# Create directories for uploads and backups
RUN mkdir -p uploads backups && \
chmod +x entrypoint.sh && \
chown -R opencrm:nodejs /app
# Switch to non-root user
USER opencrm
# Expose port
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/api/health || exit 1
# Start the application with entrypoint
ENTRYPOINT ["./entrypoint.sh"]
-139
View File
@@ -1,139 +0,0 @@
# OpenCRM Docker Deployment
## Schnellstart
1. **Umgebungsvariablen konfigurieren:**
```bash
cd docker
cp .env.example .env
nano .env # Sichere Werte setzen
```
2. **Container starten (erster Start mit RUN_SEED=true):**
```bash
docker compose up -d
```
Beim ersten Start wird automatisch:
- Auf die Datenbank gewartet
- Migrationen ausgeführt
- Seed-Daten geladen (wenn `RUN_SEED=true`)
3. **Nach erfolgreicher Installation:**
```bash
# RUN_SEED in .env auf false setzen
sed -i 's/RUN_SEED=true/RUN_SEED=false/' .env
```
4. **Anwendung aufrufen:**
- Mit Domain: `https://your-domain.com`
- Lokal: `http://localhost`
5. **Login:**
- E-Mail: `admin@admin.com`
- Passwort: `admin`
## Architektur
```
┌─────────────┐
│ Caddy │
│ (SSL/TLS) │
│ :80/:443 │
└──────┬──────┘
┌──────▼──────┐
│ OpenCRM │
│ (Node.js) │
│ :3001 │
└──────┬──────┘
┌──────▼──────┐
│ MariaDB │
│ :3306 │
└─────────────┘
```
## Befehle
### Container verwalten
```bash
# Starten
docker compose up -d
# Stoppen
docker compose down
# Logs anzeigen
docker compose logs -f app
# Neustart
docker compose restart app
```
### Datenbank
```bash
# Migration ausführen
docker compose exec app npx prisma migrate deploy
# Seed-Daten laden
docker compose exec app npx tsx prisma/seed.ts
# Prisma Studio (Datenbank-UI)
docker compose exec app npx prisma studio
```
### Backup & Restore
```bash
# Backup-Verzeichnis ist unter /app/backups gemountet
# Backups werden über die Anwendung erstellt/wiederhergestellt
```
### Update
```bash
# Image neu bauen und Container aktualisieren
docker compose build --no-cache
docker compose up -d
# Migrationen werden automatisch beim Start ausgeführt
```
## Volumes
| Volume | Beschreibung |
|--------|--------------|
| `mariadb_data` | Datenbank-Dateien |
| `uploads_data` | Hochgeladene Dokumente |
| `backups_data` | Backup-Dateien |
| `caddy_data` | SSL-Zertifikate |
| `caddy_config` | Caddy-Konfiguration |
## SSL-Zertifikat
Caddy holt automatisch ein Let's Encrypt Zertifikat wenn:
- Die Domain in `.env` korrekt gesetzt ist
- Port 80 und 443 von außen erreichbar sind
- DNS auf den Server zeigt
Für lokale Entwicklung mit `DOMAIN=localhost` wird ein selbstsigniertes Zertifikat verwendet.
## Troubleshooting
### Container startet nicht
```bash
docker compose logs app
```
### Datenbank-Verbindung fehlgeschlagen
```bash
# Warten bis MariaDB bereit ist
docker compose logs db
```
### SSL-Zertifikat Probleme
```bash
docker compose logs caddy
# Caddy-Daten zurücksetzen
docker compose down
docker volume rm opencrm_caddy_data opencrm_caddy_config
docker compose up -d
```
-107
View File
@@ -1,107 +0,0 @@
version: '3.8'
services:
# ===================================
# MariaDB Database
# ===================================
db:
image: mariadb:10.11
container_name: opencrm-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${DB_NAME:-opencrm}
MYSQL_USER: ${DB_USER:-opencrm}
MYSQL_PASSWORD: ${DB_PASSWORD:-opencrm123}
volumes:
- mariadb_data:/var/lib/mysql
networks:
- opencrm-network
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 3
# ===================================
# OpenCRM Application
# ===================================
app:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: opencrm-app
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3001
DATABASE_URL: mysql://${DB_USER:-opencrm}:${DB_PASSWORD:-opencrm123}@db:3306/${DB_NAME:-opencrm}
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
# Access-Token kurzlebig (lebt nur im Browser-Speicher → XSS-Fenster
# klein halten), Refresh-Token lang im httpOnly-Cookie.
# Frueher stand hier 7d fuer den ACCESS-Token das war ein Ueberbleibsel
# aus der Zeit vor dem Access-/Refresh-Pattern.
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-15m}
JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required}
# Caddy terminiert TLS und steht als EIN Proxy davor. Das Flag setzt das
# Secure-Attribut am Refresh-Cookie, trust proxy = 1 Hop und
# upgrade-insecure-requests in der CSP. Nur auf false setzen, wenn der
# Stack bewusst ohne TLS laeuft (reiner Test ueber http://).
HTTPS_ENABLED: ${HTTPS_ENABLED:-true}
# Nur noetig, wenn das Frontend unter einer anderen Origin laeuft.
CORS_ORIGINS: ${CORS_ORIGINS:-}
# In der Container-Umgebung muss der Dienst fuer Caddy erreichbar sein.
LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0}
# Cloud-Deploy: private IP-Ranges fuer Provider-/SMTP-Tests blocken.
SSRF_BLOCK_PRIVATE_IPS: ${SSRF_BLOCK_PRIVATE_IPS:-false}
# Siegel für das Audit-Log (optional, fail-safe ohne Wert)
AUDIT_HMAC_KEY: ${AUDIT_HMAC_KEY:-}
AUDIT_HMAC_KEY_OLD: ${AUDIT_HMAC_KEY_OLD:-}
RUN_SEED: ${RUN_SEED:-false}
volumes:
- uploads_data:/app/uploads
- backups_data:/app/backups
networks:
- opencrm-network
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/api/health"]
start_period: 60s
interval: 30s
timeout: 10s
retries: 3
# ===================================
# Caddy Reverse Proxy (with auto SSL)
# ===================================
caddy:
image: caddy:2-alpine
container_name: opencrm-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- opencrm-network
depends_on:
app:
condition: service_healthy
networks:
opencrm-network:
driver: bridge
volumes:
mariadb_data:
uploads_data:
backups_data:
caddy_data:
caddy_config:
-40
View File
@@ -1,40 +0,0 @@
#!/bin/sh
set -e
echo "=== OpenCRM Startup ==="
# Wait for database to be ready
echo "Waiting for database connection..."
MAX_RETRIES=30
RETRY_COUNT=0
while ! nc -z db 3306 2>/dev/null; do
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
echo "Error: Database not available after $MAX_RETRIES attempts"
exit 1
fi
echo " Attempt $RETRY_COUNT/$MAX_RETRIES - waiting..."
sleep 2
done
echo "Database is ready!"
# Run migrations
echo "Running database migrations..."
npx prisma migrate deploy
# Seed database if RUN_SEED is set (first install)
if [ "$RUN_SEED" = "true" ]; then
echo "Seeding database..."
npx tsx prisma/seed.ts
fi
# Einmal-Bereinigung für Pentest-Reste (Runde 12): XSS-Strings aus
# Customer/User-Feldern entfernen + unbekannte AppSettings löschen.
# Idempotent läuft bei jedem Container-Start ohne Risiko.
echo "Running data cleanup..."
npx tsx prisma/cleanup-xss-and-mass-assignment.ts || echo " (Cleanup übersprungen, nicht-kritisch)"
# Start the application
echo "Starting OpenCRM server..."
exec node dist/index.js