From 89cf92eaf5c556809400ef5eef5dc8d48e9bcd8b Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 8 Feb 2026 19:59:49 +0100 Subject: [PATCH] added docker setup --- .dockerignore | 51 ++++++++++++++ backend/dist/index.js | 14 ++++ backend/dist/index.js.map | 2 +- backend/src/index.ts | 17 +++++ docker/.env.example | 26 +++++++ docker/Caddyfile | 34 ++++++++++ docker/Dockerfile | 97 ++++++++++++++++++++++++++ docker/README.md | 139 ++++++++++++++++++++++++++++++++++++++ docker/docker-compose.yml | 88 ++++++++++++++++++++++++ docker/entrypoint.sh | 34 ++++++++++ 10 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 docker/.env.example create mode 100644 docker/Caddyfile create mode 100644 docker/Dockerfile create mode 100644 docker/README.md create mode 100644 docker/docker-compose.yml create mode 100644 docker/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c3195c4c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# Dependencies +node_modules +*/node_modules + +# Build output (will be built in container) +frontend/dist +backend/dist + +# Development files +.git +.gitignore +.vscode +.idea +*.md +!docker/README.md + +# Environment files (use Docker environment instead) +.env +.env.local +.env.*.local +backend/.env +frontend/.env + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db + +# Test files +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx +__tests__ +coverage + +# Docker files in wrong location +docker-compose.yml + +# Uploads and backups (mounted as volumes) +uploads +backups +backend/uploads +backend/backups + +# Prisma migrations (included, but not dev db) +*.db +*.db-journal diff --git a/backend/dist/index.js b/backend/dist/index.js index f1ebcb4f..f0295f03 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -66,6 +66,20 @@ app.use('/api', contractHistory_routes_js_1.default); app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); +// Production: Serve frontend static files +if (process.env.NODE_ENV === 'production') { + const publicPath = path_1.default.join(process.cwd(), 'public'); + // Serve static files + app.use(express_1.default.static(publicPath)); + // SPA fallback: serve index.html for all non-API routes + app.get('*', (req, res, next) => { + // Skip API routes + if (req.path.startsWith('/api')) { + return next(); + } + res.sendFile(path_1.default.join(publicPath, 'index.html')); + }); +} // Error handling app.use((err, req, res, next) => { console.error(err.stack); diff --git a/backend/dist/index.js.map b/backend/dist/index.js.map index dd5894f8..f625268c 100644 --- a/backend/dist/index.js.map +++ b/backend/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 4763d7bb..76a39fa3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -69,6 +69,23 @@ app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); +// Production: Serve frontend static files +if (process.env.NODE_ENV === 'production') { + const publicPath = path.join(process.cwd(), 'public'); + + // Serve static files + app.use(express.static(publicPath)); + + // SPA fallback: serve index.html for all non-API routes + app.get('*', (req, res, next) => { + // Skip API routes + if (req.path.startsWith('/api')) { + return next(); + } + res.sendFile(path.join(publicPath, 'index.html')); + }); +} + // Error handling app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error(err.stack); diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..3c3c2042 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,26 @@ +# 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 +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 +JWT_EXPIRES_IN=7d + +# Encryption Key (for portal credentials) +# Generate with: openssl rand -hex 32 +ENCRYPTION_KEY=change-this-to-a-32-byte-hex-key + +# First Install: Set to "true" to seed database on first startup +# After first successful start, set back to "false" +RUN_SEED=true diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 00000000..a6cf415c --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,34 @@ +# OpenCRM Caddyfile +# =================== +# Replace {$DOMAIN} with your actual domain or use environment variable +# For local development without SSL, use localhost:80 + +{$DOMAIN:localhost} { + # Reverse proxy to OpenCRM app + reverse_proxy app:3001 + + # Logging + log { + output stdout + format console + } + + # Security headers + header { + # Clickjacking protection + X-Frame-Options "SAMEORIGIN" + # XSS protection + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + # Referrer policy + Referrer-Policy "strict-origin-when-cross-origin" + } + + # Gzip compression + encode gzip + + # Handle file uploads (increase body limit) + request_body { + max_size 50MB + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..3965f539 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,97 @@ +# =================================== +# 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"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..34f197c9 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,139 @@ +# 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 +``` diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..7206d00d --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,88 @@ +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} + JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required} + 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: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..03c51564 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,34 @@ +#!/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 + +# Start the application +echo "Starting OpenCRM server..." +exec node dist/index.js