7.7 KiB
7.7 KiB
Compose & Infrastructure Conventions
This document outlines the standard conventions and best practices for Docker and Podman Compose files, volume layouts, networking, and smart image packaging across our infrastructure.
1. Storage & Host Volume Conventions
- Root Project Directory: All project state lives under
/volume1/docker/<stack-name>/(e.g.,/volume1/docker/auth-yes/,/volume1/docker/spire/). - Standard Folder Naming: Always use
datafor persistent application and database storage (neverdbor fragmented subfolders when a single root directory suffices). - Local Bind Mount Syntax: Always use the standard
driver_optslocal bind definition:volumes: <service>-data: driver: local driver_opts: type: none device: ${<STACK>_DATA_PATH} o: bind - PostgreSQL 18+ Volume Mount Standard:
- Starting in PostgreSQL 18+, official images expect the root volume mount at
/var/lib/postgresql(NOT/var/lib/postgresql/data). This allows PostgreSQL to create versioned cluster directories (/var/lib/postgresql/<version>/...) and executepg_upgrade --linkcleanly without crossing mount point boundaries:volumes: - auth-db-data:/var/lib/postgresql
- Starting in PostgreSQL 18+, official images expect the root volume mount at
- Environment File Naming (
stack.env):- Application stacks use
stack.envas the standard environment filename in Compose declarations (env_file: stack.env).
- Application stacks use
- Zero-Trust Inter-Process Sockets:
- Sockets shared across stacks (e.g., the SPIFFE Workload API
agent.sock) MUST use a named volume (spire-socket). - Application containers mount this volume read-only (
:ro), ensuring zero access to sensitive server CA keys, SQLite databases, or internal host state. - Client stacks declare
spire-socketasexternal: true.
- Sockets shared across stacks (e.g., the SPIFFE Workload API
2. Smart Container Images & Scripting Guidelines
- Pragmatic Scripting in Compose:
- Short one-line commands, flags, or glue checks in
command:orentrypoint:are fine. - Complex configuration generation, heredoc templating (
cat << 'EOF'), and heavy lifecycle bootstrapping MUST NOT be embedded in Compose files.
- Short one-line commands, flags, or glue checks in
- Smart Image Packaging & Repository Segregation:
- Self-seeding configuration and container lifecycle scripts belong in the container image.
- All smart image logic, Dockerfiles, entrypoint scripts (
entrypoint.sh), and configuration templates (templates/) must be cleanly segregated in dedicated directories in the codebase (e.g.,spire/).
- Persistence & User Modification:
- Smart image entrypoints must follow the self-seeding pattern: check if the target configuration exists on the host mount.
- If missing on first boot
\rightarrowatomically write the master template with guiding comments. - If already present
\rightarrowleave it untouched, preserving all user modifications across restarts and image updates.
3. Container Registries, Mirrors & Image Distribution
Our infrastructure uses a tiered registry architecture to clearly separate custom application images from upstream cached dependencies:
3.1 Registry Tier Classification
- Primary Custom Image Registry (
${REG}\rightarrowquay.atyg.org):- Hosts all internally built, project-specific custom application images
(
${REG}/library/<app>:<tag>). - Example:
${REG}/library/auth-yes-api:latest(built from project source). - Retained indefinitely on disk (auto-pruning disabled).
- Hosts all internally built, project-specific custom application images
(
- Docker Hub Pull-Through Cache (
acr.atyg.org):- Proxies and indefinitely caches upstream Docker Hub infrastructure images on the local network.
- Examples:
acr.atyg.org/library/postgres:18-alpine,acr.atyg.org/valkey/valkey:8-alpine,acr.atyg.org/library/alpine:3.20.
- GHCR Pull-Through Mirror (
${GHCR_REG}\rightarrowghcr.atyg.org):- Proxies and caches upstream GitHub Container Registry infrastructure images.
- Examples:
${GHCR_REG}/spiffe/spire-server:1.9.3,${GHCR_REG}/spiffe/spire-agent:1.9.3.
3.2 Universal Dockerfile Portability (Jules & External CI)
- Public Upstream Defaults: Dockerfiles for custom application images or
wrappers must declare public upstream registries by default so external agents
(like Jules) and cloud CI runners can build without private
.atyg.orgDNS:ARG BASE_IMAGE=alpine:3.20 FROM ${BASE_IMAGE} - Local Build Acceleration: Local builds and CLI scripts (
infra/setup.ts) can optionally pass--build-argto pull through local mirrors (ghcr.atyg.org,acr.atyg.org). - Push Destination: Custom built application images are tagged and pushed to
the local authority
${REG}/library/...(quay.atyg.org).
3.3 Compose Variable Cleanliness
- No Inline Defaults: Always write clean variable references in Compose
files (
image: ${REG}/library/auth-yes-api:latest,image: ${GHCR_REG}/spiffe/spire-server:1.9.3). - Centralized Values: Default registry variables belong strictly in
.envandinfra/setup.ts.
4. Networking & Traefik Routing
- Ingress Network:
- Web-facing containers connect to the pre-existing external network
traefik-net(external: true).
- Web-facing containers connect to the pre-existing external network
- Network Routing Ambiguity Guard:
- Whenever a service connects to more than one network (e.g., internal
bridge +
traefik-net), always specify:labels: - "traefik.docker.network=traefik-net"
- Whenever a service connects to more than one network (e.g., internal
bridge +
- Internal Service Mesh:
- Backends, databases, Valkey, and internal daemons communicate over a project
bridge network (e.g.,
auth-internal-net) without exposing ports directly to the host.
- Backends, databases, Valkey, and internal daemons communicate over a project
bridge network (e.g.,
- Standard Websecure Labels:
labels: - "traefik.enable=true" - "traefik.docker.network=traefik-net" - "traefik.http.routers.<app>.rule=Host(`${SYSTEM_DOMAIN}`)" - "traefik.http.routers.<app>.entrypoints=websecure" - "traefik.http.routers.<app>.tls=true" - "traefik.http.services.<app>.loadbalancer.server.port=<port>" - Traefik Dual-Router Pattern (Public Ingress Bypass vs. Authenticated
ForwardAuth):
- When an application exposes both public routes (e.g. webhooks, public
assets, CLI scripts,
/health) and protected user/admin routes, use priority-based dual routers:labels: - "traefik.enable=true" - "traefik.docker.network=traefik-net" # Router 1: Public Bypass (Priority 100 - Zero Auth Middleware) - "traefik.http.routers.<app>-public.rule=Host(`${SYSTEM_DOMAIN}`) && (PathPrefix(`/public`) || Path(`/health`))" - "traefik.http.routers.<app>-public.entrypoints=websecure" - "traefik.http.routers.<app>-public.tls=true" - "traefik.http.routers.<app>-public.priority=100" - "traefik.http.routers.<app>-public.service=<app>-svc" # Router 2: Authenticated Web / API (Priority 10 - Tier 2 ForwardAuth) - "traefik.http.routers.<app>-secure.rule=Host(`${SYSTEM_DOMAIN}`)" - "traefik.http.routers.<app>-secure.entrypoints=websecure" - "traefik.http.routers.<app>-secure.tls=true" - "traefik.http.routers.<app>-secure.priority=10" - "traefik.http.routers.<app>-secure.middlewares=auth-forward@docker" - "traefik.http.routers.<app>-secure.service=<app>-svc" # Service Definition - "traefik.http.services.<app>-svc.loadbalancer.server.port=<port>"
- When an application exposes both public routes (e.g. webhooks, public
assets, CLI scripts,
5. Stack Independence & Decoupling
- Standalone Infrastructure Stacks:
- General-purpose infrastructure components (e.g., SPIRE, Auth-Yes, Registry
Caches) are standalone stacks with their own dedicated project directories
(
/volume1/docker/spire,/volume1/docker/auth-yes). - Never nest one stack's storage or lifecycle under another stack's directory structure.
- General-purpose infrastructure components (e.g., SPIRE, Auth-Yes, Registry
Caches) are standalone stacks with their own dedicated project directories
(