← All posts

AI Infrastructure on a Budget: Our VPS + Caddy + n8n Stack

How we run a full AI-assisted development and automation stack on a single VPS for under €15/month — and the trade-offs that come with that choice.

  • infrastructure
  • vps
  • caddy
  • n8n
  • self-hosting

The default advice for AI infrastructure is “use managed cloud services.” Vercel for hosting, Railway for databases, Zapier for automation, managed Postgres for state. Each service adds $10–30/month. The convenience is real; so is the bill.

We run a different stack: a single KVM VPS, Docker Compose, Caddy, n8n, and Postgres. Total cost: under €15/month. This post is the honest account — what works, what doesn’t, and when the managed-cloud path makes more sense.

The Stack

VPS (KVM, Ubuntu 24.04)
└── Docker Compose
    ├── caddy           — TLS termination + reverse proxy + static file serving
    ├── n8n             — visual workflow automation (self-hosted)
    ├── postgres        — persistent state for n8n + custom workflows
    └── mcp-fs          — Model Context Protocol file server

No Cloudflare. No external CDN. No managed database. Everything on one box.

Why Caddy

We evaluated Nginx and Traefik before settling on Caddy. The decision came down to one feature: automatic TLS.

With Caddy, a site block like this:

neu.example.com {
    file_server {
        root /data/www/neu.example.com
    }
}

…automatically provisions a Let’s Encrypt certificate on first request, renews it automatically, and serves HTTP/2. No certbot cron job, no manual renewal, no certificate expiry incidents.

The trade-off: Caddy’s config syntax is simpler than Nginx’s but less flexible. For complex proxy setups with fine-grained headers, Nginx has an edge. For the 95% case — reverse proxy + static files + TLS — Caddy is significantly less operational overhead.

One critical lesson learned the hard way: always run caddy validate before caddy reload. Caddy’s graceful reload will apply an invalid config and crash the process, taking all sites down. Validate first, always.

Why Self-Hosted n8n

n8n is our automation layer — it runs scheduled workflows, handles webhook triggers, and connects Claude to external data sources. The cloud version starts at $24/month for the basic tier. Self-hosted on our existing VPS costs nothing additional.

The trade-off is real: you own the maintenance. n8n updates, container restarts, backup scheduling — these are your problem. For a solo-founder stack, that’s maybe 30 minutes a month. For a team, the managed version may be worth the cost.

One thing we didn’t expect: n8n’s self-hosted workflow import/export is excellent. We can version-control workflows as JSON, restore from backup in minutes, and move between environments without data loss. The cloud version’s collaboration features aren’t worth $24/month for a solo operation.

Postgres for Workflow State

n8n requires a database. SQLite works for single-user local setups; Postgres is the right choice for anything production-adjacent.

We run a single Postgres container serving both n8n and our custom tooling. Internal hostname postgres — reachable from any container in the same Docker network. Database app_db, single application user with limited grants.

One operational note: Docker container names matter for networking. Our container is named postgres — not my_postgres or postgres_1. When n8n’s DB_POSTGRESDB_HOST is set to postgres, it resolves to the container by name. This sounds obvious until you spend an hour debugging a connection refused error because a container was renamed during a compose file refactor.

Backup Strategy

Self-hosting means you own the backups. We run two:

Database backup:

docker exec postgres pg_dump -U app_user app_db > /backups/app_db_$(date -u +%Y%m%dT%H%M%SZ).sql

n8n volume backup:

tar -czf /backups/n8n_data_$(date -u +%Y%m%dT%H%M%SZ).tar.gz /var/lib/docker/volumes/n8n_data/_data/

Both run via cron. Both go to the same VPS. For a production setup, off-site replication is essential — we’re currently shipping backups to a separate storage location as part of a planned automation workflow.

The Static Site Layer

We recently added Astro as a static site generator, serving output via Caddy’s file_server directive. The workflow:

  1. Astro builds to dist/ locally on the VPS
  2. dist/ contents are copied into the Caddy data volume
  3. Caddy serves the static files directly — no Node.js process, no runtime

Performance is excellent. TTFB on a cold request is typically under 50ms from European edge locations. The absence of a Node.js runtime means zero cold-start latency and zero memory overhead from the web layer.

One trap we hit: Docker bind-mount inode splits (covered in detail in our deployment incident post). Config updates to bind-mounted files need to be written in-place, not via atomic replace.

When This Stack Is Wrong

Self-hosted infrastructure is the right choice when:

  • You’re a solo founder or small team with engineering capacity
  • Your traffic patterns are predictable
  • You have time for ~30 min/month of maintenance
  • Cost optimization is a real constraint

It’s the wrong choice when:

  • You need instant horizontal scaling under variable load
  • Your team doesn’t have infrastructure expertise
  • Downtime has direct revenue impact
  • You’re spending more time on infrastructure than on your product

We’re in the first category. If you’re in the second, the managed-cloud path has genuine value — the cost is buying back your time and attention.

Cost Breakdown

ComponentCost
KVM VPS (2 vCPU, 8 GB RAM, 100 GB NVMe)~€12/month
Domain registration~€1/month amortized
Let’s Encrypt TLS€0
n8n (self-hosted)€0
Postgres (self-hosted)€0
Astro (static, open source)€0
Claude API (usage-based)Separate
Total infrastructure~€13/month

Claude API costs depend heavily on usage patterns. For development workloads with prompt caching enabled, a typical month runs $20–60. For production automation workflows, budget separately.

The Maintenance Overhead (Honest Assessment)

A month of running this stack looks like:

  • 2–3 container restarts for n8n updates
  • 1 Caddy config change (new site block, webhook, etc.)
  • Weekly backup verification (5 minutes)
  • Occasional Postgres query for state debugging

Total: roughly 1–2 hours/month. That’s the real cost of self-hosting. If your time is worth more than €12/month in infrastructure savings, the managed path is better economics. If you’re a technical founder who enjoys infrastructure ownership (we are), this stack is a reasonable choice.


This stack is documented in detail as part of CoveLab Foundation — including the Docker Compose templates, Caddy configs, backup scripts, and the maintenance patterns we’ve developed over months of production use.