12 KiB
Stack Validation Patterns
This document outlines the architecture patterns and validation criteria for GitLab stack projects.
Table of Contents
- Directory Structure Patterns
- Environment Variable Patterns
- Secrets Management Patterns
- Docker Compose Patterns
- Configuration 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:
# 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:
# 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:
# 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
- Synchronization: Every variable in .env MUST be in .env.example
- Documentation: .env.example should have comments explaining each variable
- No Secrets: .env should NOT contain passwords, API keys, tokens, or secrets
- Default Values: .env.example should have safe defaults for development
- Required Variables: Both files must define all required variables
Variables That Should NOT Be in .env
Move these to ./secrets and use Docker secrets:
# ❌ 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:
# Create secret (no newline)
echo -n "my-secret-value" > ./secrets/db_password
# ✅ Correct: 16 bytes
# ❌ Wrong: 17 bytes (includes newline)
Secret File Permissions
# Directory
chmod 700 ./secrets/
# Individual files
chmod 600 ./secrets/db_password
chmod 600 ./secrets/api_key
Docker Compose Secrets Pattern
Top-level secrets definition:
secrets:
db_password:
file: ./secrets/db_password
api_key:
file: ./secrets/api_key
jwt_secret:
file: ./secrets/jwt_secret
Service secrets reference:
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:
# 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:
# ❌ OLD - Don't include version
version: '3.8'
✅ DO use modern format:
# ✅ MODERN - No version field
services:
app:
image: myapp:latest
Complete Stack Example
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):
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):
secrets:
- db_password # Mounted at /run/secrets/db_password
Persistent data (named volumes):
volumes:
- postgres-data:/var/lib/postgresql/data
- redis-data:/data
Temporary files (local directory):
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:
# 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:
# 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:
# 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
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):
# 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.