Restructure docker into layered compose files, add a Makefile
Move compose.yml/compose.override.yml/compose.traefik.yml into docker/ and
split into the explicit prod -> dev -> local layering; compose.override.yml
had to be renamed because docker compose auto-loads that filename, which
defeats the layering.
- external network traefik-public -> proxy (shared with the website stack)
- frontend host dashboard.${DOMAIN} -> app.${DOMAIN}
- stable container_names, security_opt no-new-privileges on prod services
- adminer bound to 127.0.0.1 in dev instead of all interfaces
- .env.example replaces the committed .env
- pre-commit biome hook ran npm in a bun repo
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1916f7f778
commit
529b5f9ed5
@@ -21,7 +21,7 @@ repos:
|
||||
hooks:
|
||||
- id: local-biome-check
|
||||
name: biome check
|
||||
entry: npm run lint
|
||||
entry: bun run lint
|
||||
language: system
|
||||
types: [text]
|
||||
files: ^frontend/
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# ─── Fluksio app Makefile ───
|
||||
# Convenience targets for development, testing, linting, and deployment.
|
||||
# The workspace root delegates to these (see ../Makefile).
|
||||
|
||||
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \
|
||||
generate-client test test-backend test-frontend lint lint-backend \
|
||||
lint-frontend clean help
|
||||
|
||||
COMPOSE_ROOT := $(CURDIR)
|
||||
# Explicit project name keeps this stack isolated from the sibling website
|
||||
# stack (otherwise both default to "docker", the directory their compose
|
||||
# files live in).
|
||||
COMPOSE_PROJECT := fluksio-app
|
||||
# Compose interpolation needs the project-local .env before reading compose.yml.
|
||||
COMPOSE := docker compose -p $(COMPOSE_PROJECT) --env-file $(COMPOSE_ROOT)/.env
|
||||
COMPOSE_PROD := $(COMPOSE) -f docker/compose.yml
|
||||
COMPOSE_DEV := $(COMPOSE_PROD) -f docker/compose.dev.yml
|
||||
# Integrated local stack: dev stack wired onto the shared `proxy` network.
|
||||
COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml
|
||||
|
||||
help: ## Show available targets
|
||||
@awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
# ── Development (Docker) ──────────────────────────────────────────
|
||||
|
||||
dev-utils: ## Start only the utility containers (db, adminer, proxy, mailcatcher, prestart)
|
||||
$(COMPOSE_DEV) up --build db adminer proxy mailcatcher prestart
|
||||
|
||||
dev: ## Start the full dev stack (includes a local Traefik proxy)
|
||||
$(COMPOSE_DEV) up --build
|
||||
|
||||
dev-local: ## Start the integrated local stack (called by the root `make dev`)
|
||||
DOMAIN=$${DOMAIN:-localhost} ENVIRONMENT=$${ENVIRONMENT:-local} \
|
||||
$(COMPOSE_LOCAL) up --build -d proxy db adminer prestart backend frontend mailcatcher
|
||||
|
||||
up: ## Start the production stack
|
||||
$(COMPOSE_PROD) up --build -d
|
||||
|
||||
update: ## Pull, rebuild using the layer cache, and recreate changed containers
|
||||
git pull
|
||||
$(COMPOSE_PROD) build
|
||||
$(COMPOSE_PROD) up -d --remove-orphans
|
||||
docker image prune -f
|
||||
|
||||
down: ## Stop all running containers
|
||||
-$(COMPOSE_LOCAL) down
|
||||
-$(COMPOSE_PROD) down
|
||||
|
||||
# ── Development (local, no Docker) ───────────────────────────────
|
||||
# Run `make dev-backend` and `make dev-frontend` in two separate terminals.
|
||||
|
||||
install: ## Install all dependencies (backend + frontend)
|
||||
cd backend && uv sync
|
||||
cd frontend && bun install
|
||||
|
||||
dev-backend: ## Start the FastAPI backend with hot-reload (local)
|
||||
cd backend && uv run fastapi dev app/main.py
|
||||
|
||||
dev-frontend: ## Start the Vite dev server (local)
|
||||
cd frontend && bun dev
|
||||
|
||||
generate-client: ## Regenerate the frontend SDK from the backend's OpenAPI schema
|
||||
bash scripts/generate-client.sh
|
||||
|
||||
# ── Testing ───────────────────────────────────────────────────────
|
||||
|
||||
test: test-backend test-frontend ## Run all tests (backend + frontend)
|
||||
|
||||
test-backend: ## Run backend tests (pytest + coverage)
|
||||
cd backend && uv run bash scripts/tests-start.sh
|
||||
|
||||
test-frontend: ## Run frontend tests (Playwright e2e)
|
||||
cd frontend && bunx playwright test
|
||||
|
||||
# ── Linting ───────────────────────────────────────────────────────
|
||||
|
||||
lint: lint-backend lint-frontend ## Run all linters
|
||||
|
||||
lint-backend: ## Lint backend with ruff + mypy
|
||||
cd backend && uv run ruff check .
|
||||
cd backend && uv run ruff format --check .
|
||||
cd backend && uv run mypy app
|
||||
|
||||
lint-frontend: ## Lint frontend with biome
|
||||
cd frontend && bun run lint
|
||||
|
||||
# ── Cleanup ───────────────────────────────────────────────────────
|
||||
|
||||
clean: ## Remove build artifacts and caches
|
||||
rm -rf frontend/dist frontend/blob-report frontend/test-results
|
||||
rm -rf backend/.pytest_cache backend/htmlcov
|
||||
find backend -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||
@@ -1,10 +1,12 @@
|
||||
# Local development overrides: a self-contained stack with its own Traefik,
|
||||
# published host ports, hot reload and the mail/browser test helpers.
|
||||
# Never used in production — the Makefile only layers it for `make dev*`.
|
||||
|
||||
services:
|
||||
|
||||
# Local services are available on their ports, but also available on:
|
||||
# http://api.localhost.tiangolo.com: backend
|
||||
# http://dashboard.localhost.tiangolo.com: frontend
|
||||
# etc. To enable it, update .env, set:
|
||||
# DOMAIN=localhost.tiangolo.com
|
||||
# Local Traefik. Services stay reachable both on their published ports and
|
||||
# on http://api.localhost / http://app.localhost via the routers in
|
||||
# compose.yml. Dashboard at http://localhost:8090.
|
||||
proxy:
|
||||
image: traefik:3.6
|
||||
volumes:
|
||||
@@ -12,37 +14,25 @@ services:
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8090:8080"
|
||||
# Duplicate the command from compose.yml to add --api.insecure=true
|
||||
command:
|
||||
# Enable Docker in Traefik, so that it reads labels from Docker services
|
||||
- --providers.docker
|
||||
# Add a constraint to only use services with the label for this stack
|
||||
- --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
|
||||
# Do not expose all Docker services, only the ones explicitly exposed
|
||||
- --providers.docker.constraints=Label(`traefik.constraint-label`, `proxy`)
|
||||
- --providers.docker.exposedbydefault=false
|
||||
# Create an entrypoint "http" listening on port 80
|
||||
- --entrypoints.http.address=:80
|
||||
# Create an entrypoint "https" listening on port 443
|
||||
- --entrypoints.https.address=:443
|
||||
# Enable the access log, with HTTP requests
|
||||
- --accesslog
|
||||
# Enable the Traefik log, for configurations and errors
|
||||
- --log
|
||||
# Enable debug logging for local development
|
||||
- --log.level=DEBUG
|
||||
# Enable the Dashboard and API
|
||||
- --api
|
||||
# Enable the Dashboard and API in insecure mode for local development
|
||||
- --api.insecure=true
|
||||
labels:
|
||||
# Enable Traefik for this service, to make it available in the public network
|
||||
- traefik.enable=true
|
||||
- traefik.constraint-label=traefik-public
|
||||
# Dummy https-redirect middleware that doesn't really redirect, only to
|
||||
# allow running it locally
|
||||
- traefik.constraint-label=proxy
|
||||
# Dummy https-redirect middleware that doesn't really redirect, so the
|
||||
# production routers' middleware reference still resolves locally.
|
||||
- traefik.http.middlewares.https-redirect.contenttype.autodetect=false
|
||||
networks:
|
||||
- traefik-public
|
||||
- proxy
|
||||
- default
|
||||
|
||||
db:
|
||||
@@ -53,16 +43,15 @@ services:
|
||||
adminer:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "127.0.0.1:8080:8080"
|
||||
|
||||
backend:
|
||||
restart: "no"
|
||||
ports:
|
||||
- "8000:8000"
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: backend/Dockerfile
|
||||
# command: sleep infinity # Infinite loop to keep container alive doing nothing
|
||||
command:
|
||||
- fastapi
|
||||
- run
|
||||
@@ -70,22 +59,21 @@ services:
|
||||
- "app/main.py"
|
||||
develop:
|
||||
watch:
|
||||
- path: ./backend
|
||||
- path: ../backend
|
||||
action: sync
|
||||
target: /app/backend
|
||||
ignore:
|
||||
- ./backend/.venv
|
||||
- ../backend/.venv
|
||||
- .venv
|
||||
- path: ./backend/pyproject.toml
|
||||
- path: ../backend/pyproject.toml
|
||||
action: rebuild
|
||||
# TODO: remove once coverage is done locally
|
||||
volumes:
|
||||
- ./backend/htmlcov:/app/backend/htmlcov
|
||||
- ../backend/htmlcov:/app/backend/htmlcov
|
||||
environment:
|
||||
SMTP_HOST: "mailcatcher"
|
||||
SMTP_PORT: "1025"
|
||||
SMTP_TLS: "false"
|
||||
EMAILS_FROM_EMAIL: "noreply@example.com"
|
||||
EMAILS_FROM_EMAIL: "noreply@fluksio.com"
|
||||
|
||||
mailcatcher:
|
||||
image: schickling/mailcatcher
|
||||
@@ -98,7 +86,7 @@ services:
|
||||
ports:
|
||||
- "5173:80"
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: frontend/Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=http://localhost:8000
|
||||
@@ -106,7 +94,7 @@ services:
|
||||
|
||||
playwright:
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: frontend/Dockerfile.playwright
|
||||
args:
|
||||
- VITE_API_URL=http://backend:8000
|
||||
@@ -116,20 +104,20 @@ services:
|
||||
- backend
|
||||
- mailcatcher
|
||||
env_file:
|
||||
- .env
|
||||
- ../.env
|
||||
environment:
|
||||
- VITE_API_URL=http://backend:8000
|
||||
- MAILCATCHER_HOST=http://mailcatcher:1080
|
||||
# For the reports when run locally
|
||||
- PLAYWRIGHT_HTML_HOST=0.0.0.0
|
||||
- CI=${CI}
|
||||
volumes:
|
||||
- ./frontend/blob-report:/app/frontend/blob-report
|
||||
- ./frontend/test-results:/app/frontend/test-results
|
||||
- ../frontend/blob-report:/app/frontend/blob-report
|
||||
- ../frontend/test-results:/app/frontend/test-results
|
||||
ports:
|
||||
- 9323:9323
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
# For local dev, don't expect an external Traefik network
|
||||
# Self-contained dev: create a stack-local network instead of expecting the
|
||||
# shared one. compose.local.yml flips this back for the integrated stack.
|
||||
proxy:
|
||||
external: false
|
||||
@@ -0,0 +1,33 @@
|
||||
# Integrated local stack — layered on top of compose.yml + compose.dev.yml by
|
||||
# the root `make dev`. Reattaches this stack's Traefik to the shared external
|
||||
# `proxy` network so it also routes the website stack's containers, and serves
|
||||
# everything over plain http on *.${DOMAIN} (default *.localhost).
|
||||
|
||||
services:
|
||||
|
||||
proxy:
|
||||
networks:
|
||||
- proxy
|
||||
- default
|
||||
|
||||
backend:
|
||||
environment:
|
||||
- ENVIRONMENT=local
|
||||
- FRONTEND_HOST=http://app.${DOMAIN:-localhost}
|
||||
- BACKEND_CORS_ORIGINS=http://${DOMAIN:-localhost},http://app.${DOMAIN:-localhost}
|
||||
|
||||
prestart:
|
||||
environment:
|
||||
- ENVIRONMENT=local
|
||||
- FRONTEND_HOST=http://app.${DOMAIN:-localhost}
|
||||
|
||||
frontend:
|
||||
build:
|
||||
args:
|
||||
- VITE_API_URL=http://api.${DOMAIN:-localhost}
|
||||
- NODE_ENV=development
|
||||
|
||||
networks:
|
||||
# Shared with the website stack; created by the root scripts/setup.sh.
|
||||
proxy:
|
||||
external: true
|
||||
@@ -10,8 +10,8 @@ services:
|
||||
labels:
|
||||
# Enable Traefik for this service, to make it available in the public network
|
||||
- traefik.enable=true
|
||||
# Use the traefik-public network (declared below)
|
||||
- traefik.docker.network=traefik-public
|
||||
# Use the proxy network (declared below)
|
||||
- traefik.docker.network=proxy
|
||||
# Define the port inside of the Docker service to use
|
||||
- traefik.http.services.traefik-dashboard.loadbalancer.server.port=8080
|
||||
# Make Traefik use this domain (from an environment variable) in HTTP
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
# Add Docker as a mounted volume, so that Traefik can read the labels of other services
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# Mount the volume to store the certificates
|
||||
- traefik-public-certificates:/certificates
|
||||
- traefik-certificates:/certificates
|
||||
command:
|
||||
# Enable Docker in Traefik, so that it reads labels from Docker services
|
||||
- --providers.docker
|
||||
@@ -64,14 +64,14 @@ services:
|
||||
networks:
|
||||
# Use the public network created to be shared between Traefik and
|
||||
# any other service that needs to be publicly available with HTTPS
|
||||
- traefik-public
|
||||
- proxy
|
||||
|
||||
volumes:
|
||||
# Create a volume to store the certificates, even if the container is recreated
|
||||
traefik-public-certificates:
|
||||
traefik-certificates:
|
||||
|
||||
networks:
|
||||
# Use the previously created public network "traefik-public", shared with other
|
||||
# Use the previously created public network "proxy", shared with other
|
||||
# services that need to be publicly available via this Traefik
|
||||
traefik-public:
|
||||
proxy:
|
||||
external: true
|
||||
@@ -1,8 +1,18 @@
|
||||
# Production stack. Layered by the Makefile:
|
||||
# compose.yml → production (Traefik + TLS, restart always)
|
||||
# + compose.dev.yml → local dev (published ports, hot reload)
|
||||
# + compose.local.yml → integrated stack on the shared `proxy` net
|
||||
# Paths are relative to this directory, which compose uses as the project
|
||||
# directory (build contexts therefore point at `..`, the repo root).
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres:18
|
||||
container_name: fluksio-db
|
||||
restart: always
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
@@ -12,7 +22,7 @@ services:
|
||||
volumes:
|
||||
- app-db-data:/var/lib/postgresql/data/pgdata
|
||||
env_file:
|
||||
- .env
|
||||
- ../.env
|
||||
environment:
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||
@@ -21,34 +31,29 @@ services:
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
container_name: fluksio-adminer
|
||||
restart: always
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# Deliberately not on `proxy`: the Postgres UI is reachable from the host
|
||||
# in dev only (see compose.dev.yml), never routed from the internet.
|
||||
networks:
|
||||
- traefik-public
|
||||
- default
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- ADMINER_DESIGN=pepa-linha-dark
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-public
|
||||
- traefik.constraint-label=traefik-public
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.rule=Host(`adminer.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.entrypoints=http
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-http.middlewares=https-redirect
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.rule=Host(`adminer.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.entrypoints=https
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls=true
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-adminer-https.tls.certresolver=le
|
||||
- traefik.http.services.${STACK_NAME?Variable not set}-adminer.loadbalancer.server.port=8080
|
||||
|
||||
prestart:
|
||||
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
|
||||
container_name: fluksio-prestart
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: backend/Dockerfile
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- traefik-public
|
||||
- proxy
|
||||
- default
|
||||
depends_on:
|
||||
db:
|
||||
@@ -56,7 +61,7 @@ services:
|
||||
restart: true
|
||||
command: bash scripts/prestart.sh
|
||||
env_file:
|
||||
- .env
|
||||
- ../.env
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN}
|
||||
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
|
||||
@@ -78,9 +83,12 @@ services:
|
||||
|
||||
backend:
|
||||
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
|
||||
container_name: fluksio-api
|
||||
restart: always
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- traefik-public
|
||||
- proxy
|
||||
- default
|
||||
depends_on:
|
||||
db:
|
||||
@@ -89,7 +97,7 @@ services:
|
||||
prestart:
|
||||
condition: service_completed_successfully
|
||||
env_file:
|
||||
- .env
|
||||
- ../.env
|
||||
environment:
|
||||
- DOMAIN=${DOMAIN}
|
||||
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
|
||||
@@ -116,12 +124,12 @@ services:
|
||||
retries: 5
|
||||
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: backend/Dockerfile
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-public
|
||||
- traefik.constraint-label=traefik-public
|
||||
- traefik.docker.network=proxy
|
||||
- traefik.constraint-label=proxy
|
||||
|
||||
- traefik.http.services.${STACK_NAME?Variable not set}-backend.loadbalancer.server.port=8000
|
||||
|
||||
@@ -133,42 +141,45 @@ services:
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls=true
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-https.tls.certresolver=le
|
||||
|
||||
# Enable redirection for HTTP and HTTPS
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-backend-http.middlewares=https-redirect
|
||||
|
||||
frontend:
|
||||
image: '${DOCKER_IMAGE_FRONTEND?Variable not set}:${TAG-latest}'
|
||||
container_name: fluksio-app
|
||||
restart: always
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- traefik-public
|
||||
- proxy
|
||||
- default
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: frontend/Dockerfile
|
||||
args:
|
||||
- VITE_API_URL=https://api.${DOMAIN?Variable not set}
|
||||
- NODE_ENV=production
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=traefik-public
|
||||
- traefik.constraint-label=traefik-public
|
||||
- traefik.docker.network=proxy
|
||||
- traefik.constraint-label=proxy
|
||||
|
||||
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
|
||||
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`dashboard.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`app.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.entrypoints=http
|
||||
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`dashboard.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.rule=Host(`app.${DOMAIN?Variable not set}`)
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.entrypoints=https
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls=true
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-https.tls.certresolver=le
|
||||
|
||||
# Enable redirection for HTTP and HTTPS
|
||||
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.middlewares=https-redirect
|
||||
|
||||
volumes:
|
||||
app-db-data:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
# Allow setting it to false for testing
|
||||
# Shared with the website stack and whatever reverse proxy fronts them.
|
||||
# Created once by the workspace root's scripts/setup.sh.
|
||||
proxy:
|
||||
external: true
|
||||
Reference in New Issue
Block a user