# DEPLOYMENT GUIDE - ERP Generic **Última actualización:** 2025-11-24 **Responsable:** DevOps Team **Estado:** ✅ Production-Ready --- ## TABLE OF CONTENTS 1. [Overview](#1-overview) 2. [Prerequisites](#2-prerequisites) 3. [Docker Setup](#3-docker-setup) 4. [PostgreSQL 16 Setup](#4-postgresql-16-setup) 5. [Redis Setup](#5-redis-setup) 6. [Environment Variables](#6-environment-variables) 7. [Multi-Environment Strategy](#7-multi-environment-strategy) 8. [Deployment Steps](#8-deployment-steps) 9. [Zero-Downtime Deployment](#9-zero-downtime-deployment) 10. [Rollback Procedures](#10-rollback-procedures) 11. [Troubleshooting](#11-troubleshooting) 12. [References](#12-references) --- ## 1. OVERVIEW ### 1.1 System Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Load Balancer / Nginx │ │ (SSL Termination) │ └────────────┬────────────────────────────────┬───────────────┘ │ │ ┌───────▼───────┐ ┌──────▼──────┐ │ Frontend │ │ Backend │ │ React 18 │ │ NestJS 10 │ │ Vite 5 │ │ Prisma 5 │ └───────────────┘ └──────┬──────┘ │ ┌───────────────────┼────────────────┐ │ │ │ ┌──────▼──────┐ ┌──────▼─────┐ ┌──────▼──────┐ │ PostgreSQL │ │ Redis │ │ S3 │ │ 16 │ │ 7 │ │ (Storage) │ │ 9 schemas │ │ (Cache) │ │ │ └─────────────┘ └────────────┘ └─────────────┘ ``` ### 1.2 Components | Component | Version | Purpose | Port | |-----------|---------|---------|------| | **Backend** | NestJS 10 + Node.js 20 | REST API + Business Logic | 3000 | | **Frontend** | React 18 + Vite 5 | Web UI | 5173 | | **PostgreSQL** | 16-alpine | Primary Database (9 schemas) | 5432 | | **Redis** | 7-alpine | Session store + Cache | 6379 | | **Nginx** | 1.25-alpine | Reverse proxy + SSL | 80, 443 | ### 1.3 System Requirements **Minimum (Development):** - CPU: 4 cores - RAM: 8 GB - Disk: 50 GB SSD - OS: Linux (Ubuntu 22.04 LTS recommended) **Recommended (Production):** - CPU: 8+ cores - RAM: 16+ GB - Disk: 200+ GB SSD (NVMe preferred) - OS: Ubuntu 22.04 LTS / RHEL 9 / Amazon Linux 2023 **Scalability (High Load):** - CPU: 16+ cores - RAM: 32+ GB - Disk: 500+ GB SSD NVMe - Load Balancer: AWS ELB / Azure Load Balancer / Nginx - Database: PostgreSQL 16 with read replicas --- ## 2. PREREQUISITES ### 2.1 Software Requirements ```bash # Docker & Docker Compose docker --version # >= 24.0 docker-compose --version # >= 2.20 # Git git --version # >= 2.40 # Node.js (for local development) node --version # >= 20.0 LTS npm --version # >= 10.0 # PostgreSQL Client (for manual operations) psql --version # >= 16.0 # Redis Client (for manual operations) redis-cli --version # >= 7.0 ``` ### 2.2 Installation (Ubuntu 22.04) ```bash # Update system sudo apt update && sudo apt upgrade -y # Install Docker curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER newgrp docker # Install Docker Compose sudo curl -L "https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # Install PostgreSQL client sudo apt install -y postgresql-client-16 # Install Redis client sudo apt install -y redis-tools # Install Git sudo apt install -y git # Verify installations docker --version docker-compose --version psql --version redis-cli --version git --version ``` ### 2.3 Network Requirements **Firewall Rules (Inbound):** - Port 80 (HTTP) - Allow from Load Balancer only - Port 443 (HTTPS) - Allow from Load Balancer only - Port 22 (SSH) - Allow from trusted IPs only (bastion host) - Port 3000 (Backend API) - Internal only - Port 5432 (PostgreSQL) - Internal only - Port 6379 (Redis) - Internal only **Firewall Rules (Outbound):** - Port 80, 443 (HTTPS) - Allow (for package downloads, API calls) - Port 25, 587 (SMTP) - Allow (for email notifications) **DNS Records:** ``` # Production erp-generic.com A api.erp-generic.com A *.erp-generic.com A # Multi-tenant subdomains # Staging staging.erp-generic.com A # QA qa.erp-generic.local A ``` --- ## 3. DOCKER SETUP ### 3.1 Directory Structure ``` erp-generic/ ├── backend/ │ ├── Dockerfile │ ├── .dockerignore │ ├── src/ │ ├── prisma/ │ └── package.json ├── frontend/ │ ├── Dockerfile │ ├── .dockerignore │ ├── src/ │ └── package.json ├── docker-compose.yml ├── docker-compose.prod.yml ├── docker-compose.dev.yml ├── .env.example └── nginx/ └── nginx.conf ``` ### 3.2 Backend Dockerfile **File:** `backend/Dockerfile` ```dockerfile # Stage 1: Build FROM node:20-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ COPY prisma ./prisma/ # Install dependencies (including devDependencies for build) RUN npm ci # Copy source code COPY . . # Generate Prisma Client RUN npx prisma generate # Build TypeScript RUN npm run build # Stage 2: Production FROM node:20-alpine AS production WORKDIR /app # Install only production dependencies COPY package*.json ./ COPY prisma ./prisma/ RUN npm ci --only=production # Copy built files from builder stage COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma # Create non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -S nestjs -u 1001 USER nestjs # Expose port EXPOSE 3000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); });" # Start application CMD ["node", "dist/main.js"] ``` ### 3.3 Frontend Dockerfile **File:** `frontend/Dockerfile` ```dockerfile # Stage 1: Build FROM node:20-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci # Copy source code COPY . . # Build application RUN npm run build # Stage 2: Production with Nginx FROM nginx:1.25-alpine AS production # Copy built files COPY --from=builder /app/dist /usr/share/nginx/html # Copy custom nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf # Expose port EXPOSE 80 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 # Start nginx CMD ["nginx", "-g", "daemon off;"] ``` **File:** `frontend/nginx.conf` ```nginx server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; # Gzip compression gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # SPA routing - all routes go to index.html location / { try_files $uri $uri/ /index.html; } # API proxy (optional - if frontend and backend on same domain) location /api { proxy_pass http://backend:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } } ``` ### 3.4 Docker Compose - Development **File:** `docker-compose.dev.yml` ```yaml version: '3.9' services: postgres: image: postgres:16-alpine container_name: erp-postgres-dev environment: POSTGRES_DB: erp_generic_dev POSTGRES_USER: erp_user POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dev_password_change_me} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8" volumes: - postgres_data_dev:/var/lib/postgresql/data - ./database/init-scripts:/docker-entrypoint-initdb.d:ro ports: - "5432:5432" networks: - erp-network healthcheck: test: ["CMD-SHELL", "pg_isready -U erp_user -d erp_generic_dev"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped redis: image: redis:7-alpine container_name: erp-redis-dev command: redis-server --requirepass ${REDIS_PASSWORD:-dev_redis_pass} --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis_data_dev:/data ports: - "6379:6379" networks: - erp-network healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] interval: 10s timeout: 3s retries: 5 restart: unless-stopped backend: build: context: ./backend dockerfile: Dockerfile target: builder # Use builder stage for development (includes dev dependencies) container_name: erp-backend-dev environment: NODE_ENV: development DATABASE_URL: postgresql://erp_user:${POSTGRES_PASSWORD:-dev_password_change_me}@postgres:5432/erp_generic_dev?schema=public REDIS_URL: redis://default:${REDIS_PASSWORD:-dev_redis_pass}@redis:6379 JWT_SECRET: ${JWT_SECRET:-dev_jwt_secret_change_me} JWT_EXPIRES_IN: 7d PORT: 3000 LOG_LEVEL: debug volumes: - ./backend:/app - /app/node_modules - /app/dist ports: - "3000:3000" - "9229:9229" # Debugger port depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - erp-network command: npm run start:dev restart: unless-stopped frontend: build: context: ./frontend dockerfile: Dockerfile target: builder container_name: erp-frontend-dev environment: VITE_API_URL: http://localhost:3000/api VITE_APP_ENV: development volumes: - ./frontend:/app - /app/node_modules - /app/dist ports: - "5173:5173" depends_on: - backend networks: - erp-network command: npm run dev -- --host restart: unless-stopped volumes: postgres_data_dev: name: erp_postgres_data_dev redis_data_dev: name: erp_redis_data_dev networks: erp-network: name: erp-network-dev driver: bridge ``` ### 3.5 Docker Compose - Production **File:** `docker-compose.prod.yml` ```yaml version: '3.9' services: postgres: image: postgres:16-alpine container_name: erp-postgres environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8" volumes: - postgres_data:/var/lib/postgresql/data - ./database/init-scripts:/docker-entrypoint-initdb.d:ro - /backups/postgres:/backups:rw # Do NOT expose ports publicly in production networks: - erp-network-internal healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 restart: always deploy: resources: limits: cpus: '4' memory: 8G reservations: cpus: '2' memory: 4G logging: driver: "json-file" options: max-size: "10m" max-file: "3" redis: image: redis:7-alpine container_name: erp-redis command: redis-server --requirepass ${REDIS_PASSWORD} --maxmemory 2gb --maxmemory-policy allkeys-lru --appendonly yes volumes: - redis_data:/data networks: - erp-network-internal healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] interval: 10s timeout: 3s retries: 5 restart: always deploy: resources: limits: cpus: '2' memory: 3G reservations: cpus: '1' memory: 2G logging: driver: "json-file" options: max-size: "10m" max-file: "3" backend: build: context: ./backend dockerfile: Dockerfile target: production image: ${DOCKER_REGISTRY}/erp-generic-backend:${VERSION:-latest} container_name: erp-backend environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public&connection_limit=20&pool_timeout=30 REDIS_URL: redis://default:${REDIS_PASSWORD}@redis:6379 JWT_SECRET: ${JWT_SECRET} JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-24h} JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-7d} PORT: 3000 LOG_LEVEL: ${LOG_LEVEL:-info} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT} SMTP_USER: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} S3_BUCKET: ${S3_BUCKET} S3_REGION: ${S3_REGION} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - erp-network-internal - erp-network-external restart: always deploy: replicas: 3 update_config: parallelism: 1 delay: 10s order: start-first rollback_config: parallelism: 1 delay: 5s resources: limits: cpus: '2' memory: 2G reservations: cpus: '1' memory: 1G logging: driver: "json-file" options: max-size: "50m" max-file: "5" healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"] interval: 30s timeout: 10s retries: 3 start_period: 40s frontend: build: context: ./frontend dockerfile: Dockerfile target: production image: ${DOCKER_REGISTRY}/erp-generic-frontend:${VERSION:-latest} container_name: erp-frontend environment: VITE_API_URL: ${API_URL} VITE_APP_ENV: production depends_on: - backend networks: - erp-network-external restart: always deploy: replicas: 2 update_config: parallelism: 1 delay: 10s resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M logging: driver: "json-file" options: max-size: "10m" max-file: "3" nginx: image: nginx:1.25-alpine container_name: erp-nginx volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/ssl:/etc/nginx/ssl:ro - ./nginx/logs:/var/log/nginx:rw ports: - "80:80" - "443:443" depends_on: - frontend - backend networks: - erp-network-external restart: always deploy: resources: limits: cpus: '1' memory: 512M logging: driver: "json-file" options: max-size: "20m" max-file: "5" volumes: postgres_data: name: erp_postgres_data driver: local driver_opts: type: none o: bind device: /data/postgres redis_data: name: erp_redis_data driver: local networks: erp-network-internal: name: erp-network-internal driver: bridge internal: true erp-network-external: name: erp-network-external driver: bridge ``` --- ## 4. POSTGRESQL 16 SETUP ### 4.1 Initialization Script **File:** `database/init-scripts/01-init-schemas.sql` ```sql -- ===================================================== -- ERP GENERIC - PostgreSQL 16 Initialization -- Creates 9 schemas + roles + extensions -- ===================================================== -- Create extensions CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For text search CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- For indexing CREATE EXTENSION IF NOT EXISTS "pg_stat_statements"; -- For query monitoring -- Create roles CREATE ROLE erp_readonly; CREATE ROLE erp_readwrite; CREATE ROLE erp_admin; -- Create 9 schemas CREATE SCHEMA IF NOT EXISTS auth; CREATE SCHEMA IF NOT EXISTS core; CREATE SCHEMA IF NOT EXISTS financial; CREATE SCHEMA IF NOT EXISTS inventory; CREATE SCHEMA IF NOT EXISTS purchase; CREATE SCHEMA IF NOT EXISTS sales; CREATE SCHEMA IF NOT EXISTS analytics; CREATE SCHEMA IF NOT EXISTS projects; CREATE SCHEMA IF NOT EXISTS system; -- Grant permissions to schemas GRANT USAGE ON SCHEMA auth TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA core TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA financial TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA inventory TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA purchase TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA sales TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA analytics TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA projects TO erp_readonly, erp_readwrite, erp_admin; GRANT USAGE ON SCHEMA system TO erp_readonly, erp_readwrite, erp_admin; -- Grant SELECT to readonly GRANT SELECT ON ALL TABLES IN SCHEMA auth TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA core TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA financial TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA inventory TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA purchase TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA sales TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA projects TO erp_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA system TO erp_readonly; -- Grant SELECT, INSERT, UPDATE, DELETE to readwrite GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA core TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA financial TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA inventory TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA purchase TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA sales TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA analytics TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA projects TO erp_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA system TO erp_readwrite; -- Grant ALL to admin GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA auth TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA core TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA financial TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA inventory TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA purchase TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA sales TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA analytics TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA projects TO erp_admin; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA system TO erp_admin; -- Default privileges for future tables ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA financial GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA inventory GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA purchase GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA projects GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA system GRANT SELECT ON TABLES TO erp_readonly; ALTER DEFAULT PRIVILEGES IN SCHEMA auth GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA financial GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA inventory GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA purchase GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA sales GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA projects GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; ALTER DEFAULT PRIVILEGES IN SCHEMA system GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO erp_readwrite; -- Enable Row Level Security (RLS) on all schemas ALTER SCHEMA auth ENABLE; ALTER SCHEMA core ENABLE; ALTER SCHEMA financial ENABLE; ALTER SCHEMA inventory ENABLE; ALTER SCHEMA purchase ENABLE; ALTER SCHEMA sales ENABLE; ALTER SCHEMA analytics ENABLE; ALTER SCHEMA projects ENABLE; ALTER SCHEMA system ENABLE; -- Create audit function CREATE OR REPLACE FUNCTION system.audit_trigger_function() RETURNS TRIGGER AS $$ BEGIN IF (TG_OP = 'INSERT') THEN NEW.created_at = CURRENT_TIMESTAMP; NEW.created_by = COALESCE(NEW.created_by, current_setting('app.current_user_id', TRUE)::UUID); ELSIF (TG_OP = 'UPDATE') THEN NEW.updated_at = CURRENT_TIMESTAMP; NEW.updated_by = current_setting('app.current_user_id', TRUE)::UUID; NEW.created_at = OLD.created_at; NEW.created_by = OLD.created_by; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Create tenant context function CREATE OR REPLACE FUNCTION auth.get_current_tenant_id() RETURNS UUID AS $$ BEGIN RETURN NULLIF(current_setting('app.current_tenant_id', TRUE), '')::UUID; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION auth.get_current_tenant_id() IS 'Returns the current tenant_id from session variable'; -- Success message DO $$ BEGIN RAISE NOTICE 'ERP Generic PostgreSQL initialization completed successfully!'; RAISE NOTICE 'Created 9 schemas: auth, core, financial, inventory, purchase, sales, analytics, projects, system'; RAISE NOTICE 'Created 3 roles: erp_readonly, erp_readwrite, erp_admin'; RAISE NOTICE 'Installed extensions: uuid-ossp, pgcrypto, pg_trgm, btree_gin, pg_stat_statements'; END $$; ``` ### 4.2 PostgreSQL Configuration **File:** `database/postgresql.conf` (Production optimizations) ```ini # Connection Settings max_connections = 200 superuser_reserved_connections = 3 # Memory Settings shared_buffers = 4GB # 25% of RAM effective_cache_size = 12GB # 75% of RAM maintenance_work_mem = 1GB work_mem = 20MB # max_connections * work_mem < RAM temp_buffers = 16MB # Checkpoint Settings checkpoint_completion_target = 0.9 wal_buffers = 16MB default_statistics_target = 100 random_page_cost = 1.1 # For SSD effective_io_concurrency = 200 # For SSD # WAL Settings (for PITR - Point In Time Recovery) wal_level = replica archive_mode = on archive_command = 'test ! -f /backups/wal/%f && cp %p /backups/wal/%f' max_wal_senders = 3 wal_keep_size = 1GB # Query Planning default_statistics_target = 100 random_page_cost = 1.1 # Logging log_destination = 'stderr' logging_collector = on log_directory = '/var/log/postgresql' log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' log_rotation_age = 1d log_rotation_size = 100MB log_min_duration_statement = 1000 # Log queries slower than 1s log_line_prefix = '%m [%p] %q%u@%d ' log_checkpoints = on log_connections = on log_disconnections = on log_duration = off log_lock_waits = on log_statement = 'ddl' # Log DDL statements only log_temp_files = 0 # Performance Monitoring shared_preload_libraries = 'pg_stat_statements' pg_stat_statements.track = all pg_stat_statements.max = 10000 # Locale and Formatting datestyle = 'iso, mdy' timezone = 'UTC' lc_messages = 'en_US.utf8' lc_monetary = 'en_US.utf8' lc_numeric = 'en_US.utf8' lc_time = 'en_US.utf8' default_text_search_config = 'pg_catalog.english' ``` ### 4.3 Database Migrations ```bash # Run Prisma migrations (creates tables, indexes, constraints) docker-compose exec backend npx prisma migrate deploy # Verify migration status docker-compose exec backend npx prisma migrate status # Generate Prisma Client docker-compose exec backend npx prisma generate # Seed initial data docker-compose exec backend npm run seed:initial ``` --- ## 5. REDIS SETUP ### 5.1 Redis Configuration **File:** `redis/redis.conf` ```conf # Network bind 0.0.0.0 protected-mode yes port 6379 tcp-backlog 511 timeout 0 tcp-keepalive 300 # Security requirepass ${REDIS_PASSWORD} # Disable dangerous commands rename-command FLUSHDB "" rename-command FLUSHALL "" rename-command KEYS "" rename-command CONFIG "" # Memory Management maxmemory 2gb maxmemory-policy allkeys-lru maxmemory-samples 5 # Persistence (AOF for durability) appendonly yes appendfilename "appendonly.aof" appendfsync everysec no-appendfsync-on-rewrite no auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # RDB Snapshots (backup) save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes rdbchecksum yes dbfilename dump.rdb # Logging loglevel notice logfile "/var/log/redis/redis-server.log" # Slow log slowlog-log-slower-than 10000 slowlog-max-len 128 # Performance databases 16 ``` ### 5.2 Redis Usage in ERP **Session Storage:** ```typescript // backend/src/common/redis/redis.service.ts @Injectable() export class RedisService { private client: Redis; constructor() { this.client = new Redis({ host: process.env.REDIS_HOST || 'redis', port: parseInt(process.env.REDIS_PORT) || 6379, password: process.env.REDIS_PASSWORD, db: 0, // Sessions DB retryStrategy: (times) => Math.min(times * 50, 2000), }); } // Session management async setSession(sessionId: string, data: any, ttl: number = 86400) { await this.client.setex( `session:${sessionId}`, ttl, JSON.stringify(data) ); } async getSession(sessionId: string): Promise { const data = await this.client.get(`session:${sessionId}`); return data ? JSON.parse(data) : null; } async deleteSession(sessionId: string): Promise { await this.client.del(`session:${sessionId}`); } // Cache management async cacheSet(key: string, value: any, ttl: number = 3600) { await this.client.setex( `cache:${key}`, ttl, JSON.stringify(value) ); } async cacheGet(key: string): Promise { const data = await this.client.get(`cache:${key}`); return data ? JSON.parse(data) : null; } async cacheInvalidate(pattern: string): Promise { const keys = await this.client.keys(`cache:${pattern}*`); if (keys.length > 0) { await this.client.del(...keys); } } } ``` --- ## 6. ENVIRONMENT VARIABLES ### 6.1 .env.example **File:** `.env.example` ```bash # ===================================================== # ERP GENERIC - Environment Variables # ===================================================== # Application NODE_ENV=production APP_NAME=ERP Generic APP_URL=https://erp-generic.com PORT=3000 VERSION=1.0.0 # Database (PostgreSQL 16) POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=erp_generic POSTGRES_USER=erp_user POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD_32_CHARS DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public&connection_limit=20&pool_timeout=30 # Redis REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD REDIS_URL=redis://default:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT} # JWT Authentication JWT_SECRET=CHANGE_ME_JWT_SECRET_64_CHARS_RANDOM_STRING_SECURE JWT_EXPIRES_IN=24h JWT_REFRESH_SECRET=CHANGE_ME_REFRESH_SECRET_64_CHARS JWT_REFRESH_EXPIRES_IN=7d # CORS ALLOWED_ORIGINS=https://erp-generic.com,https://www.erp-generic.com,https://*.erp-generic.com # Email (SMTP) SMTP_HOST=smtp.sendgrid.net SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=apikey SMTP_PASSWORD=SENDGRID_API_KEY SMTP_FROM_EMAIL=noreply@erp-generic.com SMTP_FROM_NAME=ERP Generic # AWS S3 (File Storage) S3_BUCKET=erp-generic-prod S3_REGION=us-east-1 AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # Logging LOG_LEVEL=info LOG_FORMAT=json SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id # Rate Limiting RATE_LIMIT_TTL=60 RATE_LIMIT_MAX=100 # Docker Registry DOCKER_REGISTRY=ghcr.io/your-org # Monitoring PROMETHEUS_PORT=9090 GRAFANA_PORT=3001 # Multi-Tenancy DEFAULT_TENANT_SCHEMA=tenant_default ENABLE_TENANT_ISOLATION=true ``` ### 6.2 Environment-Specific Variables **Development (`.env.development`):** ```bash NODE_ENV=development LOG_LEVEL=debug DATABASE_URL=postgresql://erp_user:dev_pass@localhost:5432/erp_generic_dev REDIS_URL=redis://default:dev_redis@localhost:6379 JWT_SECRET=dev_jwt_secret_not_secure ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 ``` **QA (`.env.qa`):** ```bash NODE_ENV=qa LOG_LEVEL=info DATABASE_URL=postgresql://erp_user:qa_pass@postgres-qa:5432/erp_generic_qa REDIS_URL=redis://default:qa_redis@redis-qa:6379 JWT_SECRET=qa_jwt_secret_secure_32_chars ALLOWED_ORIGINS=https://qa.erp-generic.local ``` **Staging (`.env.staging`):** ```bash NODE_ENV=staging LOG_LEVEL=info DATABASE_URL=postgresql://erp_user:staging_pass@postgres-staging:5432/erp_generic_staging REDIS_URL=redis://default:staging_redis@redis-staging:6379 JWT_SECRET=staging_jwt_secret_secure_64_chars ALLOWED_ORIGINS=https://staging.erp-generic.com S3_BUCKET=erp-generic-staging SENTRY_DSN=https://staging-sentry-dsn@sentry.io/project-id ``` **Production (`.env.production`):** ```bash NODE_ENV=production LOG_LEVEL=warn DATABASE_URL=postgresql://erp_user:PROD_PASSWORD@postgres-prod:5432/erp_generic REDIS_URL=redis://default:PROD_REDIS@redis-prod:6379 JWT_SECRET=PROD_JWT_SECRET_SECURE_64_CHARS_RANDOM ALLOWED_ORIGINS=https://erp-generic.com,https://www.erp-generic.com S3_BUCKET=erp-generic-prod SENTRY_DSN=https://prod-sentry-dsn@sentry.io/project-id ``` ### 6.3 Secret Management **Using AWS Secrets Manager:** ```bash # Fetch secrets from AWS Secrets Manager aws secretsmanager get-secret-value --secret-id erp-generic/prod/database --query SecretString --output text > .env.secrets # Or use in docker-compose services: backend: environment: DATABASE_PASSWORD: "{{secrets:erp-generic/prod/database:password}}" ``` **Using HashiCorp Vault:** ```bash # Fetch secrets from Vault vault kv get -field=password secret/erp-generic/prod/database ``` --- ## 7. MULTI-ENVIRONMENT STRATEGY ### 7.1 Environment Pipeline ``` Development → QA → Staging → Production ↓ ↓ ↓ ↓ Manual Auto Manual Manual Deploy Deploy Deploy Deploy ``` ### 7.2 Deployment Matrix | Characteristic | Development | QA | Staging | Production | |----------------|-------------|-----|---------|------------| | **Trigger** | Manual | Auto (push to develop) | Manual (approved PR) | Manual (release tag) | | **Database** | Local PostgreSQL | Anonymized prod | Prod clone | Production | | **Redis** | Local Redis | Dedicated QA | Dedicated Staging | Production cluster | | **Replicas** | 1 | 1 | 2 | 3+ | | **Resources** | Minimal | Low | Medium | High | | **Monitoring** | Basic logs | Prometheus + Grafana | Full stack | Full stack + alerts | | **Backups** | None | Daily | Hourly | Every 4 hours | | **SSL** | Self-signed | Let's Encrypt | Let's Encrypt | Commercial cert | | **Domain** | localhost | qa.local | staging.com | erp-generic.com | ### 7.3 Promotion Criteria **Development → QA:** - [ ] All unit tests passing (>80% coverage) - [ ] Code review approved - [ ] No TypeScript errors - [ ] Linter passing **QA → Staging:** - [ ] All integration tests passing - [ ] E2E tests passing (critical flows) - [ ] Manual QA sign-off - [ ] Performance tests passing - [ ] Security scan passing (no critical vulnerabilities) **Staging → Production:** - [ ] Staging validation complete (48 hours soak test) - [ ] Load testing passed (>1000 req/s) - [ ] Backup verified - [ ] Rollback plan documented - [ ] Change approval from Product Owner + CTO - [ ] Communication sent to stakeholders --- ## 8. DEPLOYMENT STEPS ### 8.1 First-Time Deployment (Fresh Install) ```bash # Step 1: Clone repository git clone https://github.com/your-org/erp-generic.git cd erp-generic # Step 2: Configure environment cp .env.example .env.production nano .env.production # Edit with real values # Step 3: Generate secrets JWT_SECRET=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -base64 32) REDIS_PASSWORD=$(openssl rand -base64 32) # Update .env.production with generated secrets # Step 4: Start infrastructure services first docker-compose -f docker-compose.prod.yml up -d postgres redis # Wait for databases to be healthy docker-compose -f docker-compose.prod.yml ps # Step 5: Run database initialization docker-compose -f docker-compose.prod.yml exec postgres psql -U erp_user -d erp_generic -f /docker-entrypoint-initdb.d/01-init-schemas.sql # Step 6: Run Prisma migrations docker-compose -f docker-compose.prod.yml run --rm backend npx prisma migrate deploy # Step 7: Seed initial data docker-compose -f docker-compose.prod.yml run --rm backend npm run seed:initial # Step 8: Start application services docker-compose -f docker-compose.prod.yml up -d backend frontend nginx # Step 9: Verify health ./scripts/health-check.sh # Step 10: Check logs docker-compose -f docker-compose.prod.yml logs -f backend # Step 11: Access application curl -I https://erp-generic.com # Expected: HTTP/1.1 200 OK ``` **Total time:** 15-20 minutes ### 8.2 Update Deployment (Rolling Update) ```bash # Step 1: Backup database (safety first!) ./scripts/backup-postgres.sh # Step 2: Pull latest changes git fetch origin git checkout v1.2.0 # Or specific release tag # Step 3: Update environment variables (if needed) diff .env.example .env.production # Add any new variables # Step 4: Build new images docker-compose -f docker-compose.prod.yml build backend frontend # Step 5: Tag images with version docker tag erp-generic-backend:latest erp-generic-backend:v1.2.0 docker tag erp-generic-frontend:latest erp-generic-frontend:v1.2.0 # Step 6: Run database migrations (zero-downtime) docker-compose -f docker-compose.prod.yml run --rm backend npx prisma migrate deploy # Step 7: Rolling update backend (one replica at a time) docker-compose -f docker-compose.prod.yml up -d --no-deps --scale backend=4 backend # Wait 30s for health checks docker-compose -f docker-compose.prod.yml scale backend=3 # Step 8: Update frontend docker-compose -f docker-compose.prod.yml up -d --no-deps frontend # Step 9: Verify health ./scripts/health-check.sh # Step 10: Run smoke tests npm run test:smoke:production # Step 11: Monitor for 15 minutes docker-compose -f docker-compose.prod.yml logs -f backend | grep ERROR ``` **Total time:** 5-10 minutes ### 8.3 Hotfix Deployment (Emergency) ```bash # Step 1: Create hotfix branch git checkout -b hotfix/critical-security-fix # Step 2: Implement fix + tests # ... make changes ... npm run test # Step 3: Fast-track deployment (skip QA for critical issues) git commit -m "hotfix: Fix critical security vulnerability" git push origin hotfix/critical-security-fix # Step 4: Deploy directly to production (with approval) ./scripts/deploy-hotfix.sh hotfix/critical-security-fix # Step 5: Monitor closely tail -f /var/log/erp-generic/backend.log # Step 6: Communicate to stakeholders # Send email notification about hotfix deployment ``` **Total time:** 5-15 minutes (depending on complexity) --- ## 9. ZERO-DOWNTIME DEPLOYMENT ### 9.1 Blue-Green Deployment Strategy ``` ┌─────────────────────────────────────────────────────┐ │ Load Balancer (Nginx) │ │ (Routes traffic to active env) │ └─────┬──────────────────────────────────────┬────────┘ │ │ │ 100% traffic │ 0% traffic ↓ ↓ ┌──────────────┐ ┌──────────────┐ │ Blue (v1.1) │ │ Green (v1.2) │ │ ACTIVE │ │ STANDBY │ │ │ │ │ │ Backend x3 │ │ Backend x3 │ │ Frontend │ │ Frontend │ │ Database │ │ Database │ └──────────────┘ └──────────────┘ After testing Green: │ 0% traffic │ 100% traffic ↓ ↓ ┌──────────────┐ ┌──────────────┐ │ Blue (v1.1) │ │ Green (v1.2) │ │ STANDBY │ ← Rollback │ ACTIVE │ └──────────────┘ └──────────────┘ ``` ### 9.2 Implementation **File:** `scripts/deploy-blue-green.sh` ```bash #!/bin/bash set -euo pipefail CURRENT_ENV=${1:-blue} NEW_VERSION=${2:-latest} echo "===== Blue-Green Deployment =====" echo "Current environment: $CURRENT_ENV" echo "New version: $NEW_VERSION" # Determine new environment if [ "$CURRENT_ENV" = "blue" ]; then NEW_ENV="green" else NEW_ENV="blue" fi echo "Deploying to: $NEW_ENV" # Step 1: Deploy to standby environment echo "1. Deploying to $NEW_ENV environment..." docker-compose -f docker-compose.$NEW_ENV.yml up -d --build # Step 2: Wait for health checks echo "2. Waiting for health checks..." sleep 30 for i in {1..10}; do if ./scripts/health-check.sh $NEW_ENV; then echo "Health check passed!" break fi if [ $i -eq 10 ]; then echo "Health check failed after 10 attempts. Aborting deployment." exit 1 fi echo "Health check failed. Retrying in 10s... ($i/10)" sleep 10 done # Step 3: Run smoke tests echo "3. Running smoke tests on $NEW_ENV..." npm run test:smoke -- --env=$NEW_ENV if [ $? -ne 0 ]; then echo "Smoke tests failed. Aborting deployment." exit 1 fi # Step 4: Switch traffic to new environment echo "4. Switching traffic to $NEW_ENV..." # Update nginx config to point to new environment sed -i "s/upstream backend_$CURRENT_ENV/upstream backend_$NEW_ENV/g" /etc/nginx/nginx.conf nginx -s reload echo "Traffic switched to $NEW_ENV" # Step 5: Monitor for 5 minutes echo "5. Monitoring $NEW_ENV for 5 minutes..." sleep 300 ERROR_COUNT=$(docker-compose -f docker-compose.$NEW_ENV.yml logs backend | grep -c ERROR || true) if [ $ERROR_COUNT -gt 10 ]; then echo "Too many errors detected ($ERROR_COUNT). Rolling back..." ./scripts/rollback.sh $CURRENT_ENV exit 1 fi # Step 6: Shutdown old environment echo "6. Shutting down old environment $CURRENT_ENV..." docker-compose -f docker-compose.$CURRENT_ENV.yml down echo "===== Deployment Complete =====" echo "Active environment: $NEW_ENV" echo "Version: $NEW_VERSION" ``` ### 9.3 Health Check Endpoint ```typescript // backend/src/health/health.controller.ts import { Controller, Get } from '@nestjs/common'; import { HealthCheck, HealthCheckService, PrismaHealthIndicator, MemoryHealthIndicator } from '@nestjs/terminus'; import { RedisHealthIndicator } from './redis.health'; @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private db: PrismaHealthIndicator, private redis: RedisHealthIndicator, private memory: MemoryHealthIndicator, ) {} @Get() @HealthCheck() check() { return this.health.check([ // Database check () => this.db.pingCheck('database', { timeout: 3000 }), // Redis check () => this.redis.isHealthy('redis'), // Memory check (heap should not exceed 150MB) () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // Memory check (RSS should not exceed 300MB) () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024), ]); } @Get('live') liveness() { // Simple liveness probe for Kubernetes return { status: 'ok', timestamp: new Date().toISOString() }; } @Get('ready') @HealthCheck() readiness() { // Readiness probe - checks all dependencies return this.health.check([ () => this.db.pingCheck('database'), () => this.redis.isHealthy('redis'), ]); } } ``` --- ## 10. ROLLBACK PROCEDURES ### 10.1 Automatic Rollback **Trigger conditions:** - Health checks failing for >2 minutes - Error rate >5% for >5 minutes - Database migration failure - Critical exception thrown **File:** `scripts/rollback.sh` ```bash #!/bin/bash set -euo pipefail ROLLBACK_TO_VERSION=${1:-previous} echo "===== EMERGENCY ROLLBACK =====" echo "Rolling back to: $ROLLBACK_TO_VERSION" # Step 1: Identify current and previous versions CURRENT_VERSION=$(docker inspect erp-backend --format='{{.Config.Labels.version}}') echo "Current version: $CURRENT_VERSION" if [ "$ROLLBACK_TO_VERSION" = "previous" ]; then ROLLBACK_TO_VERSION=$(git describe --tags --abbrev=0 HEAD^) fi echo "Rollback to version: $ROLLBACK_TO_VERSION" # Step 2: Stop current containers echo "Stopping current containers..." docker-compose -f docker-compose.prod.yml stop backend frontend # Step 3: Restore database backup (if needed) read -p "Restore database backup? (y/N) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then echo "Restoring database..." LATEST_BACKUP=$(ls -t /backups/postgres/full_*.dump | head -1) ./scripts/restore-postgres.sh --backup=$LATEST_BACKUP --no-prompt fi # Step 4: Checkout previous version echo "Checking out version $ROLLBACK_TO_VERSION..." git fetch --tags git checkout tags/$ROLLBACK_TO_VERSION # Step 5: Start services with previous version echo "Starting services with version $ROLLBACK_TO_VERSION..." docker-compose -f docker-compose.prod.yml up -d backend frontend # Step 6: Verify health echo "Waiting for services to start..." sleep 30 ./scripts/health-check.sh if [ $? -eq 0 ]; then echo "===== ROLLBACK SUCCESSFUL =====" echo "Rolled back from $CURRENT_VERSION to $ROLLBACK_TO_VERSION" # Notify team curl -X POST https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ -H 'Content-Type: application/json' \ -d "{\"text\": \"🚨 EMERGENCY ROLLBACK: $CURRENT_VERSION → $ROLLBACK_TO_VERSION\"}" else echo "===== ROLLBACK FAILED =====" echo "Manual intervention required!" exit 1 fi ``` ### 10.2 Manual Rollback Steps ```bash # 1. Identify issue and decide to rollback # Check logs, monitoring dashboards, alerts # 2. Execute rollback script ./scripts/rollback.sh v1.1.0 # 3. If automated rollback fails, manual steps: # Stop services docker-compose -f docker-compose.prod.yml stop # Restore database from backup pg_restore -h localhost -U erp_user -d erp_generic /backups/postgres/full_20251124_020000.dump # Checkout previous stable version git checkout tags/v1.1.0 # Start services docker-compose -f docker-compose.prod.yml up -d # 4. Verify system is stable ./scripts/health-check.sh npm run test:smoke # 5. Communicate to stakeholders # Post-mortem within 48 hours ``` ### 10.3 Database Rollback **Forward-only migrations (recommended):** ```bash # Never use "prisma migrate reset" in production! # Instead, create a new migration to undo changes npx prisma migrate dev --name revert_feature_x # Example: If you added a column, create migration to drop it # migrations/20251124_revert_feature_x/migration.sql: ALTER TABLE core.products DROP COLUMN IF EXISTS new_field; ``` **Emergency database rollback (last resort):** ```bash # 1. Stop application docker-compose stop backend # 2. Create safety backup pg_dump -h localhost -U erp_user -Fc erp_generic > /backups/pre-rollback-$(date +%Y%m%d_%H%M%S).dump # 3. Restore from backup (point-in-time before bad deployment) pg_restore -h localhost -U erp_user -d erp_generic -c /backups/postgres/full_20251124_020000.dump # 4. Verify data integrity psql -h localhost -U erp_user -d erp_generic -c "SELECT COUNT(*) FROM auth.users;" # 5. Restart application docker-compose start backend ``` --- ## 11. TROUBLESHOOTING ### 11.1 Common Issues **Issue: Backend fails to start** ```bash # Check logs docker-compose logs backend # Common causes: # 1. Database connection failed docker-compose exec postgres pg_isready # Fix: Verify DATABASE_URL, check PostgreSQL is running # 2. Redis connection failed docker-compose exec redis redis-cli ping # Fix: Verify REDIS_URL, check Redis is running # 3. Missing environment variables docker-compose exec backend env | grep DATABASE_URL # Fix: Update .env file # 4. Port already in use sudo lsof -i :3000 # Fix: Stop conflicting process or change PORT ``` **Issue: Database migrations fail** ```bash # Check migration status npx prisma migrate status # View pending migrations npx prisma migrate resolve # Force migration (CAUTION: only in dev) npx prisma migrate resolve --applied "20251124_migration_name" # Production fix: Create compensating migration npx prisma migrate dev --name fix_migration_issue ``` **Issue: High memory usage** ```bash # Check container stats docker stats # If PostgreSQL using too much memory: # Reduce shared_buffers in postgresql.conf # Restart: docker-compose restart postgres # If Node.js using too much memory: # Add heap limit: NODE_OPTIONS="--max-old-space-size=2048" # Restart: docker-compose restart backend ``` **Issue: Slow API responses** ```bash # Check database query performance docker-compose exec postgres psql -U erp_user -d erp_generic # View slow queries SELECT query, mean_exec_time, calls FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; # View active connections SELECT count(*) FROM pg_stat_activity; # View locks SELECT * FROM pg_locks WHERE NOT granted; # Fix: Add indexes, optimize queries, increase resources ``` ### 11.2 Disaster Recovery Checklist **Database Corruption:** - [ ] Stop application immediately - [ ] Identify corruption scope (single table vs full database) - [ ] Restore from latest backup - [ ] Replay WAL logs for point-in-time recovery - [ ] Verify data integrity - [ ] Restart application **Complete System Failure:** - [ ] Assess damage (hardware failure, network outage, etc.) - [ ] Provision new infrastructure (if needed) - [ ] Restore database from backup (cloud or offsite) - [ ] Deploy latest stable version - [ ] Restore Redis data (if needed) - [ ] Verify system health - [ ] Resume operations - [ ] Post-mortem analysis **Security Breach:** - [ ] Isolate compromised system immediately - [ ] Rotate all credentials (database, API keys, JWT secrets) - [ ] Audit access logs - [ ] Patch vulnerability - [ ] Deploy patched version - [ ] Notify affected users (if data leaked) - [ ] Conduct full security audit --- ## 12. REFERENCES **Internal Documentation:** - [README.md](./README.md) - DevOps overview - [MONITORING-OBSERVABILITY.md](./MONITORING-OBSERVABILITY.md) - Monitoring setup - [BACKUP-RECOVERY.md](./BACKUP-RECOVERY.md) - Backup procedures - [SECURITY-HARDENING.md](./SECURITY-HARDENING.md) - Security hardening - [CI-CD-PIPELINE.md](./CI-CD-PIPELINE.md) - CI/CD pipeline - [Database Schemas](../02-modelado/database-design/schemas/) - Database DDL - [ADR-001: Stack Tecnológico](../adr/ADR-001-stack-tecnologico.md) - [ADR-003: Multi-Tenancy](../adr/ADR-003-multi-tenancy.md) **External Resources:** - [Docker Documentation](https://docs.docker.com/) - [Docker Compose Documentation](https://docs.docker.com/compose/) - [PostgreSQL 16 Documentation](https://www.postgresql.org/docs/16/) - [Redis Documentation](https://redis.io/documentation) - [NestJS Documentation](https://docs.nestjs.com/) - [Prisma Documentation](https://www.prisma.io/docs/) - [12-Factor App](https://12factor.net/) - [Blue-Green Deployment Pattern](https://martinfowler.com/bliki/BlueGreenDeployment.html) --- ## APPENDIX A: Quick Reference Commands ```bash # Health check ./scripts/health-check.sh # View logs docker-compose logs -f backend docker-compose logs -f frontend docker-compose logs -f postgres docker-compose logs -f redis # Database console docker-compose exec postgres psql -U erp_user -d erp_generic # Redis console docker-compose exec redis redis-cli -a $REDIS_PASSWORD # Run migrations docker-compose exec backend npx prisma migrate deploy # Backup database ./scripts/backup-postgres.sh # Restore database ./scripts/restore-postgres.sh --backup=full_20251124_020000.dump # Rollback deployment ./scripts/rollback.sh v1.1.0 # View container stats docker stats # Restart service docker-compose restart backend # Scale service docker-compose up -d --scale backend=5 ``` --- **Documento:** DEPLOYMENT-GUIDE.md **Versión:** 1.0 **Total Páginas:** ~18 **Última Actualización:** 2025-11-24 **Próxima Revisión:** Mensual