feat(T06): deployment config - Dockerfiles, docker-compose, Coolify guide, env example
This commit is contained in:
@@ -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)
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
@@ -2,6 +2,7 @@ const { NextConfig } = require('next');
|
|||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
env: {
|
env: {
|
||||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
|
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
|
||||||
|
|||||||
Reference in New Issue
Block a user