Files
gh-rknall-claude-skills-sta…/validation-patterns.md
2025-11-30 08:52:10 +08:00

565 lines
12 KiB
Markdown

# 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.*