Skip to content
This repository was archived by the owner on Mar 24, 2026. It is now read-only.

Latest commit

 

History

History
359 lines (258 loc) · 8.66 KB

File metadata and controls

359 lines (258 loc) · 8.66 KB

Deployment Guide

This guide covers deploying Portal to a Hetzner VPS with Cloudflare as a reverse proxy.

Architecture

Users → Cloudflare (CDN/DDoS/SSL) → Hetzner VPS → Next.js App + PostgreSQL

Prerequisites

VPS Setup

  1. Hetzner VPS with:

    • Ubuntu 22.04 LTS or newer
    • Minimum 2 CPU cores, 4GB RAM (4 cores, 8GB recommended for production)
    • Docker and Docker Compose installed
    • SSH access configured
  2. Install Docker and Docker Compose:

    # Install Docker
    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
    
    # Install Docker Compose (plugin)
    sudo apt-get update
    sudo apt-get install docker-compose-plugin
    
    # Add user to docker group (replace $USER with your username)
    sudo usermod -aG docker $USER
  3. Create deployment directory:

    mkdir -p ~/portal
    cd ~/portal

Cloudflare Setup

  1. Add your domain to Cloudflare
  2. Point DNS to your VPS IP (A record)
  3. Enable proxy (orange cloud) for DDoS protection and CDN
  4. SSL/TLS mode: Set to "Full (strict)"
  5. Firewall rules: Optionally restrict to Cloudflare IPs only

GitHub Secrets

Configure the following secrets in your GitHub repository settings:

For both staging and production environments:

  • VPS_HOST: Your Hetzner VPS IP address or domain
  • VPS_USER: SSH username (e.g., deploy or root)
  • VPS_SSH_KEY: Private SSH key for authentication (generate with ssh-keygen -t ed25519)

Optional:

  • GITHUB_TOKEN: Automatically provided, but can be overridden if needed

Environment Variables

Staging Environment

Create .env.staging on your VPS:

# Database
DATABASE_URL=postgresql://postgres:postgres@portal-db-staging:5432/portal_staging
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=portal_staging

# BetterAuth
BETTER_AUTH_SECRET=your-staging-secret-key-here
BETTER_AUTH_URL=https://staging.atl.dev

# Sentry (optional)
NEXT_PUBLIC_SENTRY_DSN=your-sentry-dsn
SENTRY_ORG=your-org
SENTRY_PROJECT=portal
SENTRY_AUTH_TOKEN=your-token
SENTRY_RELEASE=staging

# Other integrations
# XMPP_DOMAIN=xmpp.atl.chat
# PROSODY_REST_URL=https://prosody.example.com

Production Environment

Create .env.production on your VPS:

# Database
DATABASE_URL=postgresql://postgres:your-secure-password@portal-db-production:5432/portal
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-secure-production-password
POSTGRES_DB=portal

# BetterAuth
BETTER_AUTH_SECRET=your-production-secret-key-here
BETTER_AUTH_URL=https://atl.dev

# Sentry (optional)
NEXT_PUBLIC_SENTRY_DSN=your-sentry-dsn
SENTRY_ORG=your-org
SENTRY_PROJECT=portal
SENTRY_AUTH_TOKEN=your-token
SENTRY_RELEASE=production

# Other integrations
# XMPP_DOMAIN=xmpp.atl.chat
# PROSODY_REST_URL=https://prosody.example.com

Security Notes:

  • Use strong, unique passwords for production
  • Generate secure secrets: openssl rand -base64 32
  • Never commit .env files to git
  • Store secrets securely (consider using a secrets manager)

Deployment Process

Automatic Deployment

  1. Staging: Automatically deploys on push to main branch
  2. Production: Manual deployment via GitHub Actions workflow dispatch

Manual Deployment

If you need to deploy manually:

# SSH into your VPS
ssh user@your-vps-ip

# Navigate to deployment directory
cd ~/portal

# Pull latest image
docker pull ghcr.io/allthingslinux/portal:staging-<commit-sha>

# Deploy (single compose.yaml; use profile for staging or production)
export GITHUB_REPOSITORY="allthingslinux/portal"
export GIT_COMMIT_SHA="<commit-sha>"
docker compose --profile staging up -d

# Or for production
docker compose --profile production up -d

Database Migrations

Migrations run automatically during production deployments. For manual migration:

# On VPS
docker compose --profile production exec portal-app-production pnpm db:migrate

Before production migrations:

  1. Backup database: docker compose --profile production exec portal-db-production pg_dump -U postgres portal > backup.sql
  2. Test migrations on staging first
  3. Review migration files in drizzle/ directory

Monitoring and Logs

View Logs

# Application logs
docker compose --profile production logs -f portal-app-production

# Database logs
docker compose --profile production logs -f portal-db-production

# All logs
docker compose --profile production logs -f

Health Checks

The application includes health check endpoints:

  • /api/health - Application health status

Check container health:

docker compose --profile production ps

Rollback

If deployment fails or you need to rollback:

# Stop current containers
docker compose --profile production down

# Pull previous image
docker pull ghcr.io/allthingslinux/portal:production-<previous-commit-sha>

# Restart with previous tag
export IMAGE_TAG=production-<previous-commit-sha>
docker compose --profile production up -d

Cloudflare Configuration

Recommended Settings

  1. SSL/TLS:

    • Encryption mode: Full (strict)
    • Always Use HTTPS: On
    • Minimum TLS Version: 1.2
  2. Caching:

    • Cache static assets (images, CSS, JS)
    • Bypass cache for API routes (/api/*)
    • Cache level: Standard
  3. Firewall Rules:

    • Optionally restrict access to Cloudflare IPs only
    • Rate limiting for API endpoints
  4. Page Rules (optional):

    • Cache static assets: *.atl.dev/_next/static/* → Cache Everything
    • Bypass API: *.atl.dev/api/* → Bypass Cache

Nginx/Caddy Reverse Proxy (Optional)

If you want an additional reverse proxy on the VPS:

# Nginx example
server {
    listen 80;
    server_name atl.dev;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

With Cloudflare in front, this is usually not necessary.

Troubleshooting

Container won't start

# Check logs
docker compose --profile production logs portal-app-production

# Check container status
docker compose --profile production ps

# Verify environment variables
docker compose --profile production exec portal-app-production env

Database connection issues

# Check database is running
docker compose --profile production ps portal-db-production

# Test connection
docker compose --profile production exec portal-db-production psql -U postgres -d portal

# Check database logs
docker compose --profile production logs portal-db-production

Port conflicts

Staging and production use different ports (see compose.yaml profiles):

  • Staging: App on 3001, DB on 5433
  • Production: App on 3000, DB on 5432

If you need to change ports, update compose.yaml for the relevant profile.

Out of disk space

# Clean up unused images
docker image prune -af

# Clean up old containers
docker container prune -f

# Remove old volumes (careful!)
docker volume prune -f

Security Best Practices

  1. Firewall: Only allow SSH (22) and HTTP/HTTPS (80/443) from Cloudflare IPs
  2. SSH: Use key-based authentication, disable password login
  3. Docker: Run containers as non-root user (already configured)
  4. Secrets: Never commit secrets, use environment variables
  5. Updates: Keep VPS and Docker images updated
  6. Backups: Regular database backups
  7. Monitoring: Set up alerts for container failures

Backup Strategy

Database Backups

# Create backup
docker compose --profile production exec portal-db-production \
  pg_dump -U postgres portal > backup-$(date +%Y%m%d-%H%M%S).sql

# Restore backup
docker compose --profile production exec -T portal-db-production \
  psql -U postgres portal < backup-20240126-120000.sql

Automated Backups

Set up a cron job for daily backups:

# Add to crontab (crontab -e)
0 2 * * * cd ~/portal && docker compose --profile production exec -T portal-db-production pg_dump -U postgres portal > /backups/portal-$(date +\%Y\%m\%d).sql

Scaling

For higher traffic, consider:

  1. Horizontal scaling: Multiple app containers behind a load balancer
  2. Database: Managed PostgreSQL (Hetzner Database, AWS RDS, etc.)
  3. Caching: Redis for session storage
  4. CDN: Cloudflare already provides this

Update compose.yaml (e.g. portal-app-production under the production profile) to add more app replicas:

portal-app-production:
  deploy:
    replicas: 3

Support

For issues or questions:

  • Check logs first
  • Review GitHub Actions workflow runs
  • Check Cloudflare analytics for traffic patterns
  • Review Sentry for error tracking