auth-yes/COMPOSE_CONVENTIONS.md

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 data for persistent application and database storage (never db or fragmented subfolders when a single root directory suffices).
  • Local Bind Mount Syntax: Always use the standard driver_opts local 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 execute pg_upgrade --link cleanly without crossing mount point boundaries:
      volumes:
        - auth-db-data:/var/lib/postgresql
      
  • Environment File Naming (stack.env):
    • Application stacks use stack.env as the standard environment filename in Compose declarations (env_file: stack.env).
  • 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-socket as external: true.

2. Smart Container Images & Scripting Guidelines

  • Pragmatic Scripting in Compose:
    • Short one-line commands, flags, or glue checks in command: or entrypoint: are fine.
    • Complex configuration generation, heredoc templating (cat << 'EOF'), and heavy lifecycle bootstrapping MUST NOT be embedded in Compose files.
  • 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 \rightarrow atomically write the master template with guiding comments.
    • If already present \rightarrow leave 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

  1. Primary Custom Image Registry (${REG} \rightarrow quay.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).
  2. 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.
  3. GHCR Pull-Through Mirror (${GHCR_REG} \rightarrow ghcr.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.org DNS:
    ARG BASE_IMAGE=alpine:3.20
    FROM ${BASE_IMAGE}
    
  • Local Build Acceleration: Local builds and CLI scripts (infra/setup.ts) can optionally pass --build-arg to 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 .env and infra/setup.ts.

4. Networking & Traefik Routing

  • Ingress Network:
    • Web-facing containers connect to the pre-existing external network traefik-net (external: true).
  • 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"
      
  • 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.
  • 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>"
      

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.