# Stack Validation Patterns This document outlines the architecture patterns and validation criteria for GitLab stack projects. ## Table of Contents 1. [Directory Structure Patterns](#directory-structure-patterns) 2. [Environment Variable Patterns](#environment-variable-patterns) 3. [Secrets Management Patterns](#secrets-management-patterns) 4. [Docker Compose Patterns](#docker-compose-patterns) 5. [Configuration Patterns](#configuration-patterns) 6. [File Ownership Patterns](#file-ownership-patterns) --- ## Directory Structure Patterns ### Standard Stack Directory Layout ``` my-stack/ ├── docker-compose.yml # Main compose file (NO version field) ├── .env # Environment variables (NOT in git) ├── .env.example # Environment template (IN git) ├── .gitignore # Must exclude secrets, .env, _temporary ├── .stack-validator.yml # Optional: Custom validation rules ├── config/ # Configuration files │ ├── nginx/ │ │ └── nginx.conf │ ├── app/ │ │ └── settings.yml │ └── db/ │ └── init.sql ├── secrets/ # Secret files (NOT in git) │ ├── db_password │ ├── api_key │ └── jwt_secret └── _temporary/ # Transient files (NOT in git) └── (cleaned after use) ``` ### Required Directories | Directory | Purpose | Git Status | Permissions | |-----------|---------|------------|-------------| | `./config` | Configuration files | Tracked | 755 | | `./secrets` | Secret files | **NOT tracked** | 700 | | `./_temporary` | Temporary/cache files | **NOT tracked** | 755 | ### .gitignore Requirements **MUST contain:** ```gitignore # Secrets - never commit /secrets/ /secrets/* # Environment variables - never commit .env # Temporary files /_temporary/ /_temporary/* # Common exclusions *.log .DS_Store ``` --- ## Environment Variable Patterns ### .env File Structure **Purpose**: Define environment-specific variables for stack deployment **Example .env:** ```bash # Application APP_NAME=my-application APP_ENV=production APP_DEBUG=false APP_URL=https://example.com # Database DB_HOST=postgres DB_PORT=5432 DB_NAME=app_database DB_USER=app_user # NOTE: DB_PASSWORD should be in ./secrets/db_password, not here! # Redis REDIS_HOST=redis REDIS_PORT=6379 # Ports WEB_PORT=80 API_PORT=8080 # Docker COMPOSE_PROJECT_NAME=my-stack ``` ### .env.example File Structure **Purpose**: Template for required environment variables **CRITICAL RULE**: .env.example MUST contain ALL variables from .env and vice versa **Example .env.example:** ```bash # Application APP_NAME=my-application APP_ENV=development APP_DEBUG=true APP_URL=http://localhost # Database DB_HOST=postgres DB_PORT=5432 DB_NAME=app_database DB_USER=app_user # NOTE: DB_PASSWORD is managed via Docker secrets in ./secrets/ # Redis REDIS_HOST=redis REDIS_PORT=6379 # Ports - Customize for your environment WEB_PORT=80 API_PORT=8080 # Docker COMPOSE_PROJECT_NAME=my-stack ``` ### Environment Variable Validation Rules 1. **Synchronization**: Every variable in .env MUST be in .env.example 2. **Documentation**: .env.example should have comments explaining each variable 3. **No Secrets**: .env should NOT contain passwords, API keys, tokens, or secrets 4. **Default Values**: .env.example should have safe defaults for development 5. **Required Variables**: Both files must define all required variables ### Variables That Should NOT Be in .env Move these to ./secrets and use Docker secrets: ```bash # ❌ BAD - Don't put these in .env DB_PASSWORD=supersecret123 API_KEY=sk_live_abc123xyz JWT_SECRET=my-jwt-secret-key STRIPE_SECRET_KEY=sk_test_123 OAUTH_CLIENT_SECRET=abc123xyz # ✅ GOOD - Reference via Docker secrets instead # See docker-compose.yml secrets section # Secrets are in ./secrets/ directory ``` --- ## Secrets Management Patterns ### Secrets Directory Structure ``` secrets/ ├── db_password # PostgreSQL password ├── db_root_password # Root password (if needed) ├── api_key # External API key ├── jwt_secret # JWT signing secret └── oauth_client_secret # OAuth secret ``` ### Secret File Format **Single-line, no trailing newline:** ```bash # Create secret (no newline) echo -n "my-secret-value" > ./secrets/db_password # ✅ Correct: 16 bytes # ❌ Wrong: 17 bytes (includes newline) ``` ### Secret File Permissions ```bash # Directory chmod 700 ./secrets/ # Individual files chmod 600 ./secrets/db_password chmod 600 ./secrets/api_key ``` ### Docker Compose Secrets Pattern **Top-level secrets definition:** ```yaml secrets: db_password: file: ./secrets/db_password api_key: file: ./secrets/api_key jwt_secret: file: ./secrets/jwt_secret ``` **Service secrets reference:** ```yaml services: app: image: myapp:latest secrets: - db_password - api_key - jwt_secret environment: # ✅ GOOD - Reference location, not value DB_PASSWORD_FILE: /run/secrets/db_password API_KEY_FILE: /run/secrets/api_key # ❌ BAD - Don't put actual secrets here # DB_PASSWORD: supersecret123 ``` ### Application Secret Usage **In application code:** ```python # Read secret from Docker secrets mount def get_secret(secret_name): secret_path = f'/run/secrets/{secret_name}' with open(secret_path, 'r') as f: return f.read().strip() # Usage db_password = get_secret('db_password') api_key = get_secret('api_key') ``` --- ## Docker Compose Patterns ### Modern Docker Compose Format **❌ DON'T use version field:** ```yaml # ❌ OLD - Don't include version version: '3.8' ``` **✅ DO use modern format:** ```yaml # ✅ MODERN - No version field services: app: image: myapp:latest ``` ### Complete Stack Example ```yaml services: app: image: myapp:latest container_name: my-app restart: unless-stopped depends_on: - postgres - redis secrets: - db_password - api_key environment: APP_ENV: ${APP_ENV} DB_HOST: ${DB_HOST} DB_PORT: ${DB_PORT} DB_NAME: ${DB_NAME} DB_USER: ${DB_USER} DB_PASSWORD_FILE: /run/secrets/db_password API_KEY_FILE: /run/secrets/api_key volumes: - ./config/app:/app/config:ro - app-data:/app/data networks: - app-network ports: - "${WEB_PORT}:80" postgres: image: postgres:16-alpine container_name: my-postgres restart: unless-stopped secrets: - db_password environment: POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD_FILE: /run/secrets/db_password volumes: - ./config/db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - postgres-data:/var/lib/postgresql/data networks: - app-network redis: image: redis:7-alpine container_name: my-redis restart: unless-stopped volumes: - redis-data:/data networks: - app-network volumes: app-data: postgres-data: redis-data: networks: app-network: driver: bridge secrets: db_password: file: ./secrets/db_password api_key: file: ./secrets/api_key ``` ### Volume Mount Patterns **Configuration files (read-only):** ```yaml volumes: - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./config/app/settings.yml:/app/config/settings.yml:ro ``` **Secrets (via Docker secrets - automatic mount):** ```yaml secrets: - db_password # Mounted at /run/secrets/db_password ``` **Persistent data (named volumes):** ```yaml volumes: - postgres-data:/var/lib/postgresql/data - redis-data:/data ``` **Temporary files (local directory):** ```yaml volumes: - ./_temporary/cache:/app/cache - ./_temporary/uploads:/app/uploads ``` --- ## Configuration Patterns ### Configuration File Organization **By service:** ``` config/ ├── nginx/ │ ├── nginx.conf │ └── ssl/ │ ├── cert.pem │ └── key.pem ├── app/ │ ├── settings.yml │ └── logging.conf └── db/ └── init.sql ``` ### Configuration vs Secrets Separation **✅ Configuration (in ./config):** - Server hostnames - Port numbers - Feature flags - Logging levels - Public certificates - Database names - Cache settings **❌ NOT Configuration (in ./secrets):** - Passwords - API keys - Tokens - Private keys - OAuth secrets - JWT secrets - Encryption keys ### Example Configuration File **config/app/settings.yml:** ```yaml # Application Settings app: name: ${APP_NAME} environment: ${APP_ENV} debug: ${APP_DEBUG} url: ${APP_URL} # Database (connection info, NOT credentials) database: host: ${DB_HOST} port: ${DB_PORT} name: ${DB_NAME} user: ${DB_USER} # Password loaded from /run/secrets/db_password # Redis cache: driver: redis host: ${REDIS_HOST} port: ${REDIS_PORT} # Logging logging: level: info output: stdout format: json ``` --- ## File Ownership Patterns ### Correct Ownership All stack files should be owned by the Docker user (current user), NOT root. **Check ownership:** ```bash # List all files with ownership eza -la --tree # Find root-owned files (should return nothing) find . -type f -user root 2>/dev/null ``` ### Common Ownership Issues **Problem**: Files created by Docker containers as root **Example scenario:** ```yaml # Container runs as root, creates files in mounted volume services: app: image: nginx:latest # Runs as root by default volumes: - ./config/nginx.conf:/etc/nginx/nginx.conf - ./_temporary/cache:/var/cache/nginx # ⚠️ Creates files as root! ``` **Fix**: Ensure containers run as non-root user ```yaml services: app: image: nginx:latest user: "${UID}:${GID}" # Run as current user volumes: - ./config/nginx.conf:/etc/nginx/nginx.conf - ./_temporary/cache:/var/cache/nginx ``` ### Fixing Ownership **Stack-validator detects ownership issues but doesn't fix them.** **Manual fix (user action):** ```bash # Fix ownership of specific file sudo chown $(id -u):$(id -g) ./config/nginx.conf # Fix ownership of entire directory sudo chown -R $(id -u):$(id -g) ./config/ # Fix ownership of all project files sudo chown -R $(id -u):$(id -g) . ``` **Prevention**: Use stack-creator skill to properly initialize stacks with correct ownership from the start. --- ## Validation Checklist ### Pre-Deployment Validation Use this checklist to ensure stack readiness: - [ ] **Directory Structure** - [ ] ./config directory exists - [ ] ./secrets directory exists with 700 permissions - [ ] ./_temporary directory exists - [ ] .gitignore excludes secrets, .env, _temporary - [ ] **Environment Variables** - [ ] .env file exists and is valid - [ ] .env.example exists and is valid - [ ] .env and .env.example are synchronized - [ ] No secrets in .env file - [ ] .env is in .gitignore - [ ] **Docker Configuration** - [ ] docker-compose.yml has no version field - [ ] docker-compose.yml passes docker-validation - [ ] Secrets defined in top-level secrets section - [ ] Services reference secrets via secrets key - [ ] Volume mounts follow patterns - [ ] **Secrets Management** - [ ] All secret files exist in ./secrets - [ ] Secret files have 600 permissions - [ ] ./secrets directory has 700 permissions - [ ] No secrets in docker-compose.yml environment - [ ] No secrets in git - [ ] **File Ownership** - [ ] No root-owned files in project - [ ] All files owned by Docker user - [ ] Config files have correct ownership - [ ] **Configuration** - [ ] Config files properly organized - [ ] No secrets in config files - [ ] Config file syntax valid --- ## Reference: Common Validation Failures | Issue | Category | Severity | Fix With | |-------|----------|----------|----------| | Missing .env.example | Environment | Critical | stack-creator | | .env/.env.example mismatch | Environment | Critical | Manual sync + stack-creator | | Secrets in .env | Security | Critical | secrets-manager | | ./secrets not in .gitignore | Security | Critical | stack-creator | | Root-owned files | Ownership | High | Manual chown | | Missing ./secrets directory | Secrets | High | stack-creator | | docker-compose.yml has version | Docker | Medium | Manual edit | | Secrets in environment vars | Security | Critical | secrets-manager | | Missing required directory | Structure | High | stack-creator | | ./_temporary not empty | Cleanup | Low | Manual cleanup | --- *These patterns ensure consistent, secure, and maintainable GitLab stack projects.*