# ADR-014: Containerization and Deployment Strategy **Date**: 2025-12-12 **Status**: Implemented **Implemented**: 2026-01-09 ## Context The project is currently run using `pm2`, and the `README.md` contains manual setup instructions. While functional, this lacks the portability, scalability, and consistency of modern deployment practices. Local development environments also suffered from inconsistency issues. ## Decision We will standardize the deployment process using a hybrid approach: 1. **PM2 for Production**: Use PM2 cluster mode for process management, load balancing, and zero-downtime reloads. 2. **Docker/Podman for Development**: Provide a complete containerized development environment with automatic initialization. 3. **VS Code Dev Containers**: Enable one-click development environment setup. 4. **Gitea Actions for CI/CD**: Automated deployment pipelines handle builds and deployments. ## Consequences - **Positive**: Ensures consistency between development and production environments. Simplifies the setup for new developers to a single "Reopen in Container" action. Improves portability and scalability of the application. - **Negative**: Requires Docker/Podman installation. Container builds take time on first setup. ## Implementation Details ### Quick Start (Development) ```bash # Prerequisites: # - Docker Desktop or Podman installed # - VS Code with "Dev Containers" extension # Option 1: VS Code Dev Containers (Recommended) # 1. Open project in VS Code # 2. Click "Reopen in Container" when prompted # 3. Wait for initialization to complete # 4. Development server starts automatically # Option 2: Manual Docker Compose podman-compose -f compose.dev.yml up -d podman exec -it flyer-crawler-dev bash ./scripts/docker-init.sh npm run dev:container ``` ### Container Services Architecture ```text ┌─────────────────────────────────────────────────────────────┐ │ Development Environment │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ app │ │ postgres │ │ redis │ │ │ │ (Node.js) │───▶│ (PostGIS) │ │ (Cache) │ │ │ │ │───▶│ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ :3000/:3001 :5432 :6379 │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### compose.dev.yml Services | Service | Image | Purpose | Healthcheck | | ---------- | ----------------------- | ---------------------- | ---------------- | | `app` | Custom (Dockerfile.dev) | Node.js application | HTTP /api/health | | `postgres` | postgis/postgis:15-3.4 | Database with PostGIS | pg_isready | | `redis` | redis:alpine | Caching and job queues | redis-cli ping | ### Automatic Initialization The container initialization script (`scripts/docker-init.sh`) performs: 1. **npm install** - Installs dependencies into isolated volume 2. **Wait for PostgreSQL** - Polls until database is ready 3. **Wait for Redis** - Polls until Redis is responding 4. **Schema Check** - Detects if database needs initialization 5. **Database Setup** - Runs `npm run db:reset:dev` if needed (schema + seed data) ### Development Dockerfile Located in `Dockerfile.dev`: ```dockerfile FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive # Install Node.js 20.x LTS + database clients RUN apt-get update && apt-get install -y \ curl git build-essential python3 \ postgresql-client redis-tools \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y nodejs WORKDIR /app ENV NODE_ENV=development ENV NODE_OPTIONS='--max-old-space-size=8192' CMD ["bash"] ``` ### Environment Configuration Copy `.env.example` to `.env` for local overrides (optional for containers): ```bash # Container defaults (set in compose.dev.yml) DB_HOST=postgres # Use Docker service name, not IP DB_PORT=5432 DB_USER=postgres DB_PASSWORD=postgres DB_NAME=flyer_crawler_dev REDIS_URL=redis://redis:6379 ``` ### VS Code Dev Container Configuration Located in `.devcontainer/devcontainer.json`: | Lifecycle Hook | Timing | Action | | ------------------- | ----------------- | ------------------------------ | | `initializeCommand` | Before container | Start Podman machine (Windows) | | `postCreateCommand` | Container created | Run `docker-init.sh` | | `postAttachCommand` | VS Code attached | Start dev server | ### Default Test Accounts After initialization, these accounts are available: | Role | Email | Password | | ----- | ------------------- | --------- | | Admin | `admin@example.com` | adminpass | | User | `user@example.com` | userpass | --- ## Production Deployment (PM2) ### PM2 Ecosystem Configuration Located in `ecosystem.config.cjs`: ```javascript module.exports = { apps: [ { // API Server - Cluster mode for load balancing name: 'flyer-crawler-api', script: './node_modules/.bin/tsx', args: 'server.ts', max_memory_restart: '500M', instances: 'max', // Use all CPU cores exec_mode: 'cluster', // Enable cluster mode kill_timeout: 5000, // Graceful shutdown timeout // Restart configuration max_restarts: 40, exp_backoff_restart_delay: 100, min_uptime: '10s', env_production: { NODE_ENV: 'production', cwd: '/var/www/flyer-crawler.projectium.com', }, env_test: { NODE_ENV: 'test', cwd: '/var/www/flyer-crawler-test.projectium.com', }, }, { // Background Worker - Single instance name: 'flyer-crawler-worker', script: './node_modules/.bin/tsx', args: 'src/services/worker.ts', max_memory_restart: '1G', kill_timeout: 10000, // Workers need more time for jobs // ... similar config }, ], }; ``` ### Deployment Directory Structure ```text /var/www/ ├── flyer-crawler.projectium.com/ # Production │ ├── server.ts │ ├── ecosystem.config.cjs │ ├── package.json │ ├── flyer-images/ │ │ ├── icons/ │ │ └── archive/ │ └── ... └── flyer-crawler-test.projectium.com/ # Test environment └── ... (same structure) ``` ### Environment-Specific Configuration | Environment | Port | Redis DB | PM2 Process Suffix | | ----------- | ---- | -------- | ------------------ | | Production | 3000 | 0 | (none) | | Test | 3001 | 1 | `-test` | | Development | 3000 | 0 | `-dev` | ### PM2 Commands Reference ```bash # Start/reload with environment pm2 startOrReload ecosystem.config.cjs --env production --update-env # Save process list for startup pm2 save # View logs pm2 logs flyer-crawler-api --lines 50 # Monitor processes pm2 monit # List all processes pm2 list # Describe process details pm2 describe flyer-crawler-api ``` ### Resource Limits | Process | Memory Limit | Restart Delay | Kill Timeout | | ---------------- | ------------ | ------------------------ | ------------ | | API Server | 500MB | Exponential (100ms base) | 5s | | Worker | 1GB | Exponential (100ms base) | 10s | | Analytics Worker | 1GB | Exponential (100ms base) | 10s | --- ## Troubleshooting ### Container Issues ```bash # Reset everything and start fresh podman-compose -f compose.dev.yml down -v podman-compose -f compose.dev.yml up -d --build # View container logs podman-compose -f compose.dev.yml logs -f app # Connect to database manually podman exec -it flyer-crawler-postgres psql -U postgres -d flyer_crawler_dev # Rebuild just the app container podman-compose -f compose.dev.yml build app ``` ### Common Issues | Issue | Solution | | ------------------------ | --------------------------------------------------------------- | | "Database not ready" | Wait for postgres healthcheck, or run `docker-init.sh` manually | | "node_modules not found" | Run `npm install` inside container | | "Permission denied" | Ensure scripts have execute permission: `chmod +x scripts/*.sh` | | "Network unreachable" | Use service names (postgres, redis) not IPs | ## Key Files - `compose.dev.yml` - Docker Compose configuration - `Dockerfile.dev` - Development container definition - `.devcontainer/devcontainer.json` - VS Code Dev Container config - `scripts/docker-init.sh` - Container initialization script - `.env.example` - Environment variable template - `ecosystem.config.cjs` - PM2 production configuration - `.gitea/workflows/deploy-to-prod.yml` - Production deployment pipeline - `.gitea/workflows/deploy-to-test.yml` - Test deployment pipeline ## Related ADRs - [ADR-017](./0017-ci-cd-and-branching-strategy.md) - CI/CD Strategy - [ADR-038](./0038-graceful-shutdown-pattern.md) - Graceful Shutdown Pattern