Heidelberg AICurriculum
Track 12 · Advanced
12.4

Self-host on a private cloud

Stand up a host you control — Dokploy on Hetzner or on-prem — and promote to it safely

7 lessons 2026-08-06 AI-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
  1. 1Pick where the private host actually lives
  2. 2Keep one copy to break and one to trust
  3. 3Make staging resemble production
  4. 4Isolate prod state — and document what you share on purpose
  5. 5Land changes on a staging branch, not on main
  6. 6Promote a proven build — never re-edit prod by hand
  7. 7Even hotfixes go through staging
  8. 8Don't trust "push succeeded" — check the artifact
  9. 9Look at it on staging with real eyes
  10. 10Know your way back before you promote
How a change reaches users test on a private copy first — promote only what you have checked 1 · Edit on staging branch — not on prod 2 · Staging private · :3012 safe to break 3 · Verify does it look right? check the real page 4 · Production live · public · users keep it boring push merge not right? fix on staging — never touch prod what actually promotes it (this site): git push origin staging → verify http://…:3012 → merge staging → main → live two copies of the same app — one to break, one to trust Staging — the rehearsal private · disposable · no users watching Production — the show public · live · only proven changes Edit · Stage · Verify · Promote — the same change, proven before it goes live.

2Techniques

Learn

Two environments

One to break, one to trust

Pick where the private host actually lives 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.
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 trust Production 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 production Staging 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 purpose Staging 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 main A 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 hand To 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 staging The 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 artifact A 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 eyes The 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 promote Decide 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.1 Create 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.

  1. Log in to the Hetzner Cloud console and create a new server instance using a Linux image.
  2. Assign a public IPv4 address to the instance and note the IP.
  3. Connect via SSH to the server using the provided root credentials.
  4. 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.2 Deploy 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

  1. Click Create Server in the Hetzner Cloud Console and select an EU location
  2. Open a terminal and SSH into the server with its root credentials
  3. Execute the Docker installation script from get.docker.com
  4. Run curl -fsSL https://dokploy.com/install.sh | sh to install Dokploy
  5. Edit the server’s firewall in the Hetzner Cloud Console and add a rule for Port 3000 TCP
  6. 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.3 Deploy 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

  1. Create a new Git branch called staging from main in your repository
  2. In the Dokploy Dashboard, click Add project, select the repository, and link it to the staging branch; toggle Auto‑build on for preview builds
  3. Add another project in the Dashboard, link it to the main branch, and leave Auto‑deploy switched off
  4. Push a test commit to the staging branch and watch Dokploy generate a private preview URL
  5. 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.4 Deploy 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.

  1. On the VM, pull the Nextcloud All‑in‑One Docker image with docker pull nextcloud/all-in-one.
  2. Create a directory on the host to store persistent data and give it appropriate permissions.
  3. Start the container using docker run -d -p 80:80 -v /path/to/data:/var/www/html nextcloud/all-in-one.
  4. 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.5 Expose 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.

  1. Install cloudflared on the VM following the Cloudflare One clientless SSH documentation.
  2. Authenticate cloudflared with your Cloudflare account using cloudflared login.
  3. 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.
  4. 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.6 Promote 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

  1. Verify the feature works as intended in the staging preview
  2. Open a pull request from staging into main
  3. Click Merge pull request after approval
  4. Click Deploy on the Dokploy dashboard or push the merged commit to trigger production deployment
  5. Retrieve /version.json from the live site (e.g., with curl) to confirm the new build identifier
  6. 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.7 Add 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 Git repository with separate staging and production branches.

  1. Clone the official Bitwarden Docker compose repo to /opt/bitwarden on the VM.
  2. Create two Git branches: staging and production. Checkout staging and edit the .env file to set ADMIN_TOKEN=staging-token.
  3. Start the staging stack with docker-compose -f docker-compose.yml up -d while on the staging branch.
  4. 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-to Everyone

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

Pick where your private host lives

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

Edit the live site directly and hope it works

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

Edits go to staging instead of live

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

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".

~5 min · no code Lesson → AI-generated
How-to Everyone

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.

~5 min · no code Lesson → AI-generated
How-to Everyone

Want a quick way to undo a bad deploy

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.

~5 min · no code Lesson → AI-generated

The same set on /recipes, filtered by tool and role.

5Videos 2

6FAQ 7

Why self-host instead of a managed platform like Vercel or Heroku?

Control over the data and the deploy pipeline: 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.

Wikipedia — Deployment environment ↗

Why not just test on production directly?

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.

Wikipedia — Deployment environment ↗

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.

The Twelve-Factor App — Dev/prod parity ↗

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.

Martin Fowler — BlueGreenDeployment ↗

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.)

The Twelve-Factor App — Backing services ↗

How do I know the deploy actually went live?

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.

Wikipedia — Deployment environment ↗

7Glossary 14 terms

Show the 14 terms
Concepts
Private cloud
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-hosted deploy 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.

8See also

💬 Discuss this chapter

Ask, share, or report — over on the Heidelberg AI community forum.