Terminal.skills
Use Cases/Deploy a Full-Stack App to Production on Your Own Server

Deploy a Full-Stack App to Production on Your Own Server

Deploy a Next.js application with PostgreSQL, Redis, and background workers on a VPS using Dokploy for self-hosted PaaS management and Nixpacks for zero-config builds.

DevOps#self-hosted#paas#deployment#docker#vps
Works with:claude-codeopenai-codexgemini-clicursor
$

The Problem

Leo runs a bootstrapped SaaS and his Vercel bill just crossed $150/month for what's essentially a Next.js app, a Postgres database, and a background job runner. A $20/month VPS on Hetzner can handle the same workload, but he doesn't want to spend a week writing Docker Compose files and configuring Nginx. He needs the Heroku/Vercel developer experience on his own hardware.

The Solution

Use the skills listed above to implement an automated workflow. Install the required skills:

bash
npx terminal-skills install dokploy nixpacks

Step-by-Step Walkthrough

Step 1: Set Up Dokploy on the VPS

Dokploy turns any Linux server into a managed platform. One command installs the dashboard, reverse proxy (Traefik), and Docker orchestration.

bash
# SSH into the VPS (Hetzner CX31: 4 vCPU, 8GB RAM, €8.49/mo)
ssh root@88.99.xxx.xxx

# Install Dokploy (takes ~2 minutes)
curl -sSL https://dokploy.com/install.sh | sh

# Dashboard is now available at https://88.99.xxx.xxx:3000
# Set your admin password on first login

# Point your domain's DNS to the VPS
# A record: dokploy.myapp.com → 88.99.xxx.xxx
# A record: myapp.com → 88.99.xxx.xxx
# A record: *.myapp.com → 88.99.xxx.xxx

# In Dokploy dashboard → Settings → Server Domain:
# Set to dokploy.myapp.com
# Dokploy auto-provisions SSL via Let's Encrypt

Step 2: Provision the Database

Leo adds PostgreSQL through Dokploy's database management — no manual Docker commands or volume configuration.

yaml
# In Dokploy dashboard → Databases → Create:
# Type: PostgreSQL
# Version: 16
# Database Name: saas_production
# Username: saas_user
# Password: (auto-generated)
# Storage: Persistent volume mounted automatically

# Dokploy provides the connection string:
# postgresql://saas_user:xxx@dokploy-postgres-xxx:5432/saas_production
# This is an internal Docker network URL — only accessible by other Dokploy services

# Also create a Redis instance for session storage and job queues:
# Type: Redis
# Version: 7
# Internal URL: redis://dokploy-redis-xxx:6379

Step 3: Configure the Application Build

Leo's Next.js app needs zero Dockerfile configuration thanks to Nixpacks. He adds a minimal config to handle Prisma's code generation step.

toml
# nixpacks.toml — Build configuration for the Next.js app
# Nixpacks auto-detects Next.js from package.json
# We only need to add Prisma generation to the build phase

[phases.build]
cmds = [
  "npx prisma generate",           # Generate Prisma client from schema
  "npm run build",                  # Next.js production build
]

[start]
cmd = "npx prisma migrate deploy && npm start"  # Run migrations, then start

[variables]
NEXT_TELEMETRY_DISABLED = "1"       # Disable Next.js telemetry in build

The Nixpacks config is intentionally minimal. Nixpacks detects Node.js 20 from the .nvmrc file, runs npm ci automatically, and sets NODE_ENV=production. The only manual additions are Prisma-specific — the code generation and migration steps that Nixpacks can't infer.

Step 4: Deploy the Application

markdown
## In Dokploy Dashboard:

### Create the Web Application
1. Applications → Create → From GitHub
2. Repository: myorg/saas-app
3. Branch: main
4. Build Type: Nixpacks (auto-detected)
5. Port: 3000
6. Auto-deploy: Enabled (deploys on every push to main)

### Set Environment Variables
In the app's "Environment" tab:
bash
# Core application config
NODE_ENV=production
DATABASE_URL=postgresql://saas_user:xxx@dokploy-postgres-xxx:5432/saas_production
REDIS_URL=redis://dokploy-redis-xxx:6379
NEXTAUTH_SECRET=<auto-generated-by-dokploy>
NEXTAUTH_URL=https://myapp.com

# Third-party services
STRIPE_SECRET_KEY=sk_live_xxx
RESEND_API_KEY=re_xxx
SENTRY_DSN=https://xxx@sentry.io/xxx

# S3-compatible storage (MinIO on the same server, or external)
S3_BUCKET=uploads
S3_ENDPOINT=https://s3.myapp.com
S3_ACCESS_KEY=xxx
S3_SECRET_KEY=xxx
markdown
### Configure the Domain
1. Domains → Add Domain
2. Host: myapp.com
3. HTTPS: Enabled (Let's Encrypt auto-provisions)
4. Force HTTPS: Enabled

### Deploy
Click "Deploy" — Dokploy:
1. Pulls the code from GitHub
2. Builds with Nixpacks (cached layers for node_modules)
3. Runs health checks on port 3000
4. Switches traffic to the new container (zero-downtime)
5. Keeps the previous container for instant rollback

Step 5: Background Worker

The SaaS needs a background worker for email sending, webhook processing, and scheduled reports. It's the same codebase, different entry point.

typescript
// src/worker.ts — Background job processor
import { Worker } from "bullmq";
import { redis } from "./lib/redis";

const worker = new Worker(
  "default",
  async (job) => {
    switch (job.name) {
      case "send-email":
        await sendTransactionalEmail(job.data);
        break;
      case "process-webhook":
        await handleWebhook(job.data);
        break;
      case "generate-report":
        await generateMonthlyReport(job.data);
        break;
      default:
        throw new Error(`Unknown job: ${job.name}`);
    }
  },
  {
    connection: redis,
    concurrency: 5,            // Process 5 jobs in parallel
    limiter: {
      max: 100,                // Max 100 jobs per minute (email rate limit)
      duration: 60_000,
    },
  }
);

worker.on("completed", (job) => {
  console.log(`✅ Job ${job.name}#${job.id} completed`);
});

worker.on("failed", (job, err) => {
  console.error(`❌ Job ${job?.name}#${job?.id} failed:`, err.message);
});

console.log("🔄 Worker started, waiting for jobs...");
toml
# worker/nixpacks.toml — Worker build config (separate Dokploy service)
[phases.build]
cmds = ["npx prisma generate", "npm run build"]

[start]
cmd = "node dist/worker.js"

Deploy the worker as a separate Dokploy application from the same repository, but with a different Nixpacks config path and no public port.

Step 6: Automated Backups

bash
# scripts/backup.sh — Database backup script
#!/bin/bash
# Run via Dokploy's cron or system crontab

set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/postgres"
RETENTION_DAYS=14

mkdir -p "$BACKUP_DIR"

# Dump the database (Dokploy exposes the container name)
docker exec dokploy-postgres-xxx \
  pg_dump -U saas_user -d saas_production \
  --format=custom --compress=9 \
  > "$BACKUP_DIR/saas_${TIMESTAMP}.dump"

# Upload to S3 (optional)
aws s3 cp "$BACKUP_DIR/saas_${TIMESTAMP}.dump" \
  s3://myapp-backups/postgres/saas_${TIMESTAMP}.dump

# Clean old backups
find "$BACKUP_DIR" -name "*.dump" -mtime +$RETENTION_DAYS -delete

echo "✅ Backup complete: saas_${TIMESTAMP}.dump"
bash
# Add to system crontab on the VPS
# crontab -e
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

Real-World Example

Leo's migration from Vercel + Supabase + Railway to a single Hetzner VPS with Dokploy took one afternoon. The monthly cost dropped from $150 to $8.49 for the VPS plus $3 for S3 backups — a 92% reduction.

Deployment speed is comparable: Nixpacks builds take 45 seconds (cached) versus Vercel's ~30 seconds. The difference is negligible. Zero-downtime deployments work through Dokploy's container swapping — the old container handles requests until the new one passes health checks.

The self-hosted setup actually improved debugging. Leo has direct SSH access to the server, can inspect Docker logs in real time, and can exec into containers to diagnose production issues. The Dokploy dashboard gives the same visual overview as Vercel's dashboard — deployment history, environment variables, domain management, and log streaming.

The only trade-off is maintenance responsibility. Leo runs apt update && apt upgrade monthly and monitors disk usage. Dokploy handles certificate renewal, container restarts, and reverse proxy configuration automatically. For a bootstrapped SaaS doing $3K MRR, the hour of monthly maintenance is worth the $140 savings.

Related Skills

  • dokploy -- Self-hosted PaaS for deploying apps with Docker and Nixpacks on your own server
  • nixpacks -- Auto-detect and build container images from source code without Dockerfiles