Back to VPS & Infra

VPS Infrastructure Basics for Vibe Coders

A practical guide to VPS infrastructure for vibe coders, covering automated database backups, instant rollback workflows, and systemd uptime watchdogs.

VPS Infrastructure Basics for Vibe Coders
ZeroLabs Field Manual · VPS Infrastructure Basics for Vibe Coders

Why does your vibe-coded project need basic VPS infrastructure?

Vibe coding makes building software radically faster. Using modern coding assistants, you can generate a complete full-stack web application, an API service, and a background database schema in an afternoon. When it comes time to deploy, moving your project from localhost to a basic $5-a-month Linux virtual private server (VPS) is the most cost-effective and flexible hosting route available.

As we discussed in our breakdown of why we self-host our stack at ZeroLabs, running your own server frees you from vendor lock-in, arbitrary request execution timeouts, and spiraling monthly platform bills. However, self-hosting introduces an uncomfortable reality: you are responsible for keeping the lights on.

When you deploy a new AI-generated feature directly to production, several common failures occur:

  • An unvetted database migration alters or drops a production table.
  • A memory leak in an unoptimized worker process quietly exhausts server RAM.
  • A syntax error in a background daemon causes the service to exit immediately upon restart.
  • An unexpected VPS kernel reboot leaves your containers offline while you sleep.

Surviving these incidents does not require an enterprise DevOps team or a complex Kubernetes cluster. You only need three disciplined primitives:

  1. Automated offsite backups: Taking daily database dumps and pushing them off the host.
  2. Atomic rollback paths: Restoring the previous working state in under sixty seconds.
  3. Autonomous uptime watchdogs: Ensuring services automatically restart and alert you on fatal crashes.
Architecture Flow
flowchart TD
    Deploy["Git Push / Container Build"] --> HealthCheck{"Health Check Passes?"}
    HealthCheck -- "Yes" --> LiveTraffic["Route Live Traffic (Port 80/443)"]
    LiveTraffic --> Watchdog["systemd & Docker Supervisor Active"]
    Watchdog --> PeriodicBackup["Daily Encrypted Offsite Backup"]
    
    HealthCheck -- "No (502 / Error)" --> AutoRollback["Trigger Immediate Atomic Rollback"]
    AutoRollback --> PreviousRelease["Restore Previous Working Release Tag"]
    PreviousRelease --> OperatorAlert["Alert Operator via Webhook"]

Primitive 1: Automated offsite backups that actually work

A local backup stored on the same VPS disk as your application is not a real backup. If the VPS provider suffers hardware corruption, if you accidentally wipe your /var/lib/docker/volumes directory, or if ransomware locks the server, local snapshots vanish alongside your database.

The golden rule of VPS backups is offsite storage: snapshots must be encrypted and transmitted to external object storage (such as an S3-compatible bucket, Cloudflare R2, or Backblaze B2) immediately after creation.

Automated PostgreSQL Backup Script

For projects running a containerized PostgreSQL database, you can automate daily logical backups using a straightforward bash script. Place this script at /opt/scripts/backup-db.sh on your server:

Terminalbash
#!/usr/bin/env bashset -euo pipefail# ConfigurationPROJECT_NAME="vibeproject"BACKUP_DIR="/var/backups/${PROJECT_NAME}"TIMESTAMP=$(date +"%Y%m%d_%H%M%S")BACKUP_FILE="${BACKUP_DIR}/postgres_${PROJECT_NAME}_${TIMESTAMP}.sql.gz"CONTAINER_NAME="vibeproject-db-1"DB_USER="postgres"DB_NAME="vibeproject_production"# Ensure local backup directory existsmkdir -p "${BACKUP_DIR}"echo "[backup] Exporting PostgreSQL database..."docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" | gzip > "${BACKUP_FILE}"echo "[backup] Verifying backup integrity..."if [ ! -s "${BACKUP_FILE}" ]; then    echo "[backup] ERROR: Generated backup file is empty!" >&2    exit 1fiecho "[backup] Syncing encrypted snapshot to offsite object storage..."# Example using rclone to an encrypted S3-compatible remoterclone copy "${BACKUP_FILE}" "remote-s3:backups-vibe-coder/${PROJECT_NAME}/"echo "[backup] Pruning local backups older than 7 days..."find "${BACKUP_DIR}" -name "*.sql.gz" -type f -mtime +7 -deleteecho "[backup] Backup completed successfully: ${BACKUP_FILE}"

Make the script executable and schedule it to run every morning at 03:00 UTC using cron:

Terminalbash
chmod +x /opt/scripts/backup-db.shsudo crontab -e# Add the following entry:0 3 * * * /opt/scripts/backup-db.sh >> /var/log/backup-cron.log 2>&1

Before relying on any backup schedule, run a restore drill. Download the .sql.gz archive to a test environment and confirm that you can import the tables without errors. A backup you have never tested is simply a wish.

Primitive 2: Atomic rollback paths for bad deployments

When an AI assistant refactors code and you deploy the changes, the fastest way to resolve an unexpected production bug is not frantically prompting the AI for a hotfix. The correct response is rolling back to the previous verified commit instantly, then debugging the issue in your local environment.

To make rollbacks instantaneous, avoid building container images directly on production with mutable tags like :latest. Instead, use immutable version tags derived from git commit SHAs.

Docker Compose with Release Tagging

Structure your docker-compose.yml to consume an environment variable for the application image tag:

yaml
services:  app:    image: ghcr.io/yourusername/vibe-app:${APP_VERSION:-latest}    restart: always    env_file:      - .env.production    ports:      - "127.0.0.1:3000:3000"    healthcheck:      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]      interval: 10s      timeout: 5s      retries: 3      start_period: 15s  db:    image: postgres:16-alpine    restart: always    environment:      POSTGRES_DB: vibeproject_production      POSTGRES_USER: postgres      POSTGRES_PASSWORD_FILE: /run/secrets/db_password    volumes:      - postgres_data:/var/lib/postgresql/data    secrets:      - db_passwordvolumes:  postgres_data:secrets:  db_password:    file: ./secrets/db_password.txt

The Atomic Rollback Script

Maintain a record of the currently active release and the immediately preceding release. Create a deployment script at /opt/scripts/deploy.sh that switches tags cleanly:

Terminalbash
#!/usr/bin/env bashset -euo pipefailTARGET_VERSION="${1:-}"if [ -z "${TARGET_VERSION}" ]; then    echo "Usage: ./deploy.sh " >&2    exit 1fiAPP_DIR="/opt/vibeproject"cd "${APP_DIR}"# Record existing version as previous release before updatingif [ -f .current_version ]; then    cp .current_version .previous_versionfiecho "[deploy] Pulling target image version: ${TARGET_VERSION}..."docker pull "ghcr.io/yourusername/vibe-app:${TARGET_VERSION}"echo "[deploy] Updating release configuration..."echo "APP_VERSION=${TARGET_VERSION}" > .env.releaseecho "${TARGET_VERSION}" > .current_version# Restart application container gracefullydocker compose --env-file .env.release up -d --no-deps app# Wait for container health checkecho "[deploy] Verifying service health..."sleep 15if docker compose ps app | grep -q "(healthy)"; then    echo "[deploy] Release ${TARGET_VERSION} deployed successfully and healthy."else    echo "[deploy] Health check failed! Initiating automatic rollback..." >&2    if [ -f .previous_version ]; then        PREV_VERSION=$(cat .previous_version)        echo "[rollback] Reverting to previous stable version: ${PREV_VERSION}"        echo "APP_VERSION=${PREV_VERSION}" > .env.release        echo "${PREV_VERSION}" > .current_version        docker compose --env-file .env.release up -d --no-deps app        echo "[rollback] Rollback completed."    else        echo "[rollback] No previous version found on host. Immediate manual intervention required." >&2    fi    exit 1fi

With this pattern, a broken deployment self-reverts in under thirty seconds before end users encounter extended downtime. As covered in our guide on deploying a first AI project from local to hosted, separating your build artifact from your production server is the foundation of dependable infrastructure.

Primitive 3: Autonomous uptime watchdogs and process supervision

Even well-written applications occasionally crash due to memory exhaustion, third-party API rate limits, or transient network timeouts. If you run your application in a detached terminal session or raw background process, any unhandled exception terminates the server permanently.

To guarantee continuous uptime, production processes must be managed by an operating system supervisor that monitors process health and restarts failed services automatically.

Systemd Process Supervision for Docker Compose

Rather than relying purely on manual docker commands, manage your entire Docker Compose stack through a native systemd service unit. This guarantees that your application starts on system boot and restarts if Docker itself restarts.

Create the service file at /etc/systemd/system/vibeproject.service:

text
[Unit]Description=Vibe Project Application StackRequires=docker.serviceAfter=docker.service network.target[Service]Type=oneshotRemainAfterExit=yesWorkingDirectory=/opt/vibeprojectExecStart=/usr/bin/docker compose --env-file .env.release up -dExecStop=/usr/bin/docker compose stopExecReload=/usr/bin/docker compose --env-file .env.release up -d --remove-orphansTimeoutStartSec=120Restart=on-failure[Install]WantedBy=multi-user.target

Enable and start the service:

Terminalbash
sudo systemctl daemon-reloadsudo systemctl enable vibeproject.servicesudo systemctl start vibeproject.service

Automated Health Check Watchdog

To catch silent hangs where a container remains running but stops responding to HTTP requests, set up a lightweight monitoring script that probes your live /api/health endpoint every sixty seconds. Place this in /opt/scripts/uptime-watchdog.sh:

Terminalbash
#!/usr/bin/env bashHEALTH_URL="http://127.0.0.1:3000/api/health"MAX_FAILURES=3STATE_FILE="/tmp/health_failure_count"touch "${STATE_FILE}"CURRENT_FAILURES=$(cat "${STATE_FILE}" || echo 0)STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "${HEALTH_URL}" || echo "000")if [ "${STATUS_CODE}" -eq 200 ]; then    echo 0 > "${STATE_FILE}"    exit 0else    CURRENT_FAILURES=$((CURRENT_FAILURES + 1))    echo "${CURRENT_FAILURES}" > "${STATE_FILE}"    echo "[watchdog] Health check probe failed with status ${STATUS_CODE} (failure count: ${CURRENT_FAILURES})" >&2fiif [ "${CURRENT_FAILURES}" -ge "${MAX_FAILURES}" ]; then    echo "[watchdog] Threshold exceeded. Restarting application container..." >&2    cd /opt/vibeproject && docker compose restart app    echo 0 > "${STATE_FILE}"    # Optional: Send a notification ping to a webhook or Telegram botfi

Schedule this watchdog via cron to run every minute:

Terminalbash
* * * * * /opt/scripts/uptime-watchdog.sh >> /var/log/uptime-watchdog.log 2>&1

By delegating process recovery to deterministic operating system tools, your VPS self-heals without requiring you to monitor logs around the clock.

The production recovery runbook: Putting it all together

When disaster strikes and your application goes completely dark, panic leads to mistakes. Follow this sequential runbook to diagnose and restore service cleanly:

StepActionCommandExpected Outcome
1. TriageCheck container process statusdocker compose psIdentify whether containers are running, restarting, or exited.
2. Log InspectionRead the latest 100 log linesdocker compose logs --tail=100 appPinpoint the unhandled exception, syntax error, or crash reason.
3. Immediate RollbackRevert to previous image tag./deploy.sh $(cat .previous_version)Restore live uptime while you diagnose the root cause locally.
4. Database RecoveryRestore latest offsite backup if data corruptedgunzip -c backup.sql.gz | docker exec -i db psql -U postgresReconstruct database state to last known good snapshot.
5. Port & Proxy AuditVerify reverse proxy upstreamcurl -Iv http://127.0.0.1:3000/api/healthConfirm local port responds before checking public domain DNS.

Following a structured runbook eliminates guesswork. By combining offsite backups, atomic rollback tags, and autonomous process supervision, you can run vibe-coded production software on an inexpensive Linux VPS with enterprise-grade stability.

FAQ

What is the most common reason self-hosted vibe projects crash?

Memory exhaustion. Coding assistants frequently write memory-intensive worker loops or unbounded database queries that consume available RAM quickly on modest 1GB or 2GB VPS instances. When RAM runs out, the Linux Out-Of-Memory (OOM) killer terminates the container immediately. Always configure swap space on your VPS (at least 2GB to 4GB) and set container memory limits in your docker-compose.yml.

How can I secure sensitive API keys on my VPS?

Never commit environment variables or .env files into public or private git repositories. Store production secrets in a dedicated .env.production file on the VPS with strict file permissions (chmod 600 .env.production). For added security, reference our guide on handling secrets, API keys, and rate limits.

Why use Docker Compose instead of running apps directly on the host?

Docker Compose encapsulates system dependencies, language runtimes, and database libraries into isolated containers. If you upgrade Node.js or Python on the host operating system, your containerized application remains completely unaffected. Containers also make rollbacks trivial because every past release exists as an immutable, pre-built image.

How do I configure HTTPS without complex SSL certificate renewal crons?

Use Caddy as your reverse proxy. Caddy automatically provisions, verifies, and renews free Let's Encrypt SSL certificates for your custom domains with zero configuration. Point your DNS A record to your VPS IP address, define your domain in a two-line Caddyfile, and Caddy manages HTTPS termination automatically.

Share