All writing
9 min

NextDeploy VPS Deployment Guide: Zero-Downtime Linux Releases Grounded in Go

Deploy Next.js apps to any Linux VPS with zero downtime, Caddy automatic TLS, HMAC-signed socket communication, and a daemon-managed release pipeline. No Docker, no Kubernetes.


Deploying Next.js applications on bare-metal or virtual private servers (Hetzner, DigitalOcean, Linode, AWS EC2, OVH) shouldn't require managing fragile Docker daemon sockets, multi-container compose networks, or manual reverse proxy configurations. NextDeploy deploys Next.js apps directly on Linux using SSH, systemd, and Caddy.

No containers, no Kubernetes, no cloud lock-in — just SSH, systemd, and Caddy driven by a dual-binary architecture.

Architecture: Two Binaries, Two Distinct Roles

NextDeploy divides responsibilities cleanly between your local developer workstation and the target host:

  • nextdeploy (CLI): Runs locally on your machine. Inspects git status, executes buildflow.Run with incremental cache fingerprinting, uploads the standalone tarball over SSH, and triggers release operations.
  • nextdeployd (Daemon): Runs as a persistent root systemd service on the VPS. Exposes a local Unix socket at /run/nextdeployd/nextdeployd.sock (0660 root:nextdeploy). Handles un-tarring, systemd unit generation, health-check probing, Caddy fragment validation, and atomic inode symlink switches.
  • When the CLI triggers a deploy, it SSHes to the server and executes sudo nextdeployd ship --tarball=<remotePath>. The subcommand acts as a client to the running daemon, signing requests with a 32-byte HMAC-SHA256 secret loaded from /etc/nextdeployd/config.json. Replay defense rejects requests with stale timestamps or duplicate nonces.

    Diagram · System Flow
    Rendering diagram...

    Quick Start: Deploy in 5 Commands

    From a clean Next.js 15+ repository to a production HTTPS release on any Linux VPS:

    bash· 16 lines
    1# 1. Scaffold nextdeploy.yml manifest
    2nextdeploy init
    3
    4# 2. Provision fresh Linux host in agent mode (Node 22, Caddy, Coraza WAF, daemon)
    5nextdeploy prepare
    6
    7# 3. Securely sync production environment variables
    8nextdeploy secrets set DATABASE_URL="postgresql://user:pass@127.0.0.1:5432/app"
    9nextdeploy secrets set NEXTAUTH_SECRET="f9b4c2e1a0d8..."
    10
    11# 4. Build, upload tarball over SSH, and trigger zero-downtime cutover
    12nextdeploy ship
    13
    14# 5. Verify live runtime metrics and stream journalctl logs
    15nextdeploy status
    16nextdeploy logs

    The 5 Steps from Init to Production

    1. Project Initialization (nextdeploy init)

    Inside your Next.js project root, run nextdeploy init. The CLI inspects package.json for the next dependency, sanitizes your application name into a strict regex slug (^[a-z0-9-]+$, 3–63 chars), and interactively prompts for your deployment topology:

    bash· 12 lines
    1# Run interactive initializer
    2nextdeploy init
    3
    4# Expected output:
    5# ✔ Detected Next.js 15.1.0 in package.json
    6# ? Target deployment primitive: VPS (Linux systemd + Caddy)
    7# ? Application slug [my-next-app]:
    8# ? Target VPS host IP or hostname: 198.51.100.25
    9# ? SSH username [ubuntu]:
    10# ? SSH private key path [~/.ssh/id_ed25519]:
    11# ? Public domain name: app.example.com
    12# ✔ Generated nextdeploy.yml

    The generated nextdeploy.yml manifest is minimal, declarative, and version-controlled:

    yaml· 19 lines
    1target_type: vps
    2
    3app:
    4  name: example-app          # sanitized slug, NOT a domain
    5  environment: production
    6  domain: app.example.com    # public domain served by Caddy
    7  port: 3000                 # internal Node.js port
    8
    9  gosidecar:
    10    enabled: false           # opt-in Go hyper-server
    11    image_optimization: true
    12    isr_cache: true
    13    max_image_workers: 4
    14
    15servers:
    16  - name: "production-01"
    17    host: 198.51.100.25      # your VPS public IP address
    18    username: ubuntu         # non-root user with passwordless sudo
    19    key_path: ~/.ssh/id_ed25519
    ℹ️
    NextDeploy does NOT generate a Dockerfile. VPS deployments run Next.js in output: standalone with native Node.js and systemd isolation.

    2. Server Provisioning (nextdeploy prepare)

    Provisioning runs in agent mode: the CLI SSHes to the server, installs the latest nextdeployd static binary from GitHub releases, and runs nextdeployd prepare directly on the target host. No local Ansible or Python dependencies required:

    bash· 14 lines
    1# Provision default target server in agent mode
    2nextdeploy prepare
    3
    4# Provision a specific server from nextdeploy.yml
    5nextdeploy prepare --server production-01
    6
    7# Provision all configured servers in parallel
    8nextdeploy prepare --all
    9
    10# Advanced provisioning flags:
    11nextdeploy prepare --skip-runtimes    # Skip Node.js 22 / Corepack if already managed
    12nextdeploy prepare --skip-hardening   # Skip Fail2Ban jails and logrotate
    13nextdeploy prepare --allow-root       # Allow provisioning over root SSH session
    14nextdeploy prepare --timeout 15m      # Override default 10m SSH provisioning timeout

    Every step checks current host state before modifying files, making prepare completely idempotent:

  • Base Utilities: curl, tar, ca-certificates, unzip via host package manager (apt/dnf/yum auto-detected).
  • Service Account: Unprivileged nextdeploy system group and user (/sbin/nologin, no home).
  • Filesystem Layout: /opt/nextdeploy/{apps,uploads,tmp} with 0750 permissions owned by nextdeploy:nextdeploy.
  • Node.js 22 & Corepack: NodeSource signed repository pinned major (Node.js 22), symlinked to /usr/local/bin.
  • Edge Proxy & WAF: Caddy compiled with the Coraza WAF module (coraza-caddy/v2) and Fail2Ban jails (caddy-auth and caddy-waf).
  • Daemon Security: Stored at /etc/nextdeployd/config.json with a random 32-byte HMAC secret (0600 root only).
  • Daemon Service: Managed under systemd as nextdeployd.service, ordered After=network.target caddy.service.
  • 3. Configure Production Secrets (nextdeploy secrets)

    Before activating your first build, register environment variables. Secrets are transmitted over encrypted SSH and written to /opt/nextdeploy/secrets/<app>.json with 0600 root-only permissions:

    bash· 10 lines
    1# Set individual secrets
    2nextdeploy secrets set DATABASE_URL="postgresql://user:pass@127.0.0.1:5432/production"
    3nextdeploy secrets set NEXTAUTH_SECRET="e9a2b847..."
    4nextdeploy secrets set RESEND_API_KEY="re_..."
    5
    6# Batch-import variables from an existing production env file
    7nextdeploy secrets load .env.production
    8
    9# Audit registered variables (values are masked for security)
    10nextdeploy secrets list

    4. Incremental Build & Packaging (nextdeploy build)

    Executing nextdeploy build triggers buildflow.Run. The CLI inspects the current git commit SHA and a cryptographic hash of nextdeploy.yml. If neither changed since the previous artifact, next build is bypassed while release metadata and tarball are fresh-minted:

    bash· 8 lines
    1# Incremental build (skips next build if git SHA and config hash match)
    2nextdeploy build
    3
    4# Force a full rebuild ignoring build cache locks
    5nextdeploy build --force
    6
    7# Inspect bundle output size and standalone chunks without building
    8nextdeploy inspect
  • Incremental Gate: Inspects .nextdeploy/build.lock against the active git commit SHA and config hash.
  • Metadata Extraction: Extracts route manifests, prerender routes, and server actions into .nextdeploy/metadata.json.
  • Tarball Compression: Packages standalone server.js, public/, .next/static/, and metadata into app.tar.gz.
  • 5. Secure Zero-Downtime Deployment (nextdeploy ship)

    Running nextdeploy ship uploads app.tar.gz over SSH to /opt/nextdeploy/uploads/ and calls nextdeployd ship over the local Unix socket. The daemon executes the atomic activateRelease sequence:

    bash· 11 lines
    1# Deploy to production server
    2nextdeploy ship
    3
    4# Ship with forced clean rebuild
    5nextdeploy ship --force
    6
    7# Deploy to an isolated named environment
    8nextdeploy ship -e staging
    9
    10# Stream verbose execution logs
    11nextdeploy ship -v
  • 1. Extract & Lock: Unpacks the tarball to a scratch directory, validates metadata.json, and acquires a per-app file lock to serialize operations.
  • 2. Directory Layout: Moves unpacked files to /opt/nextdeploy/apps/<app>/releases/<timestamp>-<shortSha>.
  • 3. Port Allocation: Reads state.json to reuse the active port, or dynamically binds a free loopback port.
  • 4. Secrets Sync: Renders /opt/nextdeploy/secrets/<app>.json into the release's .env.nextdeploy with 0600 permissions. Hard failure if secrets are missing.
  • 5. Systemd Unit: Creates nextdeploy-<app>-<releaseID>.service with User=nextdeploy, ExecStart=/usr/local/bin/node server.js, and optional cgroup resource limits (cpu_quota, memory_max, memory_high).
  • 6. Health Check: Two-tier gate in waitForHealthy (up to 5m backoff): Gate 1 confirms TCP binding; Gate 2 issues HTTP GET / requiring status < 500 to catch startup panics.
  • 7. Shared Static: Copies .next/static/ into /opt/nextdeploy/apps/<app>/shared_static/ so users with cached HTML never encounter 404s on hashed asset chunks.
  • 8. Caddy Ingress: Writes /etc/caddy/nextdeploy.d/<app>.caddy via commitFragmentSafely. Validates candidate config with caddy validate, renames atomically, and reloads Caddy via systemctl reload caddy.
  • 9. Atomic Cutover: Creates current.tmp pointing to the new release and executes os.Rename('current.tmp', 'current') on the same filesystem.
  • 10. Pruning: Stops prior release services, retires previous units, and prunes to 5 retained releases.
  • Diagram · System Flow
    Rendering diagram...
    🛡️
    If health checks fail or an uncommitted step crashes, a deferred error trap automatically restores the previous symlink, re-applies the previous Caddy fragment, and terminates the candidate service. The server never serves a broken build.

    6. Point DNS & Automatic TLS Provisioning

    Point your domain's A-record to the VPS IP address. NextDeploy automatically generates dns.md with the exact record configuration needed. On the first incoming HTTPS request, Caddy automatically provisions and manages TLS certificates via Let's Encrypt:

    bash· 8 lines
    1# DNS Configuration Record:
    2# Type: A
    3# Host / Name: app (or @ for apex)
    4# Value / Target: 198.51.100.25 (Your VPS IP)
    5# TTL: 300 (or Auto)
    6
    7# Test certificate issuance & HTTP/2 handshake
    8curl -Iv https://app.example.com

    Day-2 Operational Commands

    Manage production workloads directly from your local terminal:

    bash· 17 lines
    1# Check active service state, MainPID, and live memory consumption
    2nextdeploy status
    3
    4# Follow application logs via SSH journalctl streaming
    5nextdeploy logs
    6nextdeploy logs --route /api
    7nextdeploy logs --audit      # View daemon HMAC command history
    8nextdeploy logs --all        # Stream app, daemon, and audit logs together
    9
    10# Hot-reload environment variables without a redeploy
    11nextdeploy secrets set DATABASE_URL=postgres://...
    12nextdeploy secrets load .env.production
    13
    14# Instantly revert to previous release
    15nextdeploy rollback
    16nextdeploy rollback --steps 2
    17nextdeploy rollback --to 7c8350a0

    Infrastructure simplicity is developer independence. True zero downtime requires disciplined, release-pinned state orchestration.