mirror of
https://github.com/andwati/blog-nest.git
synced 2026-11-09 22:45:12 +00:00
feat: add Docker and Nginx configurations for production and development environments
This commit is contained in:
@@ -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
|
||||
@@ -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 <workspace-name> <task>`
|
||||
- **Turbo Commands**: `turbo run <task>` (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
|
||||
|
||||
+467
@@ -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 [email protected] \
|
||||
--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 <PID>
|
||||
```
|
||||
|
||||
### 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 <commit-hash>
|
||||
|
||||
# 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
|
||||
@@ -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 [email protected]
|
||||
|
||||
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 [email protected]
|
||||
|
||||
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 [email protected]
|
||||
|
||||
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 [email protected]
|
||||
|
||||
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 [email protected]
|
||||
|
||||
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"]
|
||||
+148
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user