feat(T06): deployment config - Dockerfiles, docker-compose, Coolify guide, env example

This commit is contained in:
2026-07-18 15:47:26 +02:00
parent fbb1b39b57
commit 1620a35d71
10 changed files with 1140 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
# Runtime Report — ERP Nutzfahrzeuge
**Date:** 2026-07-17
**Phase:** 5 — Runtime Verification
**Runtime:** `/opt/venv/bin/python` (execution environment)
---
## Summary
| Check | Status |
|-------|--------|
| Backend startup | ✅ PASS |
| Health endpoint | ✅ PASS (200, `{"status":"ok"}`) |
| Root endpoint | ✅ PASS (200, app metadata) |
| Auth login (GET) | ✅ PASS (405 Method Not Allowed — POST-only) |
| OpenAPI/Swagger | ✅ PASS (200, full schema) |
| Frontend build artifacts | ✅ PASS (`.next/` with BUILD_ID, server/, static/) |
| Runtime errors | ✅ NONE (clean logs, no tracebacks) |
| PostgreSQL | ✅ Running (localhost:5432, accepting connections) |
| Redis | ⚠️ NOT installed (JWT-based refresh tokens, no Redis storage needed) |
**Overall verdict:** Backend starts and runs cleanly. All public endpoints respond as expected. No runtime errors detected.
---
## 1. Startup Verification
**Start command:**
```bash
cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
**Result:** Server started successfully (PID 3383733).
**Startup log:**
```
INFO: Started server process [3383733]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```
**Import check:** `from app.main import app` — OK (no ImportError)
**Core dependencies verified:**
- `uvicorn`
- `fastapi`
- `sqlalchemy`
- `asyncpg`
---
## 2. Health Check
**Endpoint:** `GET http://localhost:8000/api/v1/health`
**Response:**
- HTTP Status: `200 OK`
- Body: `{"status":"ok"}`
**Verdict:** ✅ Health endpoint reachable and returning expected payload.
---
## 3. Endpoint Verification
### 3.1 Root Endpoint
- **URL:** `GET http://localhost:8000/`
- **HTTP Status:** `200 OK`
- **Body:** `{"name":"ERP Nutzfahrzeuge","version":"1.0.0","docs":"/docs"}`
### 3.2 Auth Login (GET — should fail)
- **URL:** `GET http://localhost:8000/api/v1/auth/login`
- **HTTP Status:** `405 Method Not Allowed`
- **Body:** `{"detail":"Method Not Allowed"}`
- **Expected behavior:** Login is POST-only; GET correctly rejected.
### 3.3 OpenAPI Schema
- **URL:** `GET http://localhost:8000/openapi.json`
- **HTTP Status:** `200 OK`
- **Schema version:** OpenAPI 3.1.0
- **Title:** ERP Nutzfahrzeuge Backend API
- **Version:** 1.0.0
---
## 4. Available API Endpoints (from OpenAPI)
### Auth (`/api/v1/auth`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/api/v1/auth/login` | Login with email+password → JWT tokens | No |
| POST | `/api/v1/auth/refresh` | Exchange refresh token for new token pair | No |
| GET | `/api/v1/auth/me` | Get current user profile | Bearer |
### Users (`/api/v1/users`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/users/` | List users (paginated, admin only) | Bearer |
| POST | `/api/v1/users/` | Create user (admin only) | Bearer |
| PUT | `/api/v1/users/{user_id}` | Update user (admin only) | Bearer |
| DELETE | `/api/v1/users/{user_id}` | Soft-delete user (admin only) | Bearer |
### Vehicles (`/api/v1/vehicles`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/vehicles/` | List vehicles (filter, sort, paginate) | Bearer |
| POST | `/api/v1/vehicles/` | Create vehicle | Bearer |
| GET | `/api/v1/vehicles/{vehicle_id}` | Get single vehicle | Bearer |
| PUT | `/api/v1/vehicles/{vehicle_id}` | Update vehicle | Bearer |
| DELETE | `/api/v1/vehicles/{vehicle_id}` | Soft-delete vehicle | Bearer |
| POST | `/api/v1/vehicles/{vehicle_id}/mobile-de/push` | Push to mobile.de (async, 202) | Bearer |
| GET | `/api/v1/vehicles/{vehicle_id}/mobile-de/status` | Get mobile.de sync status | Bearer |
### Contacts (`/api/v1/contacts`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/contacts/` | List contacts (filter, sort, paginate) | Bearer |
| POST | `/api/v1/contacts/` | Create contact (admin/verkaeufer) | Bearer |
| GET | `/api/v1/contacts/{contact_id}` | Get contact with persons | Bearer |
| PUT | `/api/v1/contacts/{contact_id}` | Update contact | Bearer |
| DELETE | `/api/v1/contacts/{contact_id}` | Soft-delete contact | Bearer |
| POST | `/api/v1/contacts/{contact_id}/persons` | Add contact person | Bearer |
| DELETE | `/api/v1/contacts/{contact_id}/persons/{person_id}` | Remove contact person | Bearer |
### Files (`/api/v1/vehicles/{vehicle_id}/files`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/vehicles/{vehicle_id}/files` | List files for vehicle | Bearer |
| POST | `/api/v1/vehicles/{vehicle_id}/files` | Upload file (multipart, max 20MB) | Bearer |
| GET | `/api/v1/vehicles/{vehicle_id}/files/{file_id}` | Download file | Bearer |
| DELETE | `/api/v1/vehicles/{vehicle_id}/files/{file_id}` | Delete file | Bearer |
### OCR (`/api/v1/ocr`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/api/v1/ocr/upload` | Upload scan for OCR (async, 202) | Bearer |
| GET | `/api/v1/ocr/results/{result_id}` | Get OCR result | Bearer |
| GET | `/api/v1/ocr/results` | List OCR results | Bearer |
| POST | `/api/v1/ocr/results/{result_id}/apply` | Apply OCR data to vehicle | Bearer |
### Sales (`/api/v1/sales`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/sales/` | List sales (filter by status/date) | Bearer |
| POST | `/api/v1/sales/` | Create sale (sets vehicle to 'sold') | Bearer |
| GET | `/api/v1/sales/{sale_id}` | Get sale with nested data | Bearer |
| PUT | `/api/v1/sales/{sale_id}` | Update sale | Bearer |
| DELETE | `/api/v1/sales/{sale_id}` | Cancel sale (soft delete) | Bearer |
| POST | `/api/v1/sales/{sale_id}/contract` | Regenerate contract PDF | Bearer |
| GET | `/api/v1/sales/{sale_id}/contract` | Download contract PDF | Bearer |
| POST | `/api/v1/sales/{sale_id}/verify-ust-id` | Verify USt-IdNr. via BZSt API | Bearer |
### DATEV (`/api/v1/datev`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/api/v1/datev/export` | Create DATEV export (CSV) | Bearer |
| GET | `/api/v1/datev/exports` | List DATEV exports | Bearer |
| GET | `/api/v1/datev/exports/{export_id}/download` | Download DATEV CSV | Bearer |
### Copilot (`/api/v1/copilot`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/api/v1/copilot/chat` | Send message to AI Copilot | Bearer |
| POST | `/api/v1/copilot/action` | Execute confirmed action | Bearer |
| GET | `/api/v1/copilot/history` | Get chat history | Bearer |
| POST | `/api/v1/copilot/voice` | Voice input (base64 audio) | Bearer |
### Retouch (`/api/v1/retouch`)
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| POST | `/api/v1/retouch/process` | Upload image for retouching (async, 202) | Bearer |
| GET | `/api/v1/retouch/results/{result_id}` | Get retouch result | Bearer |
| POST | `/api/v1/retouch/price-compare` | Compare prices for vehicle | Bearer |
### Health
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/api/v1/health` | Health check | No |
| GET | `/` | Root info | No |
---
## 5. Frontend Build Verification
**Location:** `frontend/.next/`
**Build artifacts confirmed:**
- `BUILD_ID` — present (20 bytes)
- `build-manifest.json` — present
- `app-build-manifest.json` — present
- `routes-manifest.json` — present
- `server/` — directory present
- `static/` — directory present
- `prerender-manifest.json` — present
- `required-server-files.json` — present
- `trace/` — build trace present
**Verdict:** ✅ Frontend build artifacts are complete and present.
---
## 6. Environment Verification
### Required Environment Variables (from `.env.example`)
| Variable | Required | Status | Notes |
|----------|----------|--------|-------|
| `DATABASE_URL` | ✅ Yes | Set (local PostgreSQL) | `postgresql+asyncpg://erp_test_user:***@localhost:5432/erp_test` |
| `REDIS_URL` | ✅ Yes | Set in .env.example | Redis NOT installed; JWT refresh tokens are stateless (no Redis storage needed) |
| `JWT_SECRET` | ✅ Yes | Placeholder in .env.example | Must be set to a secure random string for production |
| `JWT_ALGORITHM` | Optional | Default HS256 | — |
| `JWT_ACCESS_TTL_MINUTES` | Optional | Default 15 | — |
| `JWT_REFRESH_TTL_DAYS` | Optional | Default 7 | — |
| `CORS_ORIGINS` | ✅ Yes | Set | `http://localhost:3000,http://localhost:3001` |
| `UPLOAD_DIR` | Optional | Default `/tmp/uploads` | Must be persistent volume in production |
| `APP_NAME` | Optional | Default `ERP Nutzfahrzeuge` | — |
| `APP_ENV` | Optional | Default `development` | Set to `production` for deployment |
| `MOBILE_DE_API_KEY` | Optional | Empty | Required for mobile.de integration |
| `MOBILE_DE_SELLER_ID` | Optional | Empty | Required for mobile.de integration |
| `OPENROUTER_API_KEY` | Optional | Not in .env.example | Required for AI Copilot features (currently empty/non-functional) |
### Infrastructure Status
| Service | Status | Notes |
|---------|--------|-------|
| PostgreSQL | ✅ Running | localhost:5432, accepting connections |
| Redis | ⚠️ Not installed | Not required for JWT; may be needed for async task queues (OCR, retouch, mobile.de push) |
| OpenRouter API | ⚠️ Not configured | AI Copilot features will not work without API key |
---
## 7. Errors Found
**No errors, tracebacks, warnings, or critical messages found in runtime logs.**
Log output was clean:
```
INFO: Started server process [3383733]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:48688 - "GET /api/v1/health HTTP/1.1" 200 OK
INFO: 127.0.0.1:48700 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:48706 - "GET /api/v1/auth/login HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:48710 - "GET /openapi.json HTTP/1.1" 200 OK
```
---
## 8. Known Risks
| # | Risk | Severity | Mitigation |
|---|------|----------|------------|
| 1 | **Redis not installed** — async task processing (OCR, retouch, mobile.de push) may fail or fall back to synchronous processing | Medium | Install Redis or verify async tasks have graceful fallback. Check if background tasks use in-memory queues. |
| 2 | **OPENROUTER_API_KEY not set** — AI Copilot (`/api/v1/copilot/*`) will not function | Medium | Set `OPENROUTER_API_KEY` in production environment for Copilot features. |
| 3 | **JWT_SECRET is placeholder**`change-me-to-a-secure-random-string` in .env.example | High | Generate a cryptographically secure secret (≥256 bits) for production. |
| 4 | **PostgreSQL is local only** — no connection pooling or HA configured | Medium | Configure PgBouncer or use managed PostgreSQL (e.g., Coolify managed DB) for production. |
| 5 | **UPLOAD_DIR = `/tmp/uploads`** — ephemeral storage, files lost on container restart | High | Mount a persistent volume for `UPLOAD_DIR` in production. |
| 6 | **CORS_ORIGINS** — currently allows localhost:3000,3001 only | Low | Add production frontend origin to `CORS_ORIGINS` for deployment. |
| 7 | **MOBILE_DE_API_KEY empty** — mobile.de push/status endpoints will fail | Low | Set API key when mobile.de integration is needed. |
| 8 | **No HTTPS/TLS** — backend runs on plain HTTP | Medium | Use reverse proxy (nginx/Traefik) or Coolify proxy for TLS termination. |
| 9 | **No rate limiting** — no rate limiting middleware detected | Medium | Add rate limiting (e.g., slowapi) before public exposure. |
---
## 9. Recommendations
### Immediate (before deployment)
1. Generate a secure `JWT_SECRET` and configure in production environment
2. Set `APP_ENV=production` for deployment
3. Configure persistent volume for `UPLOAD_DIR`
4. Add production frontend origin to `CORS_ORIGINS`
5. Install Redis if async task processing (OCR, retouch, mobile.de) is required
6. Set `OPENROUTER_API_KEY` if AI Copilot features are needed
### Deployment preparation
7. Create Dockerfile / docker-compose.yml for containerized deployment
8. Configure reverse proxy with TLS termination (Coolify proxy or nginx)
9. Set up PostgreSQL as managed service or with backup strategy
10. Add health check to Coolify resource configuration (`GET /api/v1/health`)
11. Configure log rotation and monitoring
### Post-deployment
12. Run smoke tests against production endpoints
13. Verify JWT auth flow end-to-end
14. Test file upload with persistent volume
15. Monitor for async task failures (OCR, retouch)
---
## 10. Sign-off
| Item | Status |
|------|--------|
| App starts without errors | ✅ Yes |
| Health endpoint returns 200 | ✅ Yes |
| Bounded error output shows no critical runtime failure | ✅ Yes (zero errors) |
| Frontend build artifacts present | ✅ Yes |
| Deployment docs ready | ✅ Yes (see `deploy/` directory) |
| Ready for deployment | ⚠️ Conditional — requires secure JWT_SECRET, persistent storage, and Redis decision |
**Runtime verification: PASSED**
**Deployment readiness: CONDITIONAL** (env vars + infrastructure must be configured)
+45
View File
@@ -0,0 +1,45 @@
# ERP Nutzfahrzeuge — Backend Dockerfile
# Python 3.13-slim + FastAPI + uvicorn
# System packages: WeasyPrint (libpango, libcairo), Pillow (libjpeg, libpng)
FROM python:3.13-slim AS base
# --- System packages for WeasyPrint and Pillow ---
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libcairo2 \
libgdk-pixbuf2.0-0 \
libjpeg62-turbo \
libpng16-16 \
libffi-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# --- Python venv ---
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# --- Install dependencies first (better layer caching) ---
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# --- Copy application code ---
COPY . .
# --- Create upload and contract directories ---
RUN mkdir -p /data/uploads /tmp/contracts
# --- Expose port ---
EXPOSE 8000
# --- Health check ---
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
CMD curl -f http://localhost:8000/api/v1/health || exit 1
# --- Start command ---
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
+173
View File
@@ -0,0 +1,173 @@
# Coolify Deployment Configuration — ERP Nutzfahrzeuge
---
## Overview
This document describes how to deploy the ERP Nutzfahrzeuge system on Coolify using Docker Compose.
The system consists of 4 services:
| Service | Image | Port | Health Check |
|---------|-------|------|-------------|
| Backend | Custom (Python 3.13-slim) | 8000 | `GET /api/v1/health` |
| Frontend | Custom (Node 18-alpine) | 3000 | `GET /` |
| PostgreSQL | `postgres:16-alpine` | 5432 | `pg_isready` |
| Redis | `redis:7-alpine` | 6379 | `redis-cli ping` |
---
## Coolify Resource Setup
### Step 1: Create Resource
1. In Coolify, create a new resource
2. Select **Docker Compose** as the resource type
3. Connect your Forgejo/Git repository
4. Set the base directory to the project root
5. Coolify will detect `docker-compose.yml` automatically
### Step 2: Configure Environment Variables
1. Navigate to resource → **Environment Variables**
2. Add all variables from `.env.example` (see `deploy/env.md` for descriptions)
3. **Critical variables:**
- `JWT_SECRET` — generate with `openssl rand -hex 32`
- `POSTGRES_PASSWORD` — strong password for database
- `CORS_ORIGINS` — production frontend URL (e.g., `https://erp.your-domain.com`)
- `NEXT_PUBLIC_API_URL` — production backend URL (e.g., `https://api.your-domain.com/api/v1`)
4. **Optional but feature-dependent:**
- `OPENROUTER_API_KEY` — required for AI features (OCR, retouch, copilot)
- `MOBILE_DE_API_KEY` + `MOBILE_DE_SELLER_ID` — required for mobile.de integration
5. Click **Save** and redeploy
### Step 3: Configure Health Check
In the resource settings:
| Setting | Value |
|---------|-------|
| Health check path | `/api/v1/health` |
| Health check port | `8000` |
| Health check interval | `30s` |
| Health check timeout | `5s` |
| Health check retries | `3` |
> **Note:** The health check targets the backend service. Docker Compose health checks are also defined per-service in `docker-compose.yml`.
### Step 4: Configure Persistent Storage
Docker Compose volumes are defined in `docker-compose.yml`:
| Volume | Mount Point | Purpose |
|--------|-------------|---------|
| `uploads` | `/data/uploads` (backend) | User file uploads (images, documents) |
| `pgdata` | `/var/lib/postgresql/data` (postgres) | Database data |
| `redisdata` | `/data` (redis) | Redis persistence (AOF) |
In Coolify:
1. Ensure persistent storage is enabled for the resource
2. Verify volume names match the compose file
3. For production, consider using a managed PostgreSQL instance instead of the containerized one
### Step 5: Configure Domains / Routing
1. **Backend API:**
- Set domain: `api.your-domain.com` (or subpath)
- Port: `8000`
- This is the primary entry point for Coolify health checks
2. **Frontend:**
- Set domain: `erp.your-domain.com` (or root domain)
- Port: `3000`
- Next.js standalone server
3. **CORS:**
- Set `CORS_ORIGINS` to the frontend domain
- Set `NEXT_PUBLIC_API_URL` to the backend domain
### Step 6: Deploy
1. Click **Deploy** in Coolify
2. Wait for all services to build and start
3. Verify health checks pass (green status for all services)
4. Test endpoints:
- `GET https://api.your-domain.com/api/v1/health` → 200 `{"status":"ok"}`
- `GET https://erp.your-domain.com/` → 200 (frontend loads)
- `GET https://api.your-domain.com/openapi.json` → 200 (API schema)
---
## Coolify Service Type
- **Type:** Docker Compose (multi-service)
- **Compose file:** `docker-compose.yml` (project root)
- **Build:** Coolify builds images from `backend/Dockerfile` and `frontend/Dockerfile`
- **Pre-built images:** `postgres:16-alpine`, `redis:7-alpine`
---
## Port Mapping
| Service | Internal Port | External Port | Notes |
|---------|---------------|---------------|-------|
| Backend | 8000 | 8000 | FastAPI + uvicorn (4 workers) |
| Frontend | 3000 | 3000 | Next.js standalone server |
| PostgreSQL | 5432 | — (internal only) | Not exposed externally |
| Redis | 6379 | — (internal only) | Not exposed externally |
> In Coolify, only expose backend (8000) and frontend (3000) via domains. PostgreSQL and Redis should remain internal to the Docker network.
---
## Build Configuration
### Backend
- **Base image:** `python:3.13-slim`
- **System packages:** libpango, libcairo, libjpeg, libpng (for WeasyPrint + Pillow)
- **Python venv:** `/opt/venv`
- **Entry:** `uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4`
### Frontend
- **Base image:** `node:18-alpine` (multi-stage)
- **Build:** `npm ci && npm run build` (standalone output)
- **Entry:** `node server.js`
- **Requires:** `output: 'standalone'` in `next.config.js`
---
## Post-Deploy Verification
1. All 4 containers are running and healthy
2. `GET /api/v1/health` → 200 `{"status":"ok"}`
3. Frontend loads at production URL
4. Login flow works: `POST /api/v1/auth/login`
5. CORS headers present for frontend origin
6. No errors in container logs
7. File uploads work (test upload to `/data/uploads`)
8. Database queries return expected data
---
## Troubleshooting
| Issue | Cause | Solution |
|-------|-------|----------|
| Backend health check fails | DB not ready | Check `postgres` container health; increase `start_period` |
| WeasyPrint errors | Missing system libs | Verify `libpango`, `libcairo` installed in backend image |
| Frontend build fails | Missing `output: 'standalone'` | Add `output: 'standalone'` to `next.config.js` |
| CORS errors | Wrong `CORS_ORIGINS` | Set to exact frontend domain (no trailing slash) |
| AI features fail | Missing `OPENROUTER_API_KEY` | Set API key in Coolify environment variables |
| mobile.de fails | Missing credentials | Set `MOBILE_DE_API_KEY` and `MOBILE_DE_SELLER_ID` |
---
## Rollback
See `deploy/rollback.md` for the full rollback procedure.
Quick rollback in Coolify:
1. Navigate to resource → **Deployments**
2. Find last known-good deployment
3. Click **Redeploy**
4. Verify health check passes
+56
View File
@@ -0,0 +1,56 @@
# Environment Configuration — ERP Nutzfahrzeuge
**Source:** `backend/.env.example` (safe reference — no live values)
---
## Required Variables
These variables MUST be set before the application can start.
| Variable | Description | Example (redacted) |
|----------|-------------|---------------------|
| `DATABASE_URL` | PostgreSQL async connection string | `postgresql+asyncpg://user:***@host:5432/dbname` |
| `JWT_SECRET` | Secret key for JWT signing (≥256 bits) | `<redacted — generate with openssl rand -hex 32>` |
## Important Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `REDIS_URL` | `redis://localhost:6379/0` | Redis connection for async task queues. Not required for JWT (stateless). Required for OCR/retouch/mobile.de async processing. |
| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm |
| `JWT_ACCESS_TTL_MINUTES` | `15` | Access token TTL in minutes |
| `JWT_REFRESH_TTL_DAYS` | `7` | Refresh token TTL in days |
| `CORS_ORIGINS` | `http://localhost:3000,http://localhost:3001` | Comma-separated allowed origins. Add production frontend URL. |
| `UPLOAD_DIR` | `/tmp/uploads` | File upload directory. **Must be a persistent volume in production.** |
| `APP_NAME` | `ERP Nutzfahrzeuge` | Application display name |
| `APP_ENV` | `development` | Set to `production` for deployment |
## Optional Integration Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MOBILE_DE_API_KEY` | (empty) | mobile.de API key for vehicle listing push |
| `MOBILE_DE_SELLER_ID` | (empty) | mobile.de seller ID |
| `OPENROUTER_API_KEY` | (not set) | OpenRouter API key for AI Copilot features. Without this, Copilot endpoints will fail. |
| `BZST_API_ENABLED` | `False` | Enable BZSt USt-IdNr. verification API |
## Production Checklist
- [ ] `DATABASE_URL` points to production PostgreSQL (managed or backed up)
- [ ] `JWT_SECRET` is a cryptographically secure random string (not placeholder)
- [ ] `APP_ENV=production`
- [ ] `CORS_ORIGINS` includes production frontend origin
- [ ] `UPLOAD_DIR` points to a persistent volume mount
- [ ] `REDIS_URL` set if async features (OCR, retouch, mobile.de) are needed
- [ ] `OPENROUTER_API_KEY` set if AI Copilot is needed
- [ ] `MOBILE_DE_API_KEY` and `MOBILE_DE_SELLER_ID` set if mobile.de integration is needed
## Coolify Configuration
In Coolify, set these as environment variables (or via `.env` file in the resource):
1. Navigate to resource → Environment Variables
2. Add each variable with its production value
3. Redeploy after changes
**Never commit real secret values to the repository.** Use Coolify's secret management or an external secrets manager.
+63
View File
@@ -0,0 +1,63 @@
# Health Check Configuration — ERP Nutzfahrzeuge
---
## Health Endpoint
**URL:** `GET /api/v1/health`
**Expected response:**
- HTTP Status: `200 OK`
- Body: `{"status":"ok"}`
**No authentication required.**
---
## Coolify Health Check
Configure in Coolify resource settings:
| Setting | Value |
|---------|-------|
| Health check path | `/api/v1/health` |
| Health check port | `8000` |
| Health check interval | `30s` (recommended) |
| Health check timeout | `5s` (recommended) |
| Health check retries | `3` (recommended) |
---
## Additional Monitoring Endpoints
| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| `/` | GET | No | Root info — returns app name, version, docs URL |
| `/openapi.json` | GET | No | OpenAPI schema — useful for API monitoring |
| `/docs` | GET | No | Swagger UI (disable in production via `APP_ENV=production` if needed) |
---
## Docker Compose Health Check
If using Docker Compose, add to the backend service:
```yaml
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
```
---
## Troubleshooting
| Symptom | Likely Cause | Action |
|---------|-------------|--------|
| Health check returns 502 | Backend not started or crashed | Check container logs, verify `DATABASE_URL` is reachable |
| Health check returns 503 | App startup in progress | Increase `start_period` in health check config |
| Health check times out | Network/firewall issue | Verify port mapping and firewall rules |
| Health check returns 500 | Internal error (DB connection, config) | Check application logs for traceback |
+137
View File
@@ -0,0 +1,137 @@
# Rollback Procedure — ERP Nutzfahrzeuge
---
## Rollback Strategy
The rollback strategy depends on the deployment method used.
---
## Coolify Rollback
### Option A: Redeploy Previous Image
1. In Coolify, navigate to the resource
2. Go to **Deployments** tab
3. Find the last known-good deployment
4. Click **Redeploy** on that deployment
5. Wait for health check to pass
6. Verify endpoints:
- `GET /api/v1/health` → 200
- `GET /` → 200
### Option B: Rollback via Git
1. In the repository, identify the last known-good commit:
```bash
git log --oneline -10
```
2. Revert to the previous commit:
```bash
git revert <commit_hash> # or git reset --hard <commit_hash>
git push origin main
```
3. Trigger a new deployment in Coolify (auto-deploy or manual)
4. Wait for health check to pass
---
## Docker Compose Rollback
### Step 1: Stop Current Deployment
```bash
docker-compose down
```
### Step 2: Revert Code
```bash
git checkout <last-known-good-tag-or-commit>
```
### Step 3: Rebuild and Restart
```bash
docker-compose build
docker-compose up -d
```
### Step 4: Verify
```bash
curl -s http://localhost:8000/api/v1/health
docker-compose logs --tail 50 backend
```
---
## Database Rollback
### If Alembic migrations are used:
```bash
cd backend
/opt/venv/bin/python -m alembic downgrade -1 # rollback one revision
# or
/opt/venv/bin/python -m alembic downgrade <revision_id> # rollback to specific revision
```
### If no migration tool:
- Database schema changes are applied on app startup via SQLAlchemy `create_all`
- Rollback requires manual SQL or restoring from a database backup
- **Always take a database backup before deployment:**
```bash
pg_dump -U erp_user erp_test > backup_$(date +%Y%m%d_%H%M%S).sql
```
### Restore from backup:
```bash
psql -U erp_user erp_test < backup_YYYYMMDD_HHMMSS.sql
```
---
## Rollback Decision Tree
| Situation | Action |
|-----------|--------|
| App won't start after deploy | Redeploy previous image (Coolify) or `git checkout` + rebuild (Docker) |
| Health check fails | Redeploy previous image; check DB connectivity |
| Database migration broke schema | `alembic downgrade` or restore from `pg_dump` backup |
| Frontend broken | Rebuild frontend from previous commit; redeploy |
| Performance regression | Redeploy previous image; investigate in staging |
| Security issue found | Rollback immediately; patch in staging; redeploy |
---
## Post-Rollback Verification
After any rollback:
1. ✅ Health check passes: `GET /api/v1/health` → 200 `{"status":"ok"}`
2. ✅ Root endpoint: `GET /` → 200
3. ✅ Login works: `POST /api/v1/auth/login` → 200 with valid credentials
4. ✅ No errors in logs: `docker logs <container> --tail 50`
5. ✅ Frontend loads at production URL
6. ✅ Database queries return expected data
---
## Emergency Rollback (Quick)
If the deployment is causing immediate issues:
```bash
# Coolify: use the Redeploy button on the last good deployment
# Docker Compose:
docker-compose down
git checkout <last-good-commit>
docker-compose build && docker-compose up -d
curl -s http://localhost:8000/api/v1/health
```
**Time to rollback:** < 5 minutes (if image is cached) or < 15 minutes (if rebuild needed).
+189
View File
@@ -0,0 +1,189 @@
# Deployment Runbook — ERP Nutzfahrzeuge
---
## Prerequisites
1. **PostgreSQL** — running and accessible (managed or self-hosted)
2. **Redis** (optional) — if async features (OCR, retouch, mobile.de push) are needed
3. **Node.js** — for frontend build (if building at deploy time)
4. **Python 3.11+** — for backend runtime
5. **Coolify** (or Docker) — for containerized deployment
---
## Pre-Deployment Steps
### 1. Verify Environment Variables
Ensure all required variables are configured in Coolify or `.env`:
```bash
# Required
DATABASE_URL=postgresql+asyncpg://<user>:<password>@<host>:5432/<dbname>
JWT_SECRET=<generate with: openssl rand -hex 32>
# Important
APP_ENV=production
CORS_ORIGINS=https://your-frontend-domain.com
UPLOAD_DIR=/app/uploads # persistent volume
# Optional (feature-dependent)
REDIS_URL=redis://<host>:6379/0
OPENROUTER_API_KEY=<key>
MOBILE_DE_API_KEY=<key>
MOBILE_DE_SELLER_ID=<id>
```
### 2. Build Frontend
```bash
cd frontend
npm ci
npm run build
```
Build artifacts are output to `frontend/.next/`.
### 3. Verify Backend Dependencies
```bash
cd backend
/opt/venv/bin/pip install -r requirements.txt
/opt/venv/bin/python -c "from app.main import app; print('Import OK')"
```
### 4. Database Migration (if applicable)
```bash
cd backend
/opt/venv/bin/python -m alembic upgrade head # if Alembic is configured
# Or ensure tables are created via app startup
```
---
## Deployment Steps (Coolify)
### Step 1: Create Resource
1. In Coolify, create a new resource (Docker Compose or Dockerfile-based)
2. Configure the source repository
3. Set the build/pack type
### Step 2: Configure Environment
1. Navigate to resource → Environment Variables
2. Add all variables from `deploy/env.md`
3. Ensure `JWT_SECRET` is a secure random value
4. Set `APP_ENV=production`
### Step 3: Configure Health Check
1. Set health check path to `/api/v1/health`
2. Set port to `8000`
3. See `deploy/healthcheck.md` for full configuration
### Step 4: Configure Persistent Storage
1. Mount a persistent volume for `UPLOAD_DIR` (e.g., `/app/uploads`)
2. Ensure database volume is persistent
### Step 5: Deploy
1. Click **Deploy** in Coolify
2. Wait for build and startup to complete
3. Verify health check passes (green status)
4. Test endpoints:
- `GET /api/v1/health` → 200 `{"status":"ok"}`
- `GET /` → 200 app metadata
- `GET /openapi.json` → 200 OpenAPI schema
### Step 6: Post-Deployment Verification
1. Verify frontend is accessible at the configured domain
2. Test login flow: `POST /api/v1/auth/login` with valid credentials
3. Verify CORS headers are present for frontend origin
4. Check logs for any errors:
```bash
docker logs <container_name> --tail 50
```
---
## Deployment Steps (Docker Compose)
### docker-compose.yml (template)
```yaml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/erp
- JWT_SECRET=${JWT_SECRET}
- APP_ENV=production
- CORS_ORIGINS=https://your-domain.com
- UPLOAD_DIR=/app/uploads
- REDIS_URL=redis://redis:6379/0
volumes:
- uploads:/app/uploads
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=erp_user
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=erp
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redisdata:/data
frontend:
build: ./frontend
ports:
- "3000:3000"
depends_on:
- backend
volumes:
uploads:
pgdata:
redisdata:
```
---
## Start Command (standalone)
```bash
cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
For production with workers:
```bash
cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
```
---
## Rollback
See `deploy/rollback.md` for rollback procedure.
+125
View File
@@ -0,0 +1,125 @@
# ERP Nutzfahrzeuge — Production Docker Compose
# Services: backend (FastAPI), frontend (Next.js), postgres, redis
# Compatible with Coolify Docker Compose deployment
services:
# ============ Backend (FastAPI + uvicorn) ============
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: erp-backend
restart: unless-stopped
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
- REDIS_URL=redis://redis:6379/0
- JWT_SECRET=${JWT_SECRET}
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
- JWT_ACCESS_TTL_MINUTES=${JWT_ACCESS_TTL_MINUTES:-15}
- JWT_REFRESH_TTL_DAYS=${JWT_REFRESH_TTL_DAYS:-7}
- CORS_ORIGINS=${CORS_ORIGINS}
- UPLOAD_DIR=/data/uploads
- WEASYPRINT_OUTPUT_DIR=/tmp/contracts
- APP_NAME=${APP_NAME:-ERP Nutzfahrzeuge}
- APP_ENV=production
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
- OPENROUTER_BASE_URL=${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}
- OPENROUTER_OCR_MODEL=${OPENROUTER_OCR_MODEL:-qwen/qwen2.5-vl-72b-instruct}
- MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-50}
- MOBILE_DE_API_KEY=${MOBILE_DE_API_KEY}
- MOBILE_DE_SELLER_ID=${MOBILE_DE_SELLER_ID}
- BZST_API_ENABLED=${BZST_API_ENABLED:-false}
volumes:
- uploads:/data/uploads
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
networks:
- erp-network
# ============ Frontend (Next.js standalone) ============
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000/api/v1}
container_name: erp-frontend
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000/api/v1}
- NODE_ENV=production
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- erp-network
# ============ PostgreSQL 16 ============
postgres:
image: postgres:16-alpine
container_name: erp-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- erp-network
# ============ Redis 7 ============
redis:
image: redis:7-alpine
container_name: erp-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
networks:
- erp-network
# ============ Volumes ============
volumes:
uploads:
driver: local
pgdata:
driver: local
redisdata:
driver: local
# ============ Networks ============
networks:
erp-network:
driver: bridge
+46
View File
@@ -0,0 +1,46 @@
# ERP Nutzfahrzeuge — Frontend Dockerfile
# Node 18 + Next.js 14.2.5 standalone build
# Multi-stage build: deps -> builder -> runner
# ============ Stage 1: Dependencies ============
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ============ Stage 2: Build ============
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build-time environment variables
ARG NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
# ============ Stage 3: Runner (production) ============
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy standalone build output
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
CMD ["node", "server.js"]
+1
View File
@@ -2,6 +2,7 @@ const { NextConfig } = require('next');
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
reactStrictMode: true,
env: {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',