Skip to main content

Overview

A VPS (Virtual Private Server) is the sweet spot for most indie developers, small teams, startups, and anyone who wants full ownership without managing cloud complexity. Applad connects over SSH, synthesizes and applies a Docker Compose configuration from your project config, manages everything, and leaves. No persistent agent. Just Docker containers running your app.

Supported Providers

Any machine you can SSH into:
  • DigitalOcean Droplets
  • Hetzner Cloud
  • Linode
  • AWS EC2 (as VPS, not platform)
  • GCP Compute Engine (as VPS, not platform)
  • Bare metal servers
  • Home servers
  • Raspberry Pis
  • On-premise servers
A single mid-range VPS can comfortably run a production Applad instance serving thousands of users.

Prerequisites

On Your VPS

  • Docker installed — the only requirement
  • SSH access — with your public key added to ~/.ssh/authorized_keys
  • Firewall configured — ports 80, 443 open for web traffic

On Your Machine

  • Applad CLI installed
  • SSH key registered — your private key used for authentication
  • Project configuredapplad.yaml with environment targets

Agentless Architecture

Applad is agentless — there is no daemon on your servers. The agentless flow for a typical deployment:
Developer runs: applad deploy run production

1. Applad reads project config from your config tree
2. Opens an SSH connection using the developer's SSH key
3. Synthesizes the correct docker-compose.yml for the environment
4. Applies changes via Docker Compose
5. Logs the operation to the runtime database
6. SSH connection closes — only Docker containers remain
What this means:
  • Zero persistent footprint beyond Docker containers
  • No agent version mismatch issues
  • Works on any machine you can SSH into
  • Smaller security surface — no persistent daemon
  • Standard Docker tooling for inspection

Configuring VPS Targets

Define your VPS environments in project.yaml:
project.yaml
name: "mobile-app"
version: "1.0.0"

environments:
  staging:
    type: "vps"
    ssh:
      host: "staging.example.com"
      port: 22
      user: "deploy"
    resources:
      database: "postgres"
      storage: "filesystem"
      
  production:
    type: "vps"
    ssh:
      host: "prod.example.com"
      port: 22
      user: "deploy"
    resources:
      database: "postgres"
      storage: "s3"
You can move along the deployment continuum without changing application code. Only the environment config changes.

First Deployment

1. Preview the Deployment Plan

Always dry-run first:
applad up --env production --dry-run --diff
Shows exactly:
  • Every service that would start or restart
  • Every config change
  • Every migration pending
  • Every cloud resource that would be provisioned
  • Every SSH connection that would open
Nothing is a surprise.

2. Apply the Deployment

applad up --env production
Applad will:
  1. Validate all ${VAR} references are satisfied
  2. Check your SSH key has the required scope
  3. Connect to the VPS over SSH
  4. Synthesize docker-compose.yml from your config
  5. Apply changes in dependency order
  6. Produce a run recap

3. Review the Recap

applad up --env production
...

RECAP ─────────────────────────────────────────────
  environment   production
  duration      14.2s
  actor         alice@acme-corp (SHA256:abc123...)

  ok            12    already correct, no changes
  changed        3    database, functions, messaging
  skipped        0
  failed         0

  ✓ 2 pending migrations applied (primary)
  ✓ send-welcome-message redeployed (source updated)
  ✓ messaging config reconciled (provider changed to ses)
─────────────────────────────────────────────────────
The Operational Contract: If --dry-run showed it, it happens. If it didn’t show it, it doesn’t happen.

Environment Parity

The same Docker Compose model runs everywhere:
Local dev (Docker Compose, SQLite)
  → VPS staging (Docker Compose, Postgres)
    → VPS production (Docker Compose, Postgres)
      → VPS + cloud storage adapter (S3/R2)
        → VPS + cloud database adapter (RDS)
You move along this line as needs grow. Nothing changes about how your application is built.

Idempotent Operations

Running applad up twice produces the same result as running it once. This is guaranteed, not aspirational. Each resource has an explicit idempotency strategy:
  • Containers — compared by image digest and environment hash
  • Migrations — tracked by checksum, never re-applied
  • Config — compared against last-applied signature
  • Cloud resources — checked for existence before provisioning
A fully reconciled environment opens no SSH connections:
applad up --env production

RECAP ─────────────────────────────────────────────
  environment   production
  duration      0.8s
  actor         alice@acme-corp (SHA256:abc123...)

  ok            15    already correct, no changes
  changed        0
  skipped        0
  failed         0
─────────────────────────────────────────────────────

Drift Detection

Check if reality matches your config without changing anything:
applad status --drift --env production
DRIFT REPORT ─────────────────────────────────────
  environment   production

  ✓ database         in sync
  ✗ functions        drift detected
      send-welcome-message: running v1.2.0, config specifies v1.3.0
  ✓ storage          in sync
  ✓ messaging        in sync
  ✗ observability    drift detected
      rate_limiting.routes[/auth/*].requests: running 20, config specifies 15
  ✓ deployments      in sync
──────────────────────────────────────────────────
  2 resources drifted. Run applad up --env production to reconcile.

Selective Reconciliation

Reconcile only what you need:
# Only database migrations
applad up --env production --only database

# Everything except deployments
applad up --env production --skip deployments
Tags work across the entire config tree:
  • database, tables, storage, buckets
  • functions, workflows, messaging
  • flags, deployments, realtime, analytics

Access Control

VPS deployments require SSH key scopes:
# Check your access
applad access list

# Request production access
applad access request --scope "infrastructure:apply:production"
An administrator grants access:
applad access grant alice@acme-corp \
  --scope "infrastructure:apply:production" \
  --project mobile-app
Config files cannot enforce access control over themselves. Grants live in the admin database. Only applad access commands can change what anyone can do.

Secrets Management

Secrets for VPS environments live in the admin database, not .env files:
# Set a production secret
applad secrets set STRIPE_SECRET --env production

# List secrets for an environment
applad secrets list --env production

# Rotate a secret with transition window
applad secrets rotate STRIPE_SECRET --env production
Applad injects secrets at runtime via SSH session — never written to disk on the VPS.

Monitoring and Debugging

View Service Status

SSH to your VPS and use standard Docker tools:
ssh [email protected]

# Check running services
docker compose ps

# View logs
docker logs applad-functions-1 -f

# Execute commands in a container
docker exec -it applad-db-1 psql -U postgres
Everything is inspectable with tools your team already knows. No Applad-proprietary runtime.

Run Recap History

Every applad up is logged with full attribution:
applad history --env production

# Filter by actor
applad history --env production --actor alice@acme-corp

# Show last 10 deployments
applad history --env production --limit 10

Emergency Rollback

Revert to the last working config:
git revert HEAD
applad up --env production --dry-run --diff
applad up --env production
Config is versioned in git. Rollback is a git operation + reconciliation.

SSL and Domain Management

Applad uses Caddy for reverse proxy and automatic SSL:
deployments/web-production.yaml
name: "web-production"
type: "web"
domain: "app.example.com"
port: 3000
ssl:
  auto: true
  email: "[email protected]"
Caddy handles:
  • Automatic HTTPS via Let’s Encrypt
  • Certificate renewal
  • HTTP → HTTPS redirect
  • SSL termination

Cost Optimization

A typical production setup:
  • Hetzner CPX21 — 3 vCPU, 4 GB RAM, €8.46/month
  • Handles — 1000+ concurrent users, millions of requests/month
  • Predictable costs — no per-request pricing, no surprise bills
Compare to serverless equivalents at scale:
  • AWS Lambda + RDS + S3 + API Gateway = hundreds per month
  • VPS = flat rate, full control

Scaling Your VPS Deployment

Start simple, scale as needed:

Vertical Scaling

Resize your VPS through your provider’s dashboard. Applad adapts automatically:
# After resizing VPS
applad up --env production

Horizontal Scaling

Add multiple VPS instances behind a load balancer:
project.yaml
environments:
  production:
    type: "vps-cluster"
    nodes:
      - host: "prod-01.example.com"
      - host: "prod-02.example.com"
      - host: "prod-03.example.com"
    load_balancer:
      host: "lb.example.com"

Database Scaling

Switch to a managed database without code changes:
database/database.yaml
connections:
  primary:
    adapter: "postgres"
    host: ${RDS_HOST}
    port: 5432
    database: ${RDS_DATABASE}
    user: ${RDS_USER}
    password: ${RDS_PASSWORD}

Next Steps

Build docs developers (and LLMs) love