Stand up a host you control — Dokploy on Hetzner or on-prem — and promote to it safely
7 lessons2026-08-06AI-generated
1Overview
Self-hosting means your data never leaves infrastructure you control — a rented EU server (Hetzner) or your own on-prem hardware, running a stack like Dokploy so you get auto-deploys without handing anyone your data. But a private host is only as safe as your deploy discipline: once it has real users, the riskiest move is editing the live site directly. The fix is two environments: production (live, what users see) and a private staging copy you can break. Taught as weak → strong pairs — land changes on staging, verify them, and promote only what you have checked. The advanced example is how this very curriculum site is set up: a staging branch → a private Tailscale preview → merge to main → live.
1.1After this chapter you can
→Stand up a private host you control — Dokploy on a rented EU server (e.g. Hetzner) or on-prem hardware
→Tell staging and production apart — and why you need both, even on your own infrastructure
→Land every change on staging first, then promote the proven build
→Verify a deploy actually went live — don't trust "push succeeded"
1.2Why self‑host my site?
Because it keeps your data inside infrastructure you control, whether that’s a rented EU server or your own hardware, so no third party ever sees it.
1.3How do I avoid breaking the live site?
By using two environments—a production version for users and a private staging copy where you test changes before promoting them to live.
1.4What workflow promotes changes safely?
Develop on a staging branch, preview it through a private Tailscale link, merge to main once verified, then the change goes live automatically via Dokploy.
1.5The moves — weak → strong ladder
1Pick where the private host actually lives
2Keep one copy to break and one to trust
3Make staging resemble production
4Isolate prod state — and document what you share on purpose
5Land changes on a staging branch, not on main
6Promote a proven build — never re-edit prod by hand
7Even hotfixes go through staging
8Don't trust "push succeeded" — check the artifact
9Look at it on staging with real eyes
10Know your way back before you promote
2Techniques
Learn
Two environments
One to break, one to trust
Pick where the private host actually livesBefore staging vs. production even applies, decide where BOTH copies run — a rented EU server, your own hardware, or a managed-but-private platform on top of either.
✕Instead of
Deploy straight to a US-hyperscaler PaaS and hope the data-residency question never comes up.
✓Try this💬 AI chat
Rent a Hetzner (or similar EU) server, or use existing on-prem hardware, and run a self-hosted platform like Dokploy on top of it — auto-deploys and a dashboard, but the host stays yours.
Why it works: A private cloud is not "no infrastructure" — it is infrastructure you can name, audit and evict data from on demand. Dokploy on a rented Hetzner box gets you most of a managed platform's convenience (git-triggered builds, one dashboard, rollbacks) while the actual server stays under your control — the same buy-vs-build trade-off the governance chapter runs, applied to the host itself.
Keep one copy to break and one to trustProduction is the live site users interact with. Staging is a private copy that resembles it — there to absorb your mistakes.
✕Instead of
Edit the live site directly and hope the change is fine.
✓Try this💬 AI chat
Run two copies: production (live, what users see) and staging (a private copy that mirrors it). Make every change on staging first; promote only what you have checked.
Why it works: A staging environment "exactly resembles a production environment" but no users are watching it — so a broken build there costs you a re-deploy, not an outage. Production is the one place a mistake is felt immediately.
Make staging resemble productionStaging only predicts prod if it is built the same way. The bigger the gap, the more "worked on staging, broke in prod" surprises.
✕Instead of
Test on your laptop with a different OS, database and config, then deploy to a totally different prod.
✓Try this💬 AI chat
Build staging from the SAME image/Dockerfile and config as prod, changing only what must differ (the URL, secrets). Then "passes on staging" really means "will pass in prod".
Why it works: The twelve-factor "dev/prod parity" rule warns about three gaps that make testing lie: the time gap, the personnel gap, and the tools gap (different stacks). Closing them is what makes a staging check trustworthy.
Isolate prod state — and document what you share on purposeStaging must not be able to corrupt real data. Anything it deliberately shares with prod should be a conscious, written-down choice.
✕Instead of
Point staging at the production database, so a test run quietly mutates real user data.
✓Try this💬 AI chat
Give staging its own data. Share a prod resource only on purpose and record it. (This site shares exactly one thing — a vanity hit-counter — and nothing that matters; it is noted in DEPLOY.md.)
Why it works: The whole value of a throwaway environment evaporates if breaking it breaks prod too. Default to isolation; treat every shared resource as a decision you can defend, not an accident.
The promotion flow
Stage it, don't edit prod
Land changes on a staging branch, not on mainA long-lived staging branch is where edits go first; pushing it auto-builds the private preview. Main stays clean until you promote.
✕Instead of
git push origin main # straight to the live site
✓Try this❯_ Terminal
git push origin staging # auto-builds the private :3012 preview; main (prod) is untouched
Why it works: Separating the branches separates the environments. The thing on main is always "what is live"; the thing on staging is "what you are trying" — so you can never accidentally ship a half-finished edit.
Promote a proven build — never re-edit prod by handTo go live, move the exact change you verified — not a fresh hand-made copy of it. Promotion = merge, not re-typing.
✕Instead of
SSH into the prod server and re-make the change there by hand.
✓Try this💬 AI chat
Merge staging → main. The byte-for-byte change you checked on staging is what builds for prod; no hand edits, no drift.
Why it works: Blue-green deployment makes the same point at the infrastructure level: you bring up the new version, verify it, then switch traffic to it — and switch back if it misbehaves. Promoting an artifact you already tested beats reproducing it live.
Even hotfixes go through stagingThe temptation to skip staging is strongest exactly when you are panicking — which is when a typo does the most damage.
✕Instead of
It's urgent — push the fix straight to prod, no time to test.
✓Try this💬 AI chat
Make the hotfix on staging, glance at :3012 to confirm it works, then promote. The one-minute detour is what stops a 2am one-character outage.
Why it works: Urgency raises the error rate and the stakes at once. A staging pass is cheap insurance precisely when you are least careful — the rule that has no exceptions is the one that protects you under pressure.
Verify before you promote
Don't trust the push
Don't trust "push succeeded" — check the artifactA green push and green CI mean the build compiled, not that the new version is actually serving. Confirm the deployed thing changed.
✕Instead of
CI is green, so the new version must be live.
✓Try this❯_ Terminal
curl the deployed /version.json (or the changed page) and confirm the build/SHA actually advanced before you call it done.
Why it works:Deploys fail silently in real ways — a cached image, a stalled auto-pull, a recreated container. The only honest signal is the artifact itself reporting the new version; "it pushed" is not "it shipped".
Look at it on staging with real eyesThe preview tier exists to be opened. Read the page and click the exact thing you changed — do not approve from the diff alone.
✕Instead of
The diff looks right, so merge it to prod.
✓Try this💬 AI chat
Open the staging URL, click the actual button / read the actual paragraph you touched, and confirm it behaves — then promote.
Why it works: A diff shows intent, not result: rendering, layout, a broken link, or a copy button that silently fails only show up in the running page. Staging is where you catch those for free, before users do.
Know your way back before you promoteDecide how you would undo a change before you ship it, so a bad prod deploy is a 30-second revert, not a scramble.
✕Instead of
If prod breaks after the merge, start figuring out how to undo it.
✓Try this❯_ Terminal
git revert HEAD && git push origin main # prod rebuilds the previous version automatically
Why it works: Blue-green deployments keep the old environment idle so you can "switch the router back" instantly. A revert-and-rebuild is the small-scale version of the same safety net: every promotion should have a known, fast undo.
3Lessons 7
3.1Create a virtual machine on Hetzner and prepare it for Docker
A fresh Linux VM that will host your private services.
You will have an SSH‑accessible server with Docker installed ready for deployments.
Log in to the Hetzner Cloud console and create a new server instance using a Linux image.
Assign a public IPv4 address to the instance and note the IP.
Connect via SSH to the server using the provided root credentials.
Install Docker by running the official installation script from docker.com.
You'll see The VM responds to docker version showing client and server details, confirming Docker is ready.
Takeaway Provisioning a clean host and installing container tooling provides a reproducible base for any self‑hosted stack.
3.2Deploy Dokploy to a private Hetzner server
Dokploy is an auto‑deploy dashboard that runs on your own VM and pulls builds from Git.
Run a Dokploy instance on a rented EU server you control
Click Create Server in the Hetzner Cloud Console and select an EU location
Open a terminal and SSH into the server with its root credentials
Execute the Docker installation script from get.docker.com
Run curl -fsSL https://dokploy.com/install.sh | sh to install Dokploy
Edit the server’s firewall in the Hetzner Cloud Console and add a rule for Port 3000 TCP
Open a browser and navigate to http://<server‑ip>:3000 to view the dashboard
You'll see A reachable Dokploy dashboard showing the server’s name and a button to add new projects
Takeaway Self‑hosting gives you full control over where your code runs and keeps data residency under your governance
Check What firewall rule must you add in the Hetzner Cloud Console to make the Dokploy dashboard reachable on its default port?
3.3Deploy separate staging and production sites from Git branches
A long‑lived staging branch that triggers private preview builds while the main branch represents live production.
Create two independent deployments—one for staging, one for production—each linked to its own branch
Create a new Git branch called staging from main in your repository
In the Dokploy Dashboard, click Add project, select the repository, and link it to the staging branch; toggle Auto‑build on for preview builds
Add another project in the Dashboard, link it to the main branch, and leave Auto‑deploy switched off
Push a test commit to the staging branch and watch Dokploy generate a private preview URL
Verify that no new deployment appears for the main branch while the staging preview reflects the change
You'll see A private preview URL updates with the staging commit while the production dashboard shows no new deployment
Takeaway Branch‑based separation keeps changes out of users’ hands until they are deliberately promoted
Check How does enabling Auto‑build for the staging project while keeping Auto‑deploy disabled for the main branch influence what happens after you push commits to each branch?
3.4Deploy Nextcloud with the official All‑in‑One Docker image
Nextcloud, an open‑source file sync and collaboration platform.
You will have a running Nextcloud instance accessible via a web browser.
On the VM, pull the Nextcloud All‑in‑One Docker image with docker pull nextcloud/all-in-one.
Create a directory on the host to store persistent data and give it appropriate permissions.
Start the container using docker run -d -p 80:80 -v /path/to/data:/var/www/html nextcloud/all-in-one.
Open a browser to the server’s IP address, follow the web installer prompts, and create an admin account.
You'll see The Nextcloud setup wizard loads in the browser and after completion you can log into the dashboard.
Takeaway Containerizing applications like Nextcloud simplifies installation, updates, and data persistence on private infrastructure.
3.5Expose Nextcloud securely with a Cloudflare clientless SSH tunnel
A Cloudflare Tunnel that provides encrypted, browser‑based access without opening firewall ports.
You will be able to reach your Nextcloud instance through a private Cloudflare URL.
Install cloudflared on the VM following the Cloudflare One clientless SSH documentation.
Authenticate cloudflared with your Cloudflare account using cloudflared login.
Create a tunnel that forwards traffic to port 80 with cloudflared tunnel create nextcloud-tunnel and then cloudflared tunnel route dns nextcloud-tunnel .cfargotunnel.com.
Run the tunnel in the background using cloudflared tunnel run nextcloud-tunnel.
You'll see Visiting .cfargotunnel.com loads the Nextcloud login page over an encrypted Cloudflare tunnel.
Takeaway Cloudflare tunnels let you expose services safely without altering network firewalls, ideal for private‑cloud deployments.
3.6Promote a verified change to production
Promotion is merging the tested staging branch into main, causing Dokploy to deploy the exact artifact that passed staging.
Move a verified build from staging to production and confirm that the new version is serving live traffic
Verify the feature works as intended in the staging preview
Open a pull request from staging into main
Click Merge pull request after approval
Click Deploy on the Dokploy dashboard or push the merged commit to trigger production deployment
Retrieve /version.json from the live site (e.g., with curl) to confirm the new build identifier
If needed, run git revert on the latest production commit and redeploy
You'll see The version JSON reports the new build hash and the live site shows the changes verified in staging
Takeaway Promoting an exact tested artifact eliminates manual re‑creation errors and provides a fast, reliable rollback path
Check Which endpoint do you request on the live site to confirm that the build promoted from staging is now serving production traffic?
3.7Add a self‑hosted Bitwarden organization on the same server and promote changes via Git
Bitwarden, an open‑source password manager that can be run as a private service.
You will have Bitwarden running in Docker, managed from a Gitrepository with separate staging and production branches.
Clone the official Bitwarden Docker compose repo to /opt/bitwarden on the VM.
Create two Git branches: staging and production. Checkout staging and edit the .env file to set ADMIN_TOKEN=staging-token.
Start the staging stack with docker-compose -f docker-compose.yml up -d while on the staging branch.
After verifying functionality, merge staging into production, change ADMIN_TOKEN to a production value, and redeploy by running docker-compose down && docker-compose up -d on the production branch.
You'll see Two Bitwarden instances are reachable (one via the staging subdomain, one via the production subdomain) each requiring its respective admin token.
Takeaway Using Git branches to separate staging and production configurations enforces a disciplined promotion workflow for self‑hosted services.
4FAQ, Tips & How-to 10
one problem, one solution, one action
▸How-toEveryone
Staging can corrupt real data
Staging must not be able to corrupt real data. Anything it deliberately shares with prod should be a conscious, written-down choice. The whole value of a throwaway environment evaporates if breaking it breaks prod too. Default to isolation; treat every shared resource as a decision you can defend, not an accident.
Staging differs from production → test passes don’t mean anything
Staging only predicts prod if it is built the same way. The bigger the gap, the more "worked on staging, broke in prod" surprises. The twelve-factor "dev/prod parity" rule warns about three gaps that make testing lie: the time gap, the personnel gap, and the tools gap (different stacks). Closing them is what makes a staging check trustworthy.
Before staging vs. production even applies, decide where BOTH copies run — a rented EU server, your own hardware, or a managed-but-private platform on top of either. A private cloud is not "no infrastructure" — it is infrastructure you can name, audit and evict data from on demand. Dokploy on a rented Hetzner box gets you most of a managed platform's convenience (git-triggered builds, one dashboard, rollbacks) while the actual server stays under your control — the same buy-vs-build trade-off the governance chapter runs, applied to the host itself.
Production is the live site users interact with. Staging is a private copy that resembles it — there to absorb your mistakes. A staging environment "exactly resembles a production environment" but no users are watching it — so a broken build there costs you a re-deploy, not an outage. Production is the one place a mistake is felt immediately.
A long-lived staging branch is where edits go first; pushing it auto-builds the private preview. Main stays clean until you promote. Separating the branches separates the environments. The thing on main is always "what is live"; the thing on staging is "what you are trying" — so you can never accidentally ship a half-finished edit.
The temptation to skip staging is strongest exactly when you are panicking — which is when a typo does the most damage. Urgency raises the error rate and the stakes at once. A staging pass is cheap insurance precisely when you are least careful — the rule that has no exceptions is the one that protects you under pressure.
Want to push a verified change to prod without manual edits
To go live, move the exact change you verified — not a fresh hand-made copy of it. Promotion = merge, not re-typing. Blue-green deployment makes the same point at the infrastructure level: you bring up the new version, verify it, then switch traffic to it — and switch back if it misbehaves. Promoting an artifact you already tested beats reproducing it live.
A green push and green CI mean the build compiled, not that the new version is actually serving. Confirm the deployed thing changed. Deploys fail silently in real ways — a cached image, a stalled auto-pull, a recreated container. The only honest signal is the artifact itself reporting the new version; "it pushed" is not "it shipped".
The preview tier exists to be opened. Read the page and click the exact thing you changed — do not approve from the diff alone. A diff shows intent, not result: rendering, layout, a broken link, or a copy button that silently fails only show up in the running page. Staging is where you catch those for free, before users do.
Decide how you would undo a change before you ship it, so a bad prod deploy is a 30-second revert, not a scramble. Blue-green deployments keep the old environment idle so you can "switch the router back" instantly. A revert-and-rebuild is the small-scale version of the same safety net: every promotion should have a known, fast undo.
How real startups use a staging environment as a safety net before changes reach live users.
6FAQ 7
Why self-host instead of a managed platform like Vercel or Heroku?
Control over the data and the deploypipeline: nothing routes through a third party's infrastructure. A stack like Dokploy on a rented EU server (Hetzner) or on-prem hardware gets you back most of the managed-platform convenience — git push, auto-build, one dashboard, rollbacks — while the host itself stays yours. It costs setup time a managed PaaS would absorb for you; the trade-off is the same buy-vs-build call this curriculum makes for AI tools, applied to where the app itself runs.
What is the difference between staging and production?
Production is the live environment "users directly interact with". Staging is a pre-production environment "that exactly resembles a production environment" but is private — it exists so you can install, configure and test a change before it reaches anyone.
Because every test then risks a real outage or real data loss, in front of real users. A private staging copy absorbs the mistakes for free: you break it, fix it, and only promote what you have proven. Production should only ever see changes that already worked somewhere else.
How similar does staging need to be to production?
As similar as you can make it. The twelve-factor "dev/prod parity" principle is "Keep development, staging, and production as similar as possible," and warns of three gaps that make testing lie: the time gap (code waits to ship), the personnel gap (different people deploy), and the tools gap (different OS/database/stack). The smaller the gaps, the more a staging pass actually predicts prod.
What does "promote" mean — why not just edit prod once it works on staging?
Promotion moves the exact build you verified, rather than re-making the change by hand on prod (which drifts and reintroduces bugs). Blue-green deployment is the same idea at the infrastructure level: bring up the new version, verify it, switch traffic to it — and switch back to the old one if anything goes wrong. Ship the artifact you tested.
Do staging and production need separate databases?
By default, yes — staging must not be able to corrupt real data, or breaking it defeats the purpose. Share a production resource only deliberately and write it down. (This site shares exactly one harmless thing between the tiers — a vanity page-hit counter — and nothing that matters; that choice is documented.)
Check the artifact, not the push. A green push and green CI only mean the build compiled — the new version can still fail to serve (a cached image, a stalled pull, a recreated container). curl the deployed /version.json (or the changed page) and confirm the SHA/build advanced past your change before you call it done.
Infrastructure you control — a rented server (e.g. Hetzner), your own on-prem hardware, or both — as opposed to a third-party SaaS platform that also holds your data.
Dokploy
An open-source, self-hosteddeploy platform (Docker + Traefik + a dashboard) you install on your own server — gives most of a managed PaaS's convenience without leaving your infrastructure.
Environment
A complete running copy of your app (servers, data, config). Most teams run at least two: production and staging.
Production (prod)
The live environment users directly interact with — also called "live". A mistake here is felt immediately.
Staging
A private pre-production environment that resembles prod as closely as possible — for testing a change before it goes live.
Dev/prod parity
Keeping development, staging and production as similar as possible, so a test on one predicts behaviour on the other (twelve-factor).
Deploy
To build and run a version of the app in an environment.
Promotion
Moving the exact, already-verified build from one environment to the next (staging → production) — not re-making the change by hand.
Rollback
Reverting production to its previous working version after a bad deploy. Plan it before you promote.
Blue-green deployment
Two identical production environments; you deploy and verify on the idle one, switch traffic to it, and switch back to roll back. A related promotion pattern.
This site
staging branch
The git branch edits land on first; pushing it auto-builds the private preview. Prod lives on `main`.
:3012 (Tailscale)
The staging preview URL, bound to the private Tailscale IP only — reachable by the team, invisible to the public internet.
version.json
A small file the build bakes in (SHA + build time) so you can curl a deployed site and confirm which version is actually serving.
-p (compose project)
A docker compose project name (e.g. cos-curriculum-staging) that keeps staging’s containers isolated from prod’s.