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.
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:
- Automated offsite backups: Taking daily database dumps and pushing them off the host.
- Atomic rollback paths: Restoring the previous working state in under sixty seconds.
- Autonomous uptime watchdogs: Ensuring services automatically restart and alert you on fatal crashes.
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:
#!/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:
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>&1Before 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:
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.txtThe 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:
#!/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 1fiWith 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:
[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.targetEnable and start the service:
sudo systemctl daemon-reloadsudo systemctl enable vibeproject.servicesudo systemctl start vibeproject.serviceAutomated 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:
#!/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 botfiSchedule this watchdog via cron to run every minute:
* * * * * /opt/scripts/uptime-watchdog.sh >> /var/log/uptime-watchdog.log 2>&1By 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:
| Step | Action | Command | Expected Outcome |
|---|---|---|---|
| 1. Triage | Check container process status | docker compose ps | Identify whether containers are running, restarting, or exited. |
| 2. Log Inspection | Read the latest 100 log lines | docker compose logs --tail=100 app | Pinpoint the unhandled exception, syntax error, or crash reason. |
| 3. Immediate Rollback | Revert to previous image tag | ./deploy.sh $(cat .previous_version) | Restore live uptime while you diagnose the root cause locally. |
| 4. Database Recovery | Restore latest offsite backup if data corrupted | gunzip -c backup.sql.gz | docker exec -i db psql -U postgres | Reconstruct database state to last known good snapshot. |
| 5. Port & Proxy Audit | Verify reverse proxy upstream | curl -Iv http://127.0.0.1:3000/api/health | Confirm 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
.envfiles into public or private git repositories. Store production secrets in a dedicated.env.productionfile 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.