From 7b45344237052a84b4c7177cddf9dd48a6bcf4e3 Mon Sep 17 00:00:00 2001 From: Fusion Date: Thu, 14 May 2026 01:58:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(FN-002):=20complete=20Step=205=20=E2=80=94?= =?UTF-8?q?=20Docker=20Compose=20and=20Portainer/Traefik=20Skeleton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/Dockerfile | 15 ++++++++++ apps/web/Dockerfile | 15 ++++++++++ apps/web/nginx.conf | 19 ++++++++++++ deploy/README.md | 36 ++++++++++++++++++++++ deploy/portainer.env.example | 27 +++++++++++++++++ deploy/traefik-labels.example.yml | 27 +++++++++++++++++ docker-compose.traefik.yml | 26 ++++++++++++++++ docker-compose.yml | 50 +++++++++++++++++++++++++++++++ 8 files changed, 215 insertions(+) create mode 100644 apps/api/Dockerfile create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/nginx.conf create mode 100644 deploy/README.md create mode 100644 deploy/portainer.env.example create mode 100644 deploy/traefik-labels.example.yml create mode 100644 docker-compose.traefik.yml create mode 100644 docker-compose.yml diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..0793f1d --- /dev/null +++ b/apps/api/Dockerfile @@ -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"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..9719b64 --- /dev/null +++ b/apps/web/Dockerfile @@ -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 diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..7e7c1b8 --- /dev/null +++ b/apps/web/nginx.conf @@ -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; + } +} diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..eb5449b --- /dev/null +++ b/deploy/README.md @@ -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**. diff --git a/deploy/portainer.env.example b/deploy/portainer.env.example new file mode 100644 index 0000000..02072fd --- /dev/null +++ b/deploy/portainer.env.example @@ -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= diff --git a/deploy/traefik-labels.example.yml b/deploy/traefik-labels.example.yml new file mode 100644 index 0000000..c311ceb --- /dev/null +++ b/deploy/traefik-labels.example.yml @@ -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}" diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml new file mode 100644 index 0000000..ce22cf0 --- /dev/null +++ b/docker-compose.traefik.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7101348 --- /dev/null +++ b/docker-compose.yml @@ -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: