Back to KB
Difficulty
Intermediate
Read Time
11 min

Solo SaaS Operations: <$6/Month Stack with <45s Deploys, Automated Rollbacks, and 99.9% Uptime on a Single VPS

By Codcompass TeamΒ·Β·11 min read

Current Situation Analysis

You are a solo SaaS founder or a senior engineer running a side project. Your constraints are absolute: time is scarcer than money, but cash flow matters. You cannot afford a DevOps hire, and you cannot spend your weekends debugging Kubernetes manifests or chasing PaaS invoices.

Most tutorials fail you here. They suggest:

  1. PaaS Overkill: Deploying to Vercel/Render/AWS Elastic Beanstalk. This works until you hit the "PaaS Tax." A simple app with a DB, Redis, and background workers quickly balloons to $50–$150/month. For a bootstrapped SaaS, this is margin erosion.
  2. Under-Engineered DIY: Using pm2 on a raw Ubuntu box with manual cron backups. This collapses the moment you need a zero-downtime deployment or suffer a disk failure. I've seen solo devs lose 3 days of transaction data because their backup script failed silently and they never tested the restore.
  3. Complexity Traps: Setting up Kubernetes on a single node or using Terraform for a monolith. This introduces cognitive overhead that distracts from building the product.

The Bad Approach: Consider a typical solo setup: pm2 for process management, nginx manually configured with Let's Encrypt, and a shell script that runs pg_dump nightly.

  • Failure Mode: You push a breaking change. pm2 restart drops active connections. Users see 502 errors. You realize the migration failed halfway, leaving the DB in an inconsistent state. You have no automated rollback. You spend 45 minutes manually reverting code and fixing the DB. Downtime: 12 minutes. Revenue impact: Trust loss.
  • Cost: $6/month VPS + $10/month managed DB = $16/month. Still too high for a pre-PMF project.

The Reality Check: You need cloud-grade reliability (atomic deploys, automated backups, health checks) at bare-metal prices. You need a system that recovers from failure faster than you can notice it.

WOW Moment

The Paradigm Shift: Treat your single VPS not as a "server," but as an ephemeral container runtime with persistent volumes.

The "WOW" comes from implementing Immutable Deployment Patterns on a Single Node. Instead of updating files in place, every deploy spins up a new container, verifies it, swaps traffic, and tears down the old one. If the new container fails health checks, the system automatically rolls back to the previous container in seconds.

Combined with Snapshot-Triggered Migrations, you eliminate the fear of schema changes. The system takes a DB snapshot immediately before applying a migration. If the migration fails or the app crashes post-migration, a single command restores the DB to the pre-migration state.

The Aha Moment:

"You get 99.9% uptime and zero-downtime deploys not by buying expensive managed services, but by automating the lifecycle of your containers and database with deterministic scripts that run in <$50ms."

Core Solution

Tech Stack (2024-2025 Standards)

  • Compute: Node.js 22 (LTS), TypeScript 5.5+, Docker 27.
  • Database: PostgreSQL 17 (with pg_dump and WAL archiving capabilities).
  • Reverse Proxy: Caddy 2.8 (Automatic TLS, HTTP/3, superior health checking).
  • CI/CD: GitHub Actions 2024 runner.
  • VPS: Hetzner CPX31 or DigitalOcean Standard 1GB ($4.50–$6.00/month).

Architecture

  1. Caddy sits on port 80/443, handles TLS, and routes to the app container.
  2. App Container runs the Node.js process. It exposes a /healthz endpoint.
  3. DB Container runs PostgreSQL. Data is persisted via a named volume.
  4. Deploy Script orchestrates the atomic swap.
  5. Backup Script handles rotation and offsite replication.

1. Atomic Deploy with Auto-Rollback

Most solo devs use docker compose up -d. This is unsafe. If the new image crashes, you're down. The following TypeScript script performs an atomic deploy: it starts the new service, waits for it to be healthy, swaps the network alias, and only then removes the old container. If health checks fail, it aborts and retains the old container.

File: scripts/atomic-deploy.ts

import { execSync, ExecSyncOptions } from 'child_process';
import { setTimeout } from 'timers/promises';

const HEALTH_CHECK_URL = 'http://localhost:3000/healthz';
const MAX_RETRIES = 20;
const RETRY_INTERVAL_MS = 1000;

interface DeployConfig {
  composeFile: string;
  serviceName: string;
  imageTag: string;
}

async function exec(cmd: string, options: ExecSyncOptions = {}): Promise<string> {
  console.log(`> ${cmd}`);
  try {
    return execSync(cmd, { stdio: 'pipe', ...options }).toString().trim();
  } catch (error: any) {
    const stderr = error.stderr?.toString() || error.message;
    throw new Error(`Command failed: ${cmd}\nOutput: ${stderr}`);
  }
}

async function checkHealth(): Promise<boolean> {
  try {
    const response = await fetch(HEALTH_CHECK_URL, { method: 'GET' });
    return response.ok;
  } catch {
    return false;
  }
}

export async function atomicDeploy(config: DeployConfig): Promise<void> {
  const { composeFile, serviceName, imageTag } = config;
  
  console.log(`Starting atomic deploy for ${serviceName}:${imageTag}`);

  // 1. Pull new image
  await exec(`docker compose -f ${composeFile} pull ${serviceName}`);

  // 2. Start new container with a temporary name to avoid conflict
  // We use a label to track this deployment
  const tempName = `${serviceName}_new_${Date.now()}`;
  
  try {
    

πŸŽ‰ Mid-Year Sale β€” Unlock Full Article

Base plan from just $4.99/mo or $49/yr

Sign in to read the full article and unlock all 635+ tutorials.

Sign In / Register β€” Start Free Trial

7-day free trial Β· Cancel anytime Β· 30-day money-back

Sources

  • β€’ ai-deep-generated