Files
blog-nest/DEPLOY_VPS.md
T

10 KiB

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

    # 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)

    sudo apt-get update
    sudo apt-get install -y certbot
    
  3. Install fail2ban (security)

    sudo apt-get install -y fail2ban
    sudo systemctl enable fail2ban
    sudo systemctl start fail2ban
    
  4. Create deploy user

    sudo useradd -m -s /bin/bash deploy
    sudo usermod -aG docker deploy
    

Setup Steps

1. Clone Repository

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

# 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

# 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:

# 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

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

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

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

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

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

# Docker logs are auto-rotated; configure in docker-compose.prod.yml if needed
docker compose -f docker-compose.prod.yml restart

Database Backups

# 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

# 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:

    echo "25" > .nvmrc
    
  2. Update infra/Dockerfile:

    sed -i 's/node:24-alpine/node:25-alpine/g' infra/Dockerfile
    
  3. Rebuild and deploy:

    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

# 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

# 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

# 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

# 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

    # Edit /etc/ssh/sshd_config
    PermitRootLogin no
    PasswordAuthentication no
    PubkeyAuthentication yes
    Port 2222  # Change default port
    
  2. Firewall Rules (UFW)

    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

    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

# 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

# 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

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

# 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

# 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


Last Updated: Thursday, March 6, 2026