feat(FN-002): complete Step 5 — Docker Compose and Portainer/Traefik Skeleton

This commit is contained in:
Fusion
2026-05-14 01:58:18 +02:00
parent 2e8a558b71
commit 7b45344237
8 changed files with 215 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.14-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY pyproject.toml ./
RUN pip install --no-cache-dir -e ".[dev]"
COPY app/ ./app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+15
View File
@@ -0,0 +1,15 @@
FROM node:25-alpine AS builder
WORKDIR /app
COPY package.json ./
RUN npm install -g pnpm && pnpm install
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
+19
View File
@@ -0,0 +1,19 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
+36
View File
@@ -0,0 +1,36 @@
# Deploy Skeleton
This directory contains deployment-focused examples and environment templates for the Headquarter platform.
## Files
| File | Purpose |
|------|---------|
| `portainer.env.example` | Environment variables for a Portainer-managed stack |
| `traefik-labels.example.yml` | Reference Traefik labels for services and tool instances |
## Assumptions
- An external **Traefik** reverse proxy is already running on the target Docker host.
- Traefik is attached to a Docker network (default name: `traefik`).
- The Traefik network is declared as `external: true` in compose overlays.
- TLS termination and certificate resolution are handled by Traefik.
## Usage
1. Copy `portainer.env.example` to your secrets manager or Portainer environment configuration.
2. Fill in all empty values (client IDs, secrets, database password, encryption key).
3. Deploy the stack via Portainer using `docker-compose.yml` + `docker-compose.traefik.yml`.
4. Tool subdomain labels will be generated dynamically in FN-006.
## Traefik Network
Create the Traefik network if it does not exist:
```bash
docker network create traefik
```
## Follow-up
Detailed deployment automation, dynamic labels for spawned tool containers, and CI/CD integration are planned in **FN-006**.
+27
View File
@@ -0,0 +1,27 @@
# Portainer stack environment variables (no secrets committed)
# Copy and configure in Portainer UI or your secrets manager.
APP_NAME=Headquarter
ROOT_DOMAIN=example.com
TOOL_DOMAIN=tools.example.com
API_URL=https://api.example.com
WEB_URL=https://example.com
# Database
POSTGRES_USER=headquarter
POSTGRES_DB=headquarter
POSTGRES_PASSWORD=
# Authentik OIDC
AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=
# Traefik
TRAEFIK_NETWORK=traefik
TRAEFIK_ENTRYPOINT=websecure
TRAEFIK_CERT_RESOLVER=letsencrypt
TOOL_SUBDOMAIN_PATTERN={tool}-{project}-{user}.tools.{ROOT_DOMAIN}
# Secrets
SECRET_ENCRYPTION_KEY=
+27
View File
@@ -0,0 +1,27 @@
# Example Traefik labels for Headquarter services
# These labels are applied automatically by docker-compose.traefik.yml.
# Use this file as a reference when adding custom tool container labels later.
# API service labels
api_labels: &api
- "traefik.enable=true"
- "traefik.http.routers.headquarter-api.rule=Host(`api.example.com`)"
- "traefik.http.routers.headquarter-api.entrypoints=websecure"
- "traefik.http.routers.headquarter-api.tls.certresolver=letsencrypt"
- "traefik.http.services.headquarter-api.loadbalancer.server.port=8000"
# Web service labels
web_labels: &web
- "traefik.enable=true"
- "traefik.http.routers.headquarter-web.rule=Host(`example.com`)"
- "traefik.http.routers.headquarter-web.entrypoints=websecure"
- "traefik.http.routers.headquarter-web.tls.certresolver=letsencrypt"
- "traefik.http.services.headquarter-web.loadbalancer.server.port=80"
# Tool instance labels (template for dynamically spawned containers)
tool_labels: &tool
- "traefik.enable=true"
- "traefik.http.routers.{tool_name}.rule=Host(`{subdomain}`)"
- "traefik.http.routers.{tool_name}.entrypoints=websecure"
- "traefik.http.routers.{tool_name}.tls.certresolver=letsencrypt"
- "traefik.http.services.{tool_name}.loadbalancer.server.port={port}"
+26
View File
@@ -0,0 +1,26 @@
services:
api:
networks:
- default
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.headquarter-api.rule=Host(`api.${ROOT_DOMAIN}`)"
- "traefik.http.routers.headquarter-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
- "traefik.http.routers.headquarter-api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}"
- "traefik.http.services.headquarter-api.loadbalancer.server.port=8000"
web:
networks:
- default
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.headquarter-web.rule=Host(`${ROOT_DOMAIN}`)"
- "traefik.http.routers.headquarter-web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
- "traefik.http.routers.headquarter-web.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}"
- "traefik.http.services.headquarter-web.loadbalancer.server.port=80"
networks:
traefik:
external: true
+50
View File
@@ -0,0 +1,50 @@
services:
api:
build:
context: ./apps/api
dockerfile: Dockerfile
ports:
- "8000:8000"
env_file:
- .env
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-headquarter}
depends_on:
db:
condition: service_healthy
volumes:
- api-data:/data
restart: unless-stopped
web:
build:
context: ./apps/web
dockerfile: Dockerfile
ports:
- "5173:80"
env_file:
- .env
depends_on:
- api
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- POSTGRES_DB=${POSTGRES_DB:-headquarter}
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-headquarter}"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres-data:
api-data: