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:
# 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:workerThe 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:
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-passRoles executed in order:
- common -- System packages, firewall, user setup
- node -- Node.js 20 installation
- postgres -- PostgreSQL 17 installation and database setup
- app -- Clone repo, install dependencies, build, create
.env, run migrations, configure PM2 - nginx -- Install Nginx, deploy config, set up htpasswd for admin
- certbot -- Install certbot, obtain certificate, configure renewal hook
- 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
git clone <repo> /opt/pastebox
cd /opt/pastebox
pnpm install --frozen-lockfile
pnpm build2. Create .env file
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/.env3. Run database migrations
cd /opt/pastebox
export $(cat .env | xargs)
pnpm db:migrate4. Start with PM2
pm2 start infra/pm2/ecosystem.config.js
pm2 save
pm2 startup # generates systemd unit for auto-start on bootEnvironment Variables Reference
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV | No | development | development, production, or test |
HOST | No | 0.0.0.0 | Bind address |
PORT | No | 3000 | Bind port |
BASE_URL | No | http://localhost:3000 | Public-facing URL (used for generating paste URLs) |
DATABASE_URL | Yes | -- | PostgreSQL connection string |
MASTER_KEY | Yes | -- | 32-byte key, base64url-encoded (for envelope encryption) |
RATE_LIMIT_ANON | No | 5 | Max requests/minute for anonymous users |
RATE_LIMIT_AUTH | No | 60 | Max requests/minute for authenticated users |
TURNSTILE_ENABLED | No | false | Enable Cloudflare Turnstile CAPTCHA |
TURNSTILE_SECRET_KEY | No | "" | Turnstile secret key |
TELEGRAM_BOT_TOKEN | No | "" | Telegram Bot API token (for notifications) |
WORKER_POLL_INTERVAL_MS | No | 5000 | Webhook/Telegram worker poll interval in ms |
WORKER_WEBHOOK_MAX_RETRIES | No | 5 | Max delivery attempts before marking failed |
WORKER_TTL_CLEANUP_INTERVAL_MS | No | 60000 | TTL cleanup worker poll interval in ms |
Generate a MASTER_KEY:
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='PM2 Setup
The PM2 ecosystem config is at infra/pm2/ecosystem.config.js and defines two processes:
| Process | Mode | Instances | Memory Limit | Description |
|---|---|---|---|---|
pastebox-api | cluster | 2 | 512 MB | Fastify HTTP server |
pastebox-worker | fork | 1 | 256 MB | Background workers (webhook delivery, Telegram, TTL cleanup) |
Both processes read from /opt/pastebox/.env and log to /var/log/pastebox/.
# 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 monitNginx 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
/adminvia htpasswd file - Reverse proxy to
127.0.0.1:3000with keepalive connections client_max_body_size 10m- Static asset caching (7-day cache with
Cache-Control: immutable)
Install:
# 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 nginxUpdate server_name and ssl_certificate/ssl_certificate_key paths to match your domain.
SSL with Certbot
# 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.timerA post-renewal hook at infra/certbot/renew-hook.sh validates and reloads Nginx after certificate renewal:
sudo cp infra/certbot/renew-hook.sh /etc/letsencrypt/renewal-hooks/deploy/
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/renew-hook.shBackup 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_dumpin custom format) /opt/pastebox/.env(containsMASTER_KEY-- essential for decrypting Mode A pastes)
Retention policy
- 7 daily snapshots
- 4 weekly snapshots
- 6 monthly snapshots
Setup
# 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 -fRestore
# 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