Coolify v4 Self-Hosted on Hetzner: Complete Production Setup With Backups and Monitoring
Coolify has become the default answer for European teams that want Vercel-style deploys on hardware they control, and Hetzner is where most of them run it. The install itself takes fifteen minutes. A production Coolify Hetzner setup takes closer to a day, because production means the dashboard is not reachable from the public internet, the database backups have survived at least one restore drill, and something pages you when the disk fills up at 2 a.m.
This guide covers that full day of work on Coolify 4.x. If you are still deciding whether Coolify is the right platform at all, our comparison of Coolify, Dokploy, and Kamal covers that decision. Here we assume the decision is made and walk through everything between "empty Hetzner account" and "clients are on it."
What a production Coolify Hetzner setup includes
Six things separate a weekend install from a setup you can defend to a client: a server sized for your actual workload, a firewall that hides the dashboard, TLS on every hostname including the Coolify UI itself, database backups shipped off the server to S3-compatible storage, monitoring with an alert path that works when the whole server is down, and automatic deploys wired to GitHub. We will go through them in that order, then close with the settings that differ between Symfony and Next.js applications on the same instance.
Picking the right Hetzner server
Hetzner reorganized its cloud plans and adjusted prices in June 2026, so check the current price table before you commit. The plan families are stable though: CX plans are cost-optimized shared x86, CPX plans are AMD with more consistent performance, and CAX plans run on Ampere Arm cores with the best price per core in the lineup.
| Plan | Specs | Fits |
|---|---|---|
| CX22 | 2 vCPU, 4 GB RAM | One small app plus Coolify itself. Tight once Postgres joins. |
| CX32 | 4 vCPU, 8 GB RAM | The sweet spot for a small SaaS: app, worker, Postgres, Redis. |
| CAX31 | 8 Arm cores, 16 GB RAM | Several apps or staging plus production on one box, at a lower price per core. |
Two caveats. Coolify itself, with its proxy and helper containers, eats roughly 1 GB of RAM before your first app starts, which is why the CX22 gets cramped so quickly. And the CAX line is arm64, so every image you deploy must be built for Arm or as a multi-arch image. Official images for Postgres, Redis, and Node all are; your own Dockerfiles need a matching build step. If your CI builds on x86 runners and you deploy to CAX, either enable multi-arch builds with buildx or let Coolify build directly on the server.
Pick a German location (Falkenstein or Nuremberg) if data residency is part of your pitch to clients. That single choice does a lot of work in GDPR conversations, and we have written about why in our guide to hosting SaaS in Germany.
Installing Coolify 4
On a fresh Ubuntu 24.04 server, the official install script does the whole job:
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
It installs Docker, starts the Coolify containers, and brings up a Traefik proxy that will later handle TLS for everything. When it finishes, the dashboard is listening on port 8000, and one step should not wait: open the dashboard and create the admin account immediately. Until that account exists, anyone who finds the port can register as the instance owner. Create the account, then close the port, which brings us to the firewall.
Firewall: the dashboard has no business being public
Use the Hetzner Cloud Firewall rather than iptables on the box. It filters traffic at the network edge, survives server rebuilds, and you can read the whole policy in one screen. The rule set for a Coolify host:
Inbound 80, 443 from 0.0.0.0/0 (your apps)
Inbound 22 from your IPs only (SSH)
Inbound 8000 from your IPs only (Coolify dashboard)
Inbound 6001-6002 from your IPs only (Coolify realtime and terminal)
If your team has no stable office IP, put a VPN in front instead of opening the ports wide. Tailscale is the low-effort option: install it on the server, and the dashboard is reachable at the server's Tailscale address from every enrolled laptop while staying invisible to the internet. Finish the job on the SSH side: key-based login only, password authentication off.
One thing this breaks: GitHub cannot deliver webhooks to a firewalled port 8000. The fix is in the deploy section below, and it is a reason to configure your instance domain before wiring up GitHub.
Domains and TLS
Point an A record at the server for each app, add the domain in the application settings, and Traefik requests and renews Let's Encrypt certificates on its own. There is no certbot to babysit.
Do the same for Coolify itself. Set an instance domain like deploy.example.com under settings, and the dashboard moves behind Traefik with a proper certificate on port 443. Combined with the firewall above, you get the arrangement you actually want: the dashboard URL resolves publicly for webhook delivery and OAuth callbacks, but the login page sits behind HTTPS, and ports 8000 and 6001 to 6002 stay closed. If you plan to use preview deployments, add a wildcard record such as *.preview.example.com once, and every pull request gets its own hostname with a certificate at no extra effort.
Backups to Hetzner Object Storage
A database backup that lives on the same disk as the database disappears with it. Ship backups off the server from day one. Hetzner Object Storage is S3-compatible and sits in the same data centers, so it is the natural target: create a bucket in your region, generate credentials, and register it in Coolify under S3 storages with the endpoint, for example https://fsn1.your-objectstorage.com, plus key, secret, and bucket name.
Then enable scheduled backups on each database resource: a cron expression, the S3 destination, and a retention count. For a typical B2B SaaS, nightly dumps with 14 days of retention on production and none on staging is a sensible starting point. Coolify sends a notification when a backup fails, but only if you configure a notification channel, so set up email or Telegram under settings and switch backup failures on. A backup job that has been failing silently for six weeks is a story we have heard more than once in code audits.
The step almost everyone skips: restore once. Create a scratch Postgres on staging, pull yesterday's dump from the bucket, load it, and run your app against it. Until you have done that, you do not know whether your backups work, you only know that files appear in a bucket. Write down how long the restore took; that number is your real recovery time, and clients will ask for it in security questionnaires.
The database question
You have three realistic options for production Postgres. The first is a Coolify-managed database container on the same server, which is what most small teams should start with: latency is a loopback interface, cost is zero beyond the server, and the S3 dump schedule above covers the disaster case. Its limits are honest ones. You get nightly dumps rather than point-in-time recovery, and the database competes with your apps for RAM.
The second option is a second Hetzner server running only Postgres, still self-managed but isolated. Worth it once a runaway app process affecting the database becomes a real risk.
The third is a managed provider. Hetzner itself does not offer managed Postgres, so managed means a third party: Ubicloud sells managed Postgres running on Hetzner hardware in Germany, and Supabase, Neon, and Aiven all have EU regions. You are paying, mostly, for point-in-time recovery, failover, and not being the person who tunes autovacuum. Move here when losing up to 24 hours of data stops being acceptable, and keep the database in the same region as the server, because every query crosses that gap.
Monitoring with Grafana Cloud
Coolify ships with basic metrics: CPU, RAM, and disk per container, visible in the dashboard. That answers "what is happening right now" but nobody watches a dashboard at night, and if the server itself dies, its own monitoring dies with it. You need one alert path that lives somewhere else.
The free tier of Grafana Cloud is enough for a single-server setup. Install node_exporter on the host, run Grafana Alloy or a small Prometheus to remote-write the metrics, and create three alerts: disk usage above 80 percent, memory pressure sustained for 10 minutes, and CPU pegged for 15 minutes. Disk is the one that actually takes Coolify servers down, because Docker images and build caches accumulate until deploys start failing. A weekly docker system prune scheduled task in Coolify plus the disk alert covers it.
Add one uptime check from outside the box, whether a Grafana synthetic check or any external ping service, pointed at a health endpoint of your main app. That is the alert that still fires when the whole server is gone. For the reasoning behind this minimal stack, and what to add as the team grows, see our post on observability for small SaaS teams.
Automatic deploys from GitHub
Register a GitHub App from inside Coolify under sources, grant it access to the repositories you deploy, and pushes to your production branch trigger builds automatically. Because webhooks arrive at your instance domain over 443, the firewalled dashboard port is no obstacle, provided you set the instance domain first.
Turn on preview deployments for the repositories where they pay off. Every pull request then gets a build on its own *.preview hostname, and the link lands in the PR. On a CAX31 you can afford this for a couple of active repositories; on a CX22 the builds will fight your production workload, which is a sizing argument as much as a feature decision.
Symfony and Next.js on the same instance
Running both on one Coolify server works well, and the configuration differs in predictable places.
For Next.js, the work is in the build: standalone output mode, the split between build-time and runtime environment variables, and a health check endpoint so deploys only switch traffic to ready containers. We covered the full checklist in deploying Next.js to Coolify.
Symfony needs three things Next.js does not. The image must contain its own web server, either FrankenPHP or php-fpm behind nginx, since Traefik only proxies. Database migrations belong in a post-deployment command, php bin/console doctrine:migrations:migrate --no-interaction, so they run exactly once per release. And Messenger workers must run as a separate Coolify service from the same image with a messenger:consume command, because a worker inside the web container dies unnoticed. Scheduled console commands go into Coolify's scheduled tasks rather than a crontab in the container, which keeps them visible and logged in one place. The production gotchas of containerizing Symfony apply unchanged here.
The day after the setup
None of the steps above is difficult on its own. The value is in doing all of them before real traffic arrives, because each one is annoying to retrofit under pressure: teams tend to tighten the firewall after the incident and test the backups after the data loss. A Coolify Hetzner setup done properly gives a small team most of what a managed platform sells, at a fraction of the monthly bill and with the data on servers you chose.
If you want a second pair of eyes on your setup, or you are planning the move off a managed platform and want the migration to be boring, this is work we do regularly as part of our tech stack strategy and custom software development engagements. Write to hello@wolf-tech.io or visit wolf-tech.io, and bring your current architecture, we will tell you what we would change.

