diff --git a/deploy/.env.example b/deploy/.env.example deleted file mode 100644 index 504fd01..0000000 --- a/deploy/.env.example +++ /dev/null @@ -1,36 +0,0 @@ -# Production Environment Variables -# Copy to .env and fill in all values - -# Domain Configuration -ROOT_DOMAIN=example.com -ACME_EMAIL=admin@example.com - -# Database -POSTGRES_USER=headquarter -POSTGRES_PASSWORD=change-me-in-production -POSTGRES_DB=headquarter - -# Authentik OIDC -AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/ -AUTHENTIK_CLIENT_ID=headquarter-api -AUTHENTIK_CLIENT_SECRET=change-me-in-production - -# API Configuration -SECRET_ENCRYPTION_KEY=change-me-in-production -ACCESS_TOKEN_EXPIRE_MINUTES=60 - -# Deployment -TRAEFIK_NETWORK=traefik -TRAEFIK_LOG_LEVEL=INFO -TRAEFIK_ENTRYPOINT=websecure -TRAEFIK_CERT_RESOLVER=letsencrypt -TRAEFIK_DASHBOARD_SUBDOMAIN=traefik -API_TAG=latest -WEB_TAG=latest -REGISTRY=ghcr.io -REPO=headquarter - -# Frontend build-time variables -VITE_API_URL=https://api.example.com -VITE_OIDC_ISSUER=https://auth.example.com/application/o/headquarter/ -VITE_OIDC_CLIENT_ID=headquarter-web diff --git a/deploy/README.md b/deploy/README.md deleted file mode 100644 index eb5449b..0000000 --- a/deploy/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 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-stack.yml b/deploy/portainer-stack.yml deleted file mode 100644 index 488602d..0000000 --- a/deploy/portainer-stack.yml +++ /dev/null @@ -1,126 +0,0 @@ -version: "3.8" - -services: - traefik: - image: traefik:v3.1 - command: - - --api.dashboard=true - - --providers.docker=true - - --providers.docker.exposedbydefault=false - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 - - --entrypoints.web.http.redirections.entrypoint.to=websecure - - --entrypoints.web.http.redirections.entrypoint.scheme=https - - --certificatesresolvers.letsencrypt.acme.tlschallenge=true - - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL} - - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json - - --log.level=INFO - - --accesslog=true - - --accesslog.format=json - - --metrics.prometheus=true - ports: - - "80:80" - - "443:443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - traefik-certs:/letsencrypt - networks: - - platform - - tools - labels: - - traefik.enable=true - - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_DASHBOARD_SUBDOMAIN:-traefik}.${ROOT_DOMAIN}`) - - traefik.http.routers.traefik-dashboard.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} - - traefik.http.routers.traefik-dashboard.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - - traefik.http.routers.traefik-dashboard.service=api@internal - - traefik.http.routers.traefik-dashboard.middlewares=auth@file - restart: unless-stopped - healthcheck: - test: ["CMD", "traefik", "healthcheck"] - interval: 10s - timeout: 5s - retries: 3 - - api: - image: ${REGISTRY:-ghcr.io}/${REPO:-headquarter}/api:${API_TAG:-latest} - environment: - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - - ROOT_DOMAIN=${ROOT_DOMAIN} - - TOOL_DOMAIN=${TOOL_DOMAIN} - - API_URL=${API_URL} - - WEB_URL=${WEB_URL} - - CORS_ORIGINS=https://${ROOT_DOMAIN},https://*.${ROOT_DOMAIN} - - AUTHENTIK_ISSUER_URL=${AUTHENTIK_ISSUER_URL} - - AUTHENTIK_CLIENT_ID=${AUTHENTIK_CLIENT_ID} - - AUTHENTIK_CLIENT_SECRET=${AUTHENTIK_CLIENT_SECRET} - - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} - - ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES} - depends_on: - db: - condition: service_healthy - networks: - - platform - volumes: - - api-data:/data - labels: - - traefik.enable=true - - traefik.http.routers.api.rule=Host(`api.${ROOT_DOMAIN}`) - - traefik.http.routers.api.entrypoints=websecure - - traefik.http.routers.api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - - traefik.http.services.api.loadbalancer.server.port=8000 - - traefik.http.routers.api.middlewares=sec-headers@file - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""] - interval: 10s - timeout: 5s - retries: 3 - start_period: 5s - - web: - image: ${REGISTRY:-ghcr.io}/${REPO:-headquarter}/web:${WEB_TAG:-latest} - environment: - - VITE_API_URL=${VITE_API_URL} - - VITE_OIDC_ISSUER=${VITE_OIDC_ISSUER} - - VITE_OIDC_CLIENT_ID=${VITE_OIDC_CLIENT_ID} - depends_on: - - api - networks: - - platform - labels: - - traefik.enable=true - - traefik.http.routers.web.rule=Host(`${ROOT_DOMAIN}`) - - traefik.http.routers.web.entrypoints=websecure - - traefik.http.routers.web.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - - traefik.http.services.web.loadbalancer.server.port=8080 - - traefik.http.routers.web.middlewares=sec-headers@file - restart: unless-stopped - - db: - image: postgres:17-alpine - environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - POSTGRES_DB=${POSTGRES_DB} - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - platform - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] - interval: 5s - timeout: 5s - retries: 5 - restart: unless-stopped - -volumes: - postgres-data: - api-data: - traefik-certs: - -networks: - platform: - driver: bridge - tools: - driver: bridge - external: false diff --git a/deploy/portainer.env.example b/deploy/portainer.env.example deleted file mode 100644 index c0041f4..0000000 --- a/deploy/portainer.env.example +++ /dev/null @@ -1,26 +0,0 @@ -APP_NAME=Headquarter -ROOT_DOMAIN=example.com -TOOL_DOMAIN=tools.example.com -API_URL=https://api.example.com -WEB_URL=https://example.com - -POSTGRES_USER=headquarter -POSTGRES_DB=headquarter -POSTGRES_PASSWORD= - -AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/ -AUTHENTIK_CLIENT_ID= -AUTHENTIK_CLIENT_SECRET= - -TRAEFIK_NETWORK=traefik -TRAEFIK_ENTRYPOINT=websecure -TRAEFIK_CERT_RESOLVER=letsencrypt -TOOL_SUBDOMAIN_PATTERN={tool}-{project}-{user}.tools.{ROOT_DOMAIN} - -SECRET_ENCRYPTION_KEY= - -ACME_EMAIL=admin@example.com -API_TAG=latest -WEB_TAG=latest -REGISTRY=ghcr.io -REPO=headquarter diff --git a/deploy/traefik-labels.example.yml b/deploy/traefik-labels.example.yml deleted file mode 100644 index c311ceb..0000000 --- a/deploy/traefik-labels.example.yml +++ /dev/null @@ -1,27 +0,0 @@ -# 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.prod.yml b/docker-compose.prod.yml index 65a0d52..7ee2b8c 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,42 +1,4 @@ services: - traefik: - image: traefik:v3.1 - command: - - --api.dashboard=true - - --providers.docker=true - - --providers.docker.exposedbydefault=false - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 - - --certificatesresolvers.letsencrypt.acme.tlschallenge=true - - --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@example.com} - - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json - - --log.level=${TRAEFIK_LOG_LEVEL:-INFO} - - --accesslog=true - - --accesslog.format=json - - --metrics.prometheus=true - ports: - - "80:80" - - "443:443" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - traefik-certs:/letsencrypt - networks: - - platform - - tools - labels: - - traefik.enable=true - - traefik.http.routers.traefik-dashboard.rule=Host(`${TRAEFIK_DASHBOARD_SUBDOMAIN:-traefik}.${ROOT_DOMAIN:-localhost}`) - - traefik.http.routers.traefik-dashboard.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} - - traefik.http.routers.traefik-dashboard.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - - traefik.http.routers.traefik-dashboard.service=api@internal - - traefik.http.routers.traefik-dashboard.middlewares=auth@file - restart: unless-stopped - healthcheck: - test: ["CMD", "traefik", "healthcheck"] - interval: 10s - timeout: 5s - retries: 3 - api: build: context: ./apps/api @@ -58,12 +20,13 @@ services: condition: service_healthy networks: - platform + - traefik volumes: - api-data:/data labels: - traefik.enable=true - traefik.http.routers.api.rule=Host(`api.${ROOT_DOMAIN:-localhost}`) - - traefik.http.routers.api.entrypoints=websecure + - traefik.http.routers.api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} - traefik.http.routers.api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - traefik.http.services.api.loadbalancer.server.port=8000 - traefik.http.routers.api.middlewares=sec-headers@file @@ -87,10 +50,11 @@ services: - api networks: - platform + - traefik labels: - traefik.enable=true - traefik.http.routers.web.rule=Host(`${ROOT_DOMAIN:-localhost}`) - - traefik.http.routers.web.entrypoints=websecure + - traefik.http.routers.web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure} - traefik.http.routers.web.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} - traefik.http.services.web.loadbalancer.server.port=8080 - traefik.http.routers.web.middlewares=sec-headers@file @@ -116,11 +80,10 @@ services: volumes: postgres-data: api-data: - traefik-certs: networks: platform: driver: bridge - tools: - driver: bridge - external: false + traefik: + name: ${TRAEFIK_NETWORK:-traefik} + external: true diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml deleted file mode 100644 index 68e62f0..0000000 --- a/docker-compose.traefik.yml +++ /dev/null @@ -1,27 +0,0 @@ -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: - name: ${TRAEFIK_NETWORK:-traefik} - external: true