Files
picpeak/DEPLOYMENT_GUIDE.md
T
paul 7c79052681
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m23s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m4s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Has been skipped
refactor: consolidate deployment documentation and cleanup repository
- Merge all deployment docs into single comprehensive DEPLOYMENT_GUIDE.md
- Add instructions for non-nginx deployment options
- Reference utility scripts in deployment guide
- Remove orphaned migrations folder at root level
- Remove redundant deployment documentation files
- Keep all utility scripts in scripts/ folder
- Update CLAUDE.md to reference new deployment guide

This provides a single source of truth for all deployment scenarios.
2025-07-24 20:54:50 +02:00

18 KiB

🚀 PicPeak Complete Deployment Guide

This comprehensive guide covers all deployment methods for PicPeak, including Docker, PM2, manual installation, and deployment without a reverse proxy.

📋 Table of Contents

Prerequisites

Basic Requirements

  • Linux server (Ubuntu 20.04+ or similar)
  • Domain name (for SSL certificates)
  • SMTP credentials for email notifications
  • Basic command line knowledge

Software Requirements (varies by method)

  • Docker method: Docker and Docker Compose
  • PM2 method: Node.js 18+, PostgreSQL 14+
  • Manual method: Node.js 18+, PostgreSQL 14+, nginx (optional)

🔐 Security Requirements

Critical: JWT Secret Setup

NEVER use the default JWT secret in production! The application will refuse to start if JWT_SECRET is not properly configured.

Generate a secure JWT secret:

# Generate a 64-character secret
openssl rand -base64 32

# Or for even more security (recommended)
openssl rand -base64 64

# Or use the included script
./scripts/generate-jwt-secret.sh

Critical: Database Password

Generate a strong database password:

openssl rand -base64 24

🚀 Quick Start (Docker)

The fastest way to deploy PicPeak in production:

# 1. Clone the repository
git clone https://github.com/the-luap/picpeak.git
cd picpeak

# 2. Use the automated install script (recommended)
sudo ./scripts/install.sh

# Or manually:
# 2. Copy production environment template
cp .env.production.example .env

# 3. Generate and add JWT secret
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env

# 4. Edit configuration
nano .env  # Update all required values

# 5. Create directories
mkdir -p storage/events/active storage/events/archived storage/thumbnails storage/uploads
mkdir -p data logs certbot/conf certbot/www

# 6. Deploy
docker-compose -f docker-compose.prod.yml up -d

# 7. Check logs
docker-compose -f docker-compose.prod.yml logs -f

📦 Deployment Methods

Step 1: Environment Configuration

Create .env file with all required variables:

# SECURITY - MUST CHANGE ALL!
JWT_SECRET=<your-64-character-secret-from-openssl>
DB_PASSWORD=<your-secure-database-password>

# Application URLs
FRONTEND_URL=https://your-domain.com
BACKEND_URL=https://your-domain.com
ADMIN_URL=https://your-domain.com

# Database (PostgreSQL for Docker)
DATABASE_CLIENT=pg
DB_HOST=db
DB_PORT=5432
DB_NAME=picpeak
DB_USER=picpeak

# Email (Example: SendGrid)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=apikey
SMTP_PASS=your-sendgrid-api-key
EMAIL_FROM=PicPeak <noreply@your-domain.com>

# Application
NODE_ENV=production
PORT=3001
LOG_LEVEL=info

Step 2: Docker Volume Permissions

Create docker-compose.override.yml for proper permissions:

version: '3.8'

services:
  backend:
    volumes:
      - ./storage:/app/storage:delegated
      - ./data:/app/data:delegated
      - ./logs:/app/logs:delegated
    user: "1001:1001"  # nodejs user

  db:
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

Step 3: Build and Deploy

# Set correct permissions
chmod -R 755 storage data logs

# Build images
docker-compose -f docker-compose.prod.yml build

# Start services
docker-compose -f docker-compose.prod.yml up -d

# Run database migrations
docker-compose -f docker-compose.prod.yml exec backend npm run migrate

# Admin credentials will be displayed and saved to ADMIN_CREDENTIALS.txt

Step 4: Configure Nginx

Update nginx/sites-enabled/default with your domain:

server {
    listen 80;
    server_name your-domain.com;

    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL configuration (managed by Certbot)
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    # Frontend
    location / {
        proxy_pass http://frontend:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # API proxy
    location /api {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Protected images
    location /photos {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
    }

    # Thumbnails
    location /thumbnails {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
    }

    # Public uploads
    location /uploads {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
    }
}

Method 2: PM2 (Node.js Process Manager)

Step 1: Install Dependencies

# Install Node.js 18+
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs

# Install PostgreSQL
sudo apt-get install -y postgresql postgresql-contrib

# Install PM2 globally
sudo npm install -g pm2

# Install nginx (if using reverse proxy)
sudo apt-get install -y nginx

Step 2: Setup Database

# Create database and user
sudo -u postgres psql
CREATE DATABASE picpeak;
CREATE USER picpeak WITH ENCRYPTED PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE picpeak TO picpeak;
\q

Step 3: Clone and Configure

# Clone repository
git clone https://github.com/the-luap/picpeak.git
cd picpeak

# Install dependencies
cd backend && npm install
cd ../frontend && npm install

# Configure environment
cd ..
cp .env.production.example .env
nano .env  # Update all values

Step 4: Build Frontend

cd frontend
npm run build
cd ..

Step 5: Start with PM2

cd backend

# Start application
pm2 start ecosystem.config.js

# Save PM2 configuration
pm2 save

# Setup startup script
pm2 startup

Step 6: Configure Nginx

Create /etc/nginx/sites-available/picpeak:

server {
    listen 80;
    server_name your-domain.com;

    # Frontend (static files)
    location / {
        root /path/to/picpeak/frontend/dist;
        try_files $uri $uri/ /index.html;
    }

    # API proxy
    location /api {
        proxy_pass http://localhost:3001;
        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;
    }

    # Protected photos
    location /photos {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
    }

    # Other proxied paths
    location ~ ^/(thumbnails|uploads) {
        proxy_pass http://localhost:3001;
        proxy_set_header Host $host;
    }
}

Enable the site:

sudo ln -s /etc/nginx/sites-available/picpeak /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Method 3: Manual Installation

Similar to PM2 method but using systemd instead:

Create Systemd Service

Create /etc/systemd/system/picpeak.service:

[Unit]
Description=PicPeak Photo Sharing
After=network.target

[Service]
Type=simple
User=picpeak
WorkingDirectory=/home/picpeak/picpeak/backend
ExecStart=/usr/bin/node server.js
Restart=on-failure
Environment="NODE_ENV=production"

[Install]
WantedBy=multi-user.target

Start the service:

sudo systemctl daemon-reload
sudo systemctl enable picpeak
sudo systemctl start picpeak

Method 4: Without Nginx (Direct Access)

For deployments without a reverse proxy:

Option A: Direct Backend Access

  1. Configure environment for direct access:
# .env
FRONTEND_URL=http://your-domain.com:5173
BACKEND_URL=http://your-domain.com:3001
ADMIN_URL=http://your-domain.com:5173

# Enable CORS for direct access
CORS_ENABLED=true
  1. Run backend directly:
cd backend
NODE_ENV=production node server.js
  1. Run frontend development server (not recommended for production):
cd frontend
VITE_API_URL=http://your-domain.com:3001/api npm run dev -- --host

Option B: Backend Serves Frontend

  1. Build frontend:
cd frontend
VITE_API_URL=/api npm run build
  1. Configure backend to serve frontend:
// Add to backend/server.js after API routes
if (process.env.NODE_ENV === 'production') {
  app.use(express.static(path.join(__dirname, '../frontend/dist')));
  app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
  });
}
  1. Access everything on backend port:
# Application available at http://your-domain.com:3001
NODE_ENV=production node server.js

Option C: Using Node.js HTTP Proxy

Create a simple proxy server:

// proxy-server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');

const app = express();

// Serve frontend static files
app.use(express.static(path.join(__dirname, 'frontend/dist')));

// Proxy API requests
app.use('/api', createProxyMiddleware({
  target: 'http://localhost:3001',
  changeOrigin: true
}));

// Proxy other backend routes
app.use(['/photos', '/thumbnails', '/uploads'], createProxyMiddleware({
  target: 'http://localhost:3001',
  changeOrigin: true
}));

// Catch all - serve frontend
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'frontend/dist/index.html'));
});

app.listen(80);

🔧 Environment Configuration

Required Environment Variables

Variable Description Example
JWT_SECRET CRITICAL - Authentication secret (min 32 chars) Use openssl rand -base64 32
DATABASE_CLIENT Database type pg for PostgreSQL, sqlite3 for SQLite
DB_HOST Database host localhost or db (Docker)
DB_PORT Database port 5432
DB_NAME Database name picpeak
DB_USER Database user picpeak
DB_PASSWORD Database password Strong password
SMTP_HOST Email server smtp.gmail.com
SMTP_PORT Email port 587
SMTP_USER Email username your-email@gmail.com
SMTP_PASS Email password App-specific password
EMAIL_FROM From address PicPeak <noreply@domain.com>
FRONTEND_URL Frontend URL https://your-domain.com
BACKEND_URL Backend URL https://your-domain.com

Optional Configuration

Variable Description Default
NODE_ENV Environment production
PORT Backend port 3001
LOG_LEVEL Logging level info
SESSION_TIMEOUT_MINUTES Session timeout 60
RATE_LIMIT_WINDOW_MS Rate limit window 900000 (15 min)
RATE_LIMIT_MAX_REQUESTS Max requests 100
DB_POOL_MIN Min DB connections 5
DB_POOL_MAX Max DB connections 25
DEFAULT_EXPIRATION_DAYS Gallery expiration 30
WARNING_DAYS_BEFORE_EXPIRY Warning period 7

Frontend Environment

For production builds:

# frontend/.env.production
VITE_API_URL=/api  # For reverse proxy
# or
VITE_API_URL=https://api.your-domain.com  # For direct access

👤 Admin Setup

Automatic Admin Creation

When you run migrations for the first time, an admin account is automatically created:

# Docker
docker-compose -f docker-compose.prod.yml exec backend npm run migrate

# PM2/Manual
cd backend && npm run migrate

Output:

========================================
✅ Admin user created successfully!
========================================
Username: admin
Password: SwiftEagle3847!

⚠️  IMPORTANT: Change password on first login
========================================

Important Admin Notes

  1. Credentials are saved to backend/ADMIN_CREDENTIALS.txt
  2. Must change password on first login (enforced)
  3. Password requirements:
    • Minimum 12 characters
    • Uppercase and lowercase letters
    • Numbers and special characters
    • Not a common password

Lost Admin Password

# Docker
docker-compose -f docker-compose.prod.yml exec backend node scripts/reset-admin-password.js

# PM2/Manual
cd backend && node scripts/reset-admin-password.js

🔒 SSL/HTTPS Configuration

Option 1: Let's Encrypt with Certbot

# Initial certificate
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
  --webroot --webroot-path=/var/www/certbot \
  -d your-domain.com -d www.your-domain.com

# Auto-renewal is handled by certbot container

Option 2: Using Traefik

Add to docker-compose.override.yml:

services:
  frontend:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.picpeak.rule=Host(`your-domain.com`)"
      - "traefik.http.routers.picpeak.entrypoints=websecure"
      - "traefik.http.routers.picpeak.tls.certresolver=letsencrypt"

Option 3: CloudFlare or Other CDN

  1. Set up your domain in CloudFlare
  2. Enable "Full SSL/TLS encryption mode"
  3. Use CloudFlare's origin certificates

🔧 Maintenance & Operations

Backup Procedures

Use the included backup script or create your own:

# Use the provided backup script
./scripts/backup.sh

# Or create custom backup script:
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="./backups/$DATE"

mkdir -p $BACKUP_DIR

# Database backup
docker-compose -f docker-compose.prod.yml exec -T db \
  pg_dump -U picpeak picpeak > $BACKUP_DIR/database.sql

# Files backup
tar -czf $BACKUP_DIR/storage.tar.gz storage/

echo "Backup completed: $BACKUP_DIR"

Automated Backups

The application includes a built-in backup service. Configure via Admin Panel:

  • Settings → Backup Configuration
  • Set schedule (cron expression)
  • Configure destination (local, rsync, S3)
  • Enable email notifications

Updates

# Docker method
git pull
docker-compose -f docker-compose.prod.yml build
docker-compose -f docker-compose.prod.yml up -d

# PM2 method
git pull
cd backend && npm install
cd ../frontend && npm install && npm run build
pm2 restart picpeak

Monitoring

Health Checks

# API health
curl https://your-domain.com/api/health

# Database connection
docker-compose -f docker-compose.prod.yml exec backend \
  psql -U picpeak -d picpeak -c "SELECT 1"

# Service status
docker-compose -f docker-compose.prod.yml ps

Logs

# Docker logs
docker-compose -f docker-compose.prod.yml logs -f

# PM2 logs
pm2 logs picpeak

# System logs
tail -f /var/log/nginx/error.log

🚨 Troubleshooting

Common Issues

JWT Secret Errors

Error: "Missing required environment variable: JWT_SECRET"

  • Solution: Set JWT_SECRET in your .env file
  • Generate: openssl rand -base64 32

Error: "JWT_SECRET is set to the insecure default value"

  • Solution: Change from default to secure value

Database Connection Failed

Error: "connect ECONNREFUSED"

  • Check: Database is running
  • Check: Correct host/port in .env
  • Docker: Use db as host, not localhost

Permission Errors

Error: "EACCES: permission denied"

# Fix Docker permissions
sudo chown -R 1001:1001 storage data logs

# Fix PM2/Manual permissions
sudo chown -R $USER:$USER storage data logs
chmod -R 755 storage

Email Not Sending

  • Check: SMTP credentials are correct
  • Gmail: Use app-specific password
  • Test: Admin Panel → Settings → Email → Test Email
  • Logs: Check email_queue table for errors

Photos Not Appearing

  • Check: File watcher is running
  • Permissions: chmod -R 755 storage/
  • Logs: grep watcher in backend logs

Frontend Can't Connect to Backend

  • CORS: Ensure FRONTEND_URL matches in backend .env
  • Proxy: Check nginx configuration
  • Direct: Set CORS_ENABLED=true for non-proxy setup

Debug Commands

# Check all services
docker-compose -f docker-compose.prod.yml ps

# Backend shell access
docker-compose -f docker-compose.prod.yml exec backend sh

# Database access
docker-compose -f docker-compose.prod.yml exec db psql -U picpeak

# Test API
curl -I http://localhost:3001/api/health

# Check disk space
df -h storage/

# View running processes
ps aux | grep node

Security Checklist

  • JWT_SECRET is randomly generated (min 32 chars)
  • Database password is strong and unique
  • Admin password changed from auto-generated
  • SSL/HTTPS enabled and working
  • Firewall configured (only 80/443 open)
  • File permissions set correctly (755 for storage)
  • Rate limiting enabled (default: 100 req/15min)
  • CORS properly configured
  • Environment files not in version control
  • Backups configured and tested
  • Monitoring alerts set up
  • Updates scheduled regularly
  • Access logs being monitored
  • Email using app-specific passwords
  • Umami analytics configured (optional)

📞 Support


Need help? Check the logs first, then open an issue with:

  • Deployment method used
  • Error messages
  • Relevant log output
  • Environment (without secrets)