Skip to content

Deployment

Prerequisites

  • Node.js 20.x or later
  • pnpm 9.x or later
  • PostgreSQL 17
  • Nginx (for production reverse proxy, TLS, and admin Basic Auth)
  • PM2 (for production process management)

Quick Start with Docker Compose

For local development, Docker Compose provides a PostgreSQL instance:

bash
# Start PostgreSQL
cd infra
docker compose up -d

# Install dependencies
cd ..
pnpm install

# Build shared package
pnpm --filter @pastebox/shared build

# Push database schema
export DATABASE_URL="postgres://pastebox:pastebox@localhost:5432/pastebox"
pnpm db:push

# Generate a MASTER_KEY (32 random bytes, base64url)
export MASTER_KEY=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')

# Start API server
pnpm dev

# In a separate terminal, start workers
pnpm dev:worker

The API server starts on http://localhost:3000 by default.

Production Deployment with Ansible

The infra/ansible/ directory contains a complete Ansible playbook that provisions a production server:

bash
cd infra/ansible

# Create an Ansible vault for secrets
ansible-vault create group_vars/vault.yml
# Define: vault_server_ip, vault_ssh_user, vault_pg_password,
#         vault_master_key, vault_htpasswd_user, vault_htpasswd_password
#         Optionally: vault_turnstile_secret_key, vault_telegram_bot_token

# Run the playbook
ansible-playbook -i inventory/hosts.yml site.yml --ask-vault-pass

Roles executed in order:

  1. common -- System packages, firewall, user setup
  2. node -- Node.js 20 installation
  3. postgres -- PostgreSQL 17 installation and database setup
  4. app -- Clone repo, install dependencies, build, create .env, run migrations, configure PM2
  5. nginx -- Install Nginx, deploy config, set up htpasswd for admin
  6. certbot -- Install certbot, obtain certificate, configure renewal hook
  7. backup -- Set up restic repository and systemd timer for daily backups

After deployment, the playbook verifies /healthz and /readyz endpoints.

Manual Deployment

1. Clone and build

bash
git clone <repo> /opt/pastebox
cd /opt/pastebox
pnpm install --frozen-lockfile
pnpm build

2. Create .env file

bash
cat > /opt/pastebox/.env << 'EOF'
NODE_ENV=production
HOST=0.0.0.0
PORT=3000
BASE_URL=https://paste.example.com

DATABASE_URL=postgres://pastebox:<password>@localhost:5432/pastebox

MASTER_KEY=<32-bytes-base64url>

RATE_LIMIT_ANON=5
RATE_LIMIT_AUTH=60

TURNSTILE_ENABLED=false
TURNSTILE_SECRET_KEY=

TELEGRAM_BOT_TOKEN=

WORKER_POLL_INTERVAL_MS=5000
WORKER_WEBHOOK_MAX_RETRIES=5
WORKER_TTL_CLEANUP_INTERVAL_MS=60000
EOF

chmod 600 /opt/pastebox/.env

3. Run database migrations

bash
cd /opt/pastebox
export $(cat .env | xargs)
pnpm db:migrate

4. Start with PM2

bash
pm2 start infra/pm2/ecosystem.config.js
pm2 save
pm2 startup  # generates systemd unit for auto-start on boot

Environment Variables Reference

VariableRequiredDefaultDescription
NODE_ENVNodevelopmentdevelopment, production, or test
HOSTNo0.0.0.0Bind address
PORTNo3000Bind port
BASE_URLNohttp://localhost:3000Public-facing URL (used for generating paste URLs)
DATABASE_URLYes--PostgreSQL connection string
MASTER_KEYYes--32-byte key, base64url-encoded (for envelope encryption)
RATE_LIMIT_ANONNo5Max requests/minute for anonymous users
RATE_LIMIT_AUTHNo60Max requests/minute for authenticated users
TURNSTILE_ENABLEDNofalseEnable Cloudflare Turnstile CAPTCHA
TURNSTILE_SECRET_KEYNo""Turnstile secret key
TELEGRAM_BOT_TOKENNo""Telegram Bot API token (for notifications)
WORKER_POLL_INTERVAL_MSNo5000Webhook/Telegram worker poll interval in ms
WORKER_WEBHOOK_MAX_RETRIESNo5Max delivery attempts before marking failed
WORKER_TTL_CLEANUP_INTERVAL_MSNo60000TTL cleanup worker poll interval in ms

Generate a MASTER_KEY:

bash
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='

PM2 Setup

The PM2 ecosystem config is at infra/pm2/ecosystem.config.js and defines two processes:

ProcessModeInstancesMemory LimitDescription
pastebox-apicluster2512 MBFastify HTTP server
pastebox-workerfork1256 MBBackground workers (webhook delivery, Telegram, TTL cleanup)

Both processes read from /opt/pastebox/.env and log to /var/log/pastebox/.

bash
# Start
pm2 start infra/pm2/ecosystem.config.js

# Reload API with zero downtime
pm2 reload pastebox-api

# View logs
pm2 logs pastebox-api
pm2 logs pastebox-worker

# Monitor
pm2 monit

Nginx Configuration

The production Nginx config is at infra/nginx/pastebox.conf. Key features:

  • HTTP-to-HTTPS redirect
  • TLS 1.2/1.3 with Let's Encrypt certificates
  • Security headers (CSP, HSTS, X-Frame-Options, etc.)
  • Basic Auth on /admin via htpasswd file
  • Reverse proxy to 127.0.0.1:3000 with keepalive connections
  • client_max_body_size 10m
  • Static asset caching (7-day cache with Cache-Control: immutable)

Install:

bash
# Copy config
sudo cp infra/nginx/pastebox.conf /etc/nginx/sites-available/pastebox.conf
sudo ln -s /etc/nginx/sites-available/pastebox.conf /etc/nginx/sites-enabled/

# Create htpasswd for admin
sudo mkdir -p /etc/nginx/htpasswd
sudo htpasswd -c /etc/nginx/htpasswd/pastebox <admin-username>

# Test and reload
sudo nginx -t
sudo systemctl reload nginx

Update server_name and ssl_certificate/ssl_certificate_key paths to match your domain.

SSL with Certbot

bash
# Install certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate (with Nginx plugin)
sudo certbot --nginx -d paste.example.com --email admin@example.com --agree-tos

# Or standalone (if Nginx is not yet configured)
sudo certbot certonly --webroot -w /var/www/certbot -d paste.example.com

# Auto-renewal is set up by default via systemd timer
sudo systemctl status certbot.timer

A post-renewal hook at infra/certbot/renew-hook.sh validates and reloads Nginx after certificate renewal:

bash
sudo cp infra/certbot/renew-hook.sh /etc/letsencrypt/renewal-hooks/deploy/
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/renew-hook.sh

Backup Setup with Restic

Backups are handled by restic with a systemd timer that runs daily at 03:00 UTC.

What is backed up

  • PostgreSQL dump (pg_dump in custom format)
  • /opt/pastebox/.env (contains MASTER_KEY -- essential for decrypting Mode A pastes)

Retention policy

  • 7 daily snapshots
  • 4 weekly snapshots
  • 6 monthly snapshots

Setup

bash
# Initialize restic repository
sudo -u pastebox restic init --repo /var/backups/pastebox

# Save the restic password
echo '<restic-password>' | sudo -u pastebox tee /opt/pastebox/.restic-password
sudo chmod 600 /opt/pastebox/.restic-password

# Install systemd units
sudo cp infra/restic/pastebox-backup.service /etc/systemd/system/
sudo cp infra/restic/pastebox-backup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pastebox-backup.timer

# Verify timer
sudo systemctl list-timers | grep pastebox

# Manual backup test
sudo systemctl start pastebox-backup.service
sudo journalctl -u pastebox-backup.service -f

Restore

bash
# List snapshots
restic -r /var/backups/pastebox snapshots --tag pastebox

# Restore latest snapshot to /tmp/restore
restic -r /var/backups/pastebox restore latest --tag pastebox --target /tmp/restore

# Restore database
pg_restore -U pastebox -d pastebox -c /tmp/restore/tmp/pastebox-pgdump/pastebox.dump

# Restore .env if needed
cp /tmp/restore/opt/pastebox/.env /opt/pastebox/.env

Self-hosted paste service with E2EE