Deploy PostgreSQL on a Hetzner VPS in 5 minutes
A complete walkthrough: provision a Hetzner Cloud server, connect it to Vessl, and deploy a production-ready PostgreSQL instance with SSL, persistent storage, and automated backups.
A complete walkthrough: provision a Hetzner Cloud server, connect it to Vessl, and deploy a production-ready PostgreSQL instance with SSL, persistent storage, and automated backups.
If you've ever priced out a managed PostgreSQL instance on AWS RDS or Google Cloud SQL, you know the math gets uncomfortable fast. A db.t3.medium with 2 vCPU and 4 GB RAM on RDS costs around $60/month before storage, IOPS, and backups. The same hardware on a Hetzner Cloud CX22 is €4.51/month — roughly 12x cheaper.
The catch is that managed Postgres handles a lot for you: provisioning, upgrades, backups, point-in-time recovery, monitoring. Self-hosting trades that for control and price.
This post shows how to get most of the convenience back — with Vessl, a Hetzner VPS, and about 5 minutes of clicking. The result: a production Postgres instance that survives reboots, gets automated backups, and is reachable only from your application network.
The total bill comes to about €4.51/month for the VPS + Vessl's free tier. No managed service fees.
In the Hetzner Cloud console, create a new project and click Add Server:
Hit Create & Buy Now. Hetzner provisions the server in about 10 seconds and gives you a public IP.
Vessl is agentless — it operates the server over plain SSH from the control plane. No daemon, no socket exposed, no inbound tunnel. The same SSH key you used during VPS creation is what Vessl uses to provision and deploy. If you ever stop using Vessl, you remove the key and the server is back to a regular Ubuntu box.
Open the Vessl dashboard, head to Servers, and click Connect Server. The wizard runs in two steps:
Step 1 — Network Identity. Give the server a name (e.g. prod-db-1), enter the public IP from Hetzner, and the SSH user/port (root / 22 if you used the defaults).
Step 2 — SSH Key. Pick a key from your Vessl wallet. You have three options:
~/.ssh/authorized_keys on the box) before continuing.Click Check Reachability to confirm Vessl can reach the SSH port, then Verify SSH Credentials to confirm the key works. Both should turn green within a few seconds.
Hit Start Provisioning. Vessl runs the host bootstrap over SSH:
Provisioning takes about 90 seconds end-to-end, with each phase reported live in the dashboard. When it flips to ready, the server is yours to deploy onto.
In the Vessl dashboard, head to Templates and pick PostgreSQL. The template panel asks for two things:
Click Deploy. Vessl:
/var/lib/postgresql/data so the data survives container restartsThe container is healthy within about 15 seconds. The dashboard shows the auto-generated DATABASE_URL you'll plug into your application.
If your app is also deployed on Vessl in the same project, you don't need to copy anything manually. Vessl injects DATABASE_URL (and the individual DB_HOST, DB_USERNAME, DB_PASSWORD vars) into the application container automatically. Frameworks like Laravel, Rails, Django, and Prisma pick them up on first boot.
If your app runs elsewhere, you have two options:
ufw rule on the server, not a dashboard toggle.For most production stacks, keep Postgres internal-only. Public exposure is fine for cases where you genuinely need cross-region access, but always pin the source IP and require sslmode=require on the client.
Persistent volumes survive container restarts but don't survive disk loss. For real durability you need off-server backups.
Managed backups (scheduled pg_dump to S3-compatible storage) are on the Vessl roadmap but not yet shipped. Until they land, the standard pattern is a cron-driven pg_dump that ships the archive to Cloudflare R2 (cheapest at $0.015/GB/month, no egress fees) or any S3-compatible bucket.
Vessl's Cron tab on the server lets you schedule the dump on the host directly:
# Runs daily at 02:00, keeps last 30 dumps in R2
docker exec vessl-postgres pg_dump -U postgres postgres | \
gzip | \
aws s3 cp - s3://my-backup-bucket/postgres-$(date +%F).sql.gz \
--endpoint-url https://<account>.r2.cloudflarestorage.com
Pair it with a lifecycle rule on the bucket to expire archives after 30 days. We'll cover the full setup — including encryption-at-rest and restore drills — in a follow-up post.
A single Postgres instance is a single point of failure. If the Hetzner VPS goes down, your database goes with it. For most early-stage apps that's an acceptable trade — Hetzner's uptime is genuinely good (99.9%+ on most CX-class servers in our experience), and Vessl restarts the container automatically if Postgres crashes.
When you outgrow single-instance, the upgrade path is:
Most apps go years before they need any of this. Don't build it before you have to.
Adding it all up for a real production setup:
| Component | Cost / month |
|---|---|
| Hetzner CX22 VPS | €4.51 |
| Cloudflare R2 backups (5 GB) | $0.08 |
| Domain (Cloudflare Registrar, .com) | $1.00 |
| Vessl free tier | $0 |
| Total |
A managed Postgres of equivalent specs on AWS RDS, Google Cloud SQL, or Supabase Pro lands somewhere between $50 and $90/month. You're trading 10x the price for managed convenience.
If your team would rather spend that delta on engineering than on a managed service, self-hosting on Vessl is a fair trade. If your time costs more than $50/hour and you'd rather not think about Postgres at all, stay managed — that's also a valid call.
Self-hosting doesn't have to mean writing Ansible playbooks. Pick the right control plane, and you get the cheap-and-yours benefits without the operational tax.
Ready to ship?
Vessl deploys containers, provisions SSL, and ships zero-downtime updates to any Linux server you own. No DevOps team required.
Start for free