diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5709869 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,54 @@ +# Git +.git +.gitignore +.gitattributes + +# Node +node_modules +npm-debug.log +pnpm-debug.log +yarn-error.log +.npm +.eslintcache + +# Build +dist +build +.next +.turbo +.turboignored + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode +.idea +.DS_Store +*.swp +*.swo +*~ + +# Testing +coverage +.jest +.nyc_output + +# CI/CD +.github +.gitlab-ci.yml + +# Package managers +.pnpj +.pnp.js + +# Misc +*.log +*.pem +*.key +*.pub +.cache +tmp +temp diff --git a/.github/.copilot-instructions.md b/.github/.copilot-instructions.md index f22a6aa..528635b 100644 --- a/.github/.copilot-instructions.md +++ b/.github/.copilot-instructions.md @@ -30,12 +30,21 @@ Produce production-grade changes that are correct, verifiable, maintainable, and - Jest for unit/integration tests - TypeScript strict mode enabled -### Package Manager -- **Primary**: pnpm +### Runtime & Package Manager +- **Node.js**: 24.x (see `.nvmrc`) +- **Package Manager**: pnpm@10.30.3 - **Commands**: `pnpm dev`, `pnpm test`, `pnpm lint`, `pnpm build` - **Monorepo Command**: `pnpm --filter ` - **Turbo Commands**: `turbo run ` (respects pnpm workspaces) +### Production Deployment +- **Strategy**: Self-managed VPS with Docker Compose +- **Database**: PostgreSQL 17 +- **Cache**: Redis 7 +- **Reverse Proxy**: Nginx with SSL/TLS +- **SSL**: Let's Encrypt with auto-renewal +- **See**: [DEPLOY_VPS.md](DEPLOY_VPS.md) for complete deployment guide + --- ## 2. Rule Priority and Conflict Resolution diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/DEPLOY_VPS.md b/DEPLOY_VPS.md new file mode 100644 index 0000000..a37790f --- /dev/null +++ b/DEPLOY_VPS.md @@ -0,0 +1,467 @@ +# VPS Deployment Guide - blog-nest Monorepo + +**Node Version**: 24.x (see `.nvmrc`) +**Docker Command**: `docker compose` (not `docker-compose`) + +--- + +## Prerequisites + +### On Your VPS (Ubuntu 24.04 LTS) + +1. **Install Docker & Docker Compose** + ```bash + # Install Docker + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + + # Add your user to docker group + sudo usermod -aG docker $USER + newgrp docker + + # Verify Docker version (should be 20.10+) + docker --version + docker compose version + ``` + +2. **Install Certbot (for HTTPS/SSL)** + ```bash + sudo apt-get update + sudo apt-get install -y certbot + ``` + +3. **Install fail2ban (security)** + ```bash + sudo apt-get install -y fail2ban + sudo systemctl enable fail2ban + sudo systemctl start fail2ban + ``` + +4. **Create deploy user** + ```bash + sudo useradd -m -s /bin/bash deploy + sudo usermod -aG docker deploy + ``` + +--- + +## Setup Steps + +### 1. Clone Repository + +```bash +sudo -u deploy bash << 'EOF' +cd /home/deploy +git clone https://github.com/YOUR_USERNAME/blog-nest.git +cd blog-nest +EOF +``` + +All deployment files are in the `infra/` subdirectory. + +### 2. Create Environment Files + +```bash +# Create .env in infra directory +sudo tee /home/deploy/blog-nest/infra/.env > /dev/null << 'EOF' +# Database +DB_USER=blog_user +DB_PASSWORD=your_secure_password_here +DB_NAME=blog_production + +# Redis +REDIS_PASSWORD=your_redis_secure_password_here + +# Frontend +NEXT_PUBLIC_API_URL=https://yourdomain.com/api + +# Logging +LOG_LEVEL=info +EOF + +# Restrict permissions +sudo chmod 600 /home/deploy/blog-nest/infra/.env +sudo chown deploy:deploy /home/deploy/blog-nest/infra/.env +``` + +### 3. Generate SSL Certificates + +```bash +# Using Let's Encrypt +sudo certbot certonly --standalone \ + -d yourdomain.com \ + -d www.yourdomain.com \ + --email your-email@example.com \ + --agree-tos \ + --no-eff-email + +# Copy to infra directory +sudo mkdir -p /home/deploy/blog-nest/infra/ssl +sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /home/deploy/blog-nest/infra/ssl/cert.pem +sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /home/deploy/blog-nest/infra/ssl/key.pem +sudo chown -R deploy:deploy /home/deploy/blog-nest/infra/ssl +sudo chmod 600 /home/deploy/blog-nest/infra/ssl/key.pem +``` + +### 4. Update Nginx Configuration + +Edit `/home/deploy/blog-nest/infra/nginx.conf`: + +```bash +# Update the server_name and SSL paths +sed -i 's/server_name _/server_name yourdomain.com www.yourdomain.com/' /home/deploy/blog-nest/infra/nginx.conf +``` + +### 5. Build and Start Services + +```bash +cd /home/deploy/blog-nest/infra + +# Build images +docker compose -f docker-compose.prod.yml build + +# Start services +docker compose -f docker-compose.prod.yml up -d + +# Check status +docker compose -f docker-compose.prod.yml ps +``` + +### 6. Setup SSL Auto-Renewal + +```bash +# Create renewal hook +sudo tee /etc/letsencrypt/renewal-hooks/post/docker-restart.sh > /dev/null << 'EOF' +#!/bin/bash +cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /home/deploy/blog-nest/deploy/ssl/cert.pem +cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /home/deploy/blog-nest/deploy/ssl/key.pem +chown deploy:deploy /home/deploy/blog-nest/deploy/ssl/* +cd /home/deploy/blog-nest/deploy +docker compose -f docker-compose.prod.yml restart nginx +EOF + +sudo chmod +x /etc/letsencrypt/renewal-hooks/post/docker-restart.sh + +# Test renewal +sudo certbot renew --dry-run +``` + +### 7. Setup Systemd Service (Optional but Recommended) + +```bash +sudo tee /etc/systemd/system/blog-nest.service > /dev/null << 'EOF' +[Unit] +Description=blog-nest Docker Compose +After=docker.service +Requires=docker.service + +[Service] +Type=forking +User=deploy +WorkingDirectory=/home/deploy/blog-nest/infra +ExecStart=/usr/bin/docker compose -f docker-compose.prod.yml up -d +ExecStop=/usr/bin/docker compose -f docker-compose.prod.yml down +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable blog-nest +sudo systemctl start blog-nest +``` + +--- + +## Deployment Workflow + +### Initial Deploy +```bash +cd /home/deploy/blog-nest/infra +git pull origin main +docker compose -f docker-compose.prod.yml build +docker compose -f docker-compose.prod.yml up -d +docker compose -f docker-compose.prod.yml logs -f +``` + +### Deploy Updates +```bash +cd /home/deploy/blog-nest/infra +git pull origin main +docker compose -f docker-compose.prod.yml build --no-cache +docker compose -f docker-compose.prod.yml up -d api frontend +docker compose -f docker-compose.prod.yml logs -f +``` + +### Check Health +```bash +cd /home/deploy/blog-nest/infra + +# Service status +docker compose -f docker-compose.prod.yml ps + +# View logs +docker compose -f docker-compose.prod.yml logs api +docker compose -f docker-compose.prod.yml logs frontend +docker compose -f docker-compose.prod.yml logs nginx + +# Check database connection +docker compose -f docker-compose.prod.yml exec postgres \ + psql -U blog_user -d blog_production -c "SELECT 1;" + +# Check Redis connection +docker compose -f docker-compose.prod.yml exec redis \ + redis-cli -a $REDIS_PASSWORD ping +``` + +--- + +## Monitoring & Maintenance + +### Logs Rotation +```bash +# Docker logs are auto-rotated; configure in docker-compose.prod.yml if needed +docker compose -f docker-compose.prod.yml restart +``` + +### Database Backups + +```bash +# Daily backup script +sudo tee /usr/local/bin/backup-blog-db.sh > /dev/null << 'EOF' +#!/bin/bash +BACKUP_DIR="/backups/blog-nest" +mkdir -p $BACKUP_DIR + +docker compose -f /home/deploy/blog-nest/infra/docker-compose.prod.yml exec -T postgres \ + pg_dump -U blog_user blog_production | gzip > "$BACKUP_DIR/db_$(date +%Y%m%d_%H%M%S).sql.gz" + +# Keep only last 7 days +find $BACKUP_DIR -name "db_*.sql.gz" -mtime +7 -delete +EOF + +sudo chmod +x /usr/local/bin/backup-blog-db.sh + +# Add to crontab +sudo tee /etc/cron.d/blog-backup > /dev/null << 'EOF' +0 2 * * * root /usr/local/bin/backup-blog-db.sh +EOF +``` + +### Monitor Resource Usage +```bash +# Real-time stats +docker stats + +# Check disk usage +docker system df + +# Prune unused images/volumes +docker system prune -a --volumes +``` + +### Update Node Version + +If you need to update Node (e.g., from 24 to 25): + +1. Update `.nvmrc`: + ```bash + echo "25" > .nvmrc + ``` + +2. Update `infra/Dockerfile`: + ```bash + sed -i 's/node:24-alpine/node:25-alpine/g' infra/Dockerfile + ``` + +3. Rebuild and deploy: + ```bash + cd infra + docker compose -f docker-compose.prod.yml build --no-cache + docker compose -f docker-compose.prod.yml up -d + ``` + +--- + +## Troubleshooting + +### Services won't start +```bash +# Check logs +docker compose -f docker-compose.prod.yml logs + +# Check if ports are in use +sudo netstat -ltnp | grep -E ':(80|443|3000|3001|5432|6379)' + +# Kill conflicting processes +sudo lsof -i :80 +sudo kill -9 +``` + +### Database connection fails +```bash +# Check database is running +docker compose -f docker-compose.prod.yml ps postgres + +# Check DATABASE_URL in .env +cat /home/deploy/blog-nest/deploy/.env | grep DATABASE_URL + +# Test connection +docker compose -f docker-compose.prod.yml exec postgres \ + psql -U blog_user -d blog_production -c "\conninfo" +``` + +### High memory usage +```bash +# Check which container +docker stats + +# Reduce Node memory (in docker-compose.prod.yml): +# Add: environment: NODE_OPTIONS=--max-old-space-size=1024 + +# Restart container +docker compose -f docker-compose.prod.yml restart api +``` + +### SSL certificate issues +```bash +# Check cert status +sudo certbot certificates + +# Manually renew +sudo certbot renew --force-renewal --quiet + +# Check expiration +openssl s_client -connect yourdomain.com:443 < /dev/null | grep -A1 "Validity" +``` + +--- + +## Security Best Practices + +1. **SSH Hardening** + ```bash + # Edit /etc/ssh/sshd_config + PermitRootLogin no + PasswordAuthentication no + PubkeyAuthentication yes + Port 2222 # Change default port + ``` + +2. **Firewall Rules** (UFW) + ```bash + sudo ufw default deny incoming + sudo ufw default allow outgoing + sudo ufw allow 22/tcp # SSH + sudo ufw allow 80/tcp # HTTP + sudo ufw allow 443/tcp # HTTPS + sudo ufw enable + ``` + +3. **Regular Updates** + ```bash + sudo apt-get update + sudo apt-get upgrade -y + sudo apt-get autoremove -y + ``` + +4. **Secrets Management** + - Never commit `.env` to git + - Use strong passwords (20+ chars, mixed case, symbols) + - Rotate passwords quarterly + - Store backups in secure, offsite location + +--- + +## Performance Tuning + +### Nginx Caching +Already configured in `deploy/nginx.conf`: +- Static assets cached for 1 year +- API responses cached for 5 minutes +- Rate limiting: 100r/s for API, 50r/s for web + +### Database Optimization +```bash +# Connect to database +docker compose -f docker-compose.prod.yml exec postgres psql -U blog_user -d blog_production + +# Analyze query performance +EXPLAIN ANALYZE SELECT * FROM users; + +# Create indexes if needed +CREATE INDEX idx_users_email ON users(email); +``` + +### Redis Optimization +```bash +# Monitor Redis +docker compose -f docker-compose.prod.yml exec redis redis-cli MONITOR + +# Check memory usage +docker compose -f docker-compose.prod.yml exec redis redis-cli INFO memory +``` + +--- + +## Rollback Strategy + +### Quick Rollback +```bash +cd /home/deploy/blog-nest/deploy + +# Revert code +git revert + +# Rebuild and redeploy +docker compose -f docker-compose.prod.yml build +docker compose -f docker-compose.prod.yml up -d +``` + +### Database Rollback +```bash +# Restore from backup +gunzip < /backups/blog-nest/db_20260306_120000.sql.gz | \ + docker compose -f docker-compose.prod.yml exec -T postgres \ + psql -U blog_user blog_production +``` + +--- + +## Useful Commands + +```bash +# Enter service shell +docker compose -f docker-compose.prod.yml exec api sh +docker compose -f docker-compose.prod.yml exec frontend sh + +# Run migrations (if using Prisma) +docker compose -f docker-compose.prod.yml exec api \ + npx prisma migrate deploy + +# Access database directly +docker compose -f docker-compose.prod.yml exec postgres \ + psql -U blog_user -d blog_production + +# Check API health +curl https://yourdomain.com/api/health + +# View real-time logs +docker compose -f docker-compose.prod.yml logs -f --tail=50 +``` + +--- + +## Additional Resources + +- [Docker Compose Docs](https://docs.docker.com/compose/) +- [Nginx Reverse Proxy](https://nginx.org/en/docs/) +- [Let's Encrypt](https://letsencrypt.org/) +- [UFW Firewall](https://help.ubuntu.com/community/UFW) +- [Node.js Best Practices](https://nodejs.org/en/docs/guides/nodejs-performance/) + +--- + +**Last Updated**: Thursday, March 6, 2026 diff --git a/infra/Dockerfile b/infra/Dockerfile new file mode 100644 index 0000000..8c603db --- /dev/null +++ b/infra/Dockerfile @@ -0,0 +1,70 @@ +# Multi-stage Dockerfile for blog-nest monorepo +# Builds: backend (NestJS), frontend (Next.js), and shares dependencies +# Location: deploy/Dockerfile - build from root with: docker build -f deploy/Dockerfile . + +# Stage 1: Dependencies +FROM node:24-alpine AS deps +WORKDIR /app +RUN npm install -g pnpm@10.30.3 + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +RUN pnpm install --frozen-lockfile + +# Stage 2: Build Backend +FROM node:24-alpine AS backend-build +WORKDIR /app +RUN npm install -g pnpm@10.30.3 + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/api ./apps/api +COPY tsconfig.json ./ + +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @dpep/api run build + +# Stage 3: Backend Runtime +FROM node:24-alpine AS backend-prod +WORKDIR /app +RUN npm install -g pnpm@10.30.3 + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/api/package.json ./apps/api/ + +RUN pnpm install --prod --frozen-lockfile + +COPY --from=backend-build /app/apps/api/dist ./apps/api/dist +COPY --from=backend-build /app/apps/api/package.json ./apps/api/ + +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["node", "apps/api/dist/main.js"] + +# Stage 4: Build Frontend +FROM node:24-alpine AS frontend-build +WORKDIR /app +RUN npm install -g pnpm@10.30.3 + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/frontend ./apps/frontend +COPY tsconfig.json ./ + +RUN pnpm install --frozen-lockfile +RUN pnpm --filter frontend run build + +# Stage 5: Frontend Runtime +FROM node:24-alpine AS frontend-prod +WORKDIR /app +RUN npm install -g pnpm@10.30.3 + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY apps/frontend/package.json ./apps/frontend/ + +RUN pnpm install --prod --frozen-lockfile + +COPY --from=frontend-build /app/apps/frontend/.next ./apps/frontend/.next +COPY --from=frontend-build /app/apps/frontend/public ./apps/frontend/public +COPY apps/frontend/package.json ./apps/frontend/ + +EXPOSE 3001 +ENV NODE_ENV=production +CMD ["pnpm", "--filter", "frontend", "run", "start"] diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..6745612 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,148 @@ +# Infrastructure Directory + +This directory contains all production and development infrastructure configurations for the blog-nest monorepo. + +## Files + +### `Dockerfile` +Multi-stage Docker build configuration for backend (NestJS) and frontend (Next.js). +- **Base Image**: `node:24-alpine` +- **Stages**: backend-build → backend-prod, frontend-build → frontend-prod + +**Build from root:** +```bash +docker build -f infra/Dockerfile -t blog-nest:latest . +``` + +### `docker-compose.prod.yml` +Production Docker Compose orchestration with PostgreSQL, Redis, Nginx, and both services. + +**Usage:** +```bash +cd deploy +docker compose -f docker-compose.prod.yml up -d +``` + +**Or from root:** +```bash +docker compose -f deploy/docker-compose.prod.yml up -d +``` + +### `docker-compose.yml` +Development Docker Compose setup with hot-reload volumes and exposed ports. + +**Usage:** +```bash +cd deploy +docker compose up -d +``` + +### `nginx.conf` +Nginx reverse proxy configuration with: +- SSL/TLS support (Let's Encrypt ready) +- Rate limiting (API: 100r/s, Web: 50r/s) +- Gzip compression +- Security headers +- Caching strategies +- Health check endpoint + +## Quick Start (VPS) + +### 1. Copy files to VPS +```bash +scp -r infra/ user@vps:/home/deploy/blog-nest/ +``` + +### 2. Create environment file +```bash +# On VPS, in /home/deploy/blog-nest/infra/ +cp .env.example .env +# Edit .env with your values +``` + +### 3. Setup SSL certificates +```bash +# From infra directory +mkdir -p ssl + +# If using Let's Encrypt (run once) +sudo certbot certonly --standalone \ + -d yourdomain.com \ + -d www.yourdomain.com + +# Copy certs +sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem ./ssl/cert.pem +sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem ./ssl/key.pem +sudo chown deploy:deploy ./ssl/* +sudo chmod 600 ./ssl/key.pem +``` + +### 4. Start services +```bash +cd /home/deploy/blog-nest/infra +docker compose -f docker-compose.prod.yml up -d +``` + +### 5. Verify health +```bash +docker compose -f docker-compose.prod.yml ps +curl http://localhost/health +``` + +## Environment Variables + +Create `.env` file in this directory: + +```env +# Database +DB_USER=blog_user +DB_PASSWORD=your_secure_password_here +DB_NAME=blog_production + +# Redis +REDIS_PASSWORD=your_redis_secure_password_here + +# Frontend +NEXT_PUBLIC_API_URL=https://yourdomain.com/api + +# Logging +LOG_LEVEL=info +``` + +## Production Checklist + +- [ ] Ubuntu 24.04 LTS VPS deployed +- [ ] Docker & Docker Compose installed +- [ ] SSL certificates obtained (Let's Encrypt) +- [ ] `.env` file created with strong passwords +- [ ] Nginx config updated with your domain +- [ ] Database backups configured +- [ ] Firewall rules set (UFW) +- [ ] SSH hardened +- [ ] Services running and healthy +- [ ] DNS pointing to VPS IP +- [ ] Monitoring setup (optional) + +## Common Commands + +```bash +# View logs +docker compose -f docker-compose.prod.yml logs -f + +# Restart services +docker compose -f docker-compose.prod.yml restart + +# Stop all +docker compose -f docker-compose.prod.yml down + +# Database backup +docker compose -f docker-compose.prod.yml exec postgres \ + pg_dump -U blog_user blog_production | gzip > backup.sql.gz + +# SSH into container +docker compose -f docker-compose.prod.yml exec api sh +``` + +## See Also + +- [../DEPLOY_VPS.md](../DEPLOY_VPS.md) — Complete VPS deployment guide diff --git a/infra/docker-compose.prod.yml b/infra/docker-compose.prod.yml new file mode 100644 index 0000000..e4544c4 --- /dev/null +++ b/infra/docker-compose.prod.yml @@ -0,0 +1,131 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:17-alpine + container_name: blog_postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${DB_USER:-blog} + POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme} + POSTGRES_DB: ${DB_NAME:-blog_prod} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" # Only expose to localhost + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-blog}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - blog_network + + # Redis Cache + redis: + image: redis:7-alpine + container_name: blog_redis + restart: unless-stopped + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-changeme} + volumes: + - redis_data:/data + ports: + - "127.0.0.1:6379:6379" # Only expose to localhost + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - blog_network + + # NestJS Backend API + api: + build: + context: .. + dockerfile: deploy/Dockerfile + target: backend-prod + container_name: blog_api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3000 + DATABASE_URL: postgresql://${DB_USER:-blog}:${DB_PASSWORD:-changeme}@postgres:5432/${DB_NAME:-blog_prod} + REDIS_URL: redis://:${REDIS_PASSWORD:-changeme}@redis:6379 + API_PORT: 3000 + LOG_LEVEL: ${LOG_LEVEL:-info} + expose: + - "3000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - blog_network + volumes: + - /var/log/blog:/app/logs # Optional: mount logs to host + + # Next.js Frontend + frontend: + build: + context: .. + dockerfile: deploy/Dockerfile + target: frontend-prod + container_name: blog_frontend + restart: unless-stopped + environment: + NODE_ENV: production + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost/api} + expose: + - "3001" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - blog_network + + # Nginx Reverse Proxy + nginx: + image: nginx:alpine + container_name: blog_nginx + restart: unless-stopped + depends_on: + - api + - frontend + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + - nginx_cache:/var/cache/nginx + networks: + - blog_network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +networks: + blog_network: + driver: bridge + +volumes: + postgres_data: + driver: local + redis_data: + driver: local + nginx_cache: + driver: local diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml new file mode 100644 index 0000000..66837fd --- /dev/null +++ b/infra/docker-compose.yml @@ -0,0 +1,111 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:17-alpine + container_name: blog_postgres_dev + restart: unless-stopped + environment: + POSTGRES_USER: blog + POSTGRES_PASSWORD: postgres + POSTGRES_DB: blog_dev + volumes: + - postgres_data_dev:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U blog"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - blog_network + + # Redis Cache + redis: + image: redis:7-alpine + container_name: blog_redis_dev + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis_data_dev:/data + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - blog_network + + # NestJS Backend API + api: + build: + context: .. + dockerfile: deploy/Dockerfile + target: backend-prod + container_name: blog_api_dev + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: development + PORT: 3000 + DATABASE_URL: postgresql://blog:postgres@postgres:5432/blog_dev + REDIS_URL: redis://redis:6379 + API_PORT: 3000 + LOG_LEVEL: debug + ports: + - "3000:3000" + volumes: + - ../apps/api/src:/app/apps/api/src + networks: + - blog_network + + # Next.js Frontend + frontend: + build: + context: .. + dockerfile: deploy/Dockerfile + target: frontend-prod + container_name: blog_frontend_dev + restart: unless-stopped + environment: + NODE_ENV: development + NEXT_PUBLIC_API_URL: http://localhost:3000 + ports: + - "3001:3001" + volumes: + - ../apps/frontend/src:/app/apps/frontend/src + networks: + - blog_network + + # Nginx Reverse Proxy + nginx: + image: nginx:alpine + container_name: blog_nginx_dev + restart: unless-stopped + depends_on: + - api + - frontend + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - blog_network + +networks: + blog_network: + driver: bridge + +volumes: + postgres_data_dev: + driver: local + redis_data_dev: + driver: local diff --git a/infra/nginx.conf b/infra/nginx.conf new file mode 100644 index 0000000..3b6d2ae --- /dev/null +++ b/infra/nginx.conf @@ -0,0 +1,169 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 2048; + use epoll; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 20M; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1000; + gzip_proxied any; + gzip_types text/plain text/css text/xml text/javascript + application/x-javascript application/xml+rss + application/javascript application/json; + + # Cache + proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=blog_cache:10m max_size=1g inactive=60m; + proxy_cache_key "$scheme$request_method$host$request_uri"; + + # 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; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s; + limit_req_zone $binary_remote_addr zone=web_limit:10m rate=50r/s; + + # Upstream services + upstream api_backend { + least_conn; + server api:3000 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + upstream frontend_server { + least_conn; + server frontend:3001 max_fails=3 fail_timeout=30s; + keepalive 32; + } + + # Health check endpoint + server { + listen 8080; + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } + + # HTTP to HTTPS redirect + server { + listen 80; + server_name _; + + # Let's Encrypt ACME challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other traffic to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + # HTTPS server + server { + listen 443 ssl http2; + server_name _; + + # SSL certificates (replace with your domain) + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # SSL security + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_stapling on; + ssl_stapling_verify on; + + # Root location - frontend + location / { + limit_req zone=web_limit burst=20 nodelay; + proxy_pass http://frontend_server; + 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_redirect off; + proxy_buffering off; + } + + # Next.js static assets - cache aggressively + location /_next/static/ { + proxy_pass http://frontend_server; + proxy_cache blog_cache; + proxy_cache_valid 200 365d; + proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Public assets - cache for long time + location /public/ { + proxy_pass http://frontend_server; + proxy_cache blog_cache; + proxy_cache_valid 200 1d; + add_header Cache-Control "public, max-age=86400"; + } + + # API endpoints + location /api/ { + limit_req zone=api_limit burst=50 nodelay; + proxy_pass http://api_backend; + proxy_http_version 1.1; + 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_set_header Connection ""; + proxy_buffering off; + + # API caching (optional - exclude for dynamic content) + proxy_cache blog_cache; + proxy_cache_valid 200 5m; + proxy_cache_methods GET HEAD; + add_header X-Cache-Status $upstream_cache_status; + } + + # Health check endpoint (internal only) + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +}