Self-hosting guides tend to read like shopping lists: here are twenty tools, here are their docker run commands, go forth. That's the fun part and the smallest part.
The tools are easy. Running them is the thing — and the difference between a stack that's still healthy in a year and one that's a source of quiet dread is almost never tool selection. It's whether you got the boring foundations right. So this guide spends real time on those, and is honest about when you shouldn't self-host at all.
The trade you're actually making
You're not trading money for free software. You're trading a predictable monthly bill for an unpredictable claim on your attention.
The real total cost:
server(s) — small, and the only part people budget for
+ backup storage — small
+ your time on initial setup — a weekend, honestly
+ your time on updates forever — the real cost
+ your time when it breaks — always at the worst moment
+ the risk you're now carrying — you are the on-call engineer
That last line is the one to sit with. When your Git host goes down at your provider, someone else is awake fixing it, and you complain on the status page. When your self-hosted Git host goes down, you are that someone, and your team is blocked until you're finished.
Be honest with yourself against this list:
- You'd be self-hosting your team's critical path with no redundancy. If your Git server dying stops five engineers working, and it lives on one VPS you administer alone in your spare time, that's a business risk wearing a hobbyist's clothes.
- Nobody owns it. A stack maintained by whoever has time is a stack that will be six months behind on security patches by summer. Self-hosting needs a name attached, not a team's good intentions.
- You're doing it purely to save money on a small team. A few SaaS seats cost less than the hours you'll spend, and your hours are worth more than you charge yourself for them.
- You have compliance requirements you don't yet understand. Self-hosting doesn't automatically make you compliant. It moves every obligation onto you — including the ones you haven't read yet.
The good reasons are narrower and real: data residency you can't negotiate, per-seat pricing that has genuinely become absurd at your headcount, a need to work offline or air-gapped, wanting to escape a vendor that changed terms on you — and, legitimately, wanting to learn. A homelab is a fine reason to self-host. Just don't confuse it with a production decision.
Foundations first
Get these three right and every tool below becomes easy. Get them wrong and no amount of good tooling saves you.
1. Docker Compose as your source of truth
Ad-hoc docker run commands are how you end up with a server nobody can rebuild. Every service goes in a Compose file, every Compose file goes in Git. Your infrastructure becomes reviewable, diffable, and restorable.
2. A reverse proxy that handles TLS for you
Caddy gets automatic HTTPS via Let's Encrypt with essentially no configuration — it provisions and renews certificates on its own. Traefik is the other common choice, with stronger dynamic service discovery. Either ends the era of expired certificates.
Here's a foundation that actually works — Docker Compose with Caddy terminating TLS in front of a Git host and monitoring:
# compose.yaml
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # certificates live here — back this up
- caddy_config:/config
networks: [web]
forgejo:
image: codeberg.org/forgejo/forgejo:9
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=db:5432
- FORGEJO__database__NAME=forgejo
- FORGEJO__database__USER=forgejo
- FORGEJO__database__PASSWD=${FORGEJO_DB_PASSWORD}
volumes:
- forgejo_data:/data
networks: [web, internal]
depends_on: [db]
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=forgejo
- POSTGRES_PASSWORD=${FORGEJO_DB_PASSWORD}
- POSTGRES_DB=forgejo
volumes:
- db_data:/var/lib/postgresql/data
networks: [internal] # no route to the internet
uptime-kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- kuma_data:/app/data
networks: [web]
networks:
web:
internal:
internal: true
volumes:
caddy_data:
caddy_config:
forgejo_data:
db_data:
kuma_data:
# Caddyfile — automatic HTTPS, no cert config
git.example.com {
reverse_proxy forgejo:3000
}
status.example.com {
reverse_proxy uptime-kuma:3001
}
Three details in there carry most of the value. internal: true on the database network means Postgres has no route out — a container escape doesn't get a free egress path. Secrets come from the environment, not the file, so the Compose file is safe to commit. And restart: unless-stopped means a host reboot doesn't require you.
3. Backups you have actually restored
A backup you have never restored is not a backup. It's a hypothesis. Most self-hosted disasters aren't "we had no backups" — they're "we had backups and discovered on the worst day that they'd been silently failing since March."
The 3-2-1 rule still holds: three copies, on two kinds of media, one off-site. For a small stack that means volume snapshots, plus an encrypted copy pushed to object storage somewhere that isn't your provider.
Practical rules that matter more than the tool you pick:
- Dump databases properly. Copying a Postgres data directory while it's running gives you a file that may or may not restore. Use
pg_dump, or snapshot with the database quiesced. - Automate a restore drill. Quarterly, restore to a scratch host and confirm the thing boots with your data in it. Put it in the calendar; it will never happen otherwise.
- Monitor the backup job itself. A cron job that fails silently is the default failure mode. Have it ping a dead-man's-switch — Uptime Kuma can watch for a heartbeat that stops arriving, which is exactly what you want here.
- Encrypt before it leaves the host. Tools like restic and Borg do this by default and deduplicate, which keeps the storage bill trivial.
Git hosting
Forgejo and Gitea
Gitea earned its reputation as the lightweight self-hosted Git server: written in Go, undemanding enough to run on very modest hardware — a small VPS or a Raspberry Pi is genuinely fine — and covering the GitHub features most teams actually use (issues, pull requests, releases, wiki, packages, and a CI runner).
The governance story is worth knowing before you commit. In 2022 the project moved under a for-profit company, and part of the community forked it as Forgejo, now developed under the non-profit Codeberg e.V. The two remain broadly similar in day-to-day use. Forgejo's pitch is community governance; Gitea's is commercial backing. For most teams either works — pick based on which trade you prefer, and know that this kind of licence-and-governance shift is exactly the risk self-hosters are trying to insure against in the first place.
GitLab CE
GitLab Community Edition is the other end of the spectrum: an entire DevOps platform — repos, CI/CD, container registry, issue tracking, security scanning — in one deployment. It's genuinely comprehensive, and correspondingly hungry; it wants substantially more RAM than Forgejo and is a heavier thing to upgrade. Choose it when you want one integrated platform and have the hardware. Choose Forgejo when you want a Git server that gets out of the way.
CI/CD
Forgejo and Gitea both ship an Actions-compatible runner, which is a big deal in practice: workflows written for GitHub Actions largely work, so your team's existing knowledge transfers.
Woodpecker CI (a community fork of Drone) is the lightweight standalone option — YAML pipelines, containers for steps, minimal ceremony:
# .woodpecker.yaml
when:
- event: [push, pull_request]
steps:
- name: test
image: node:22-alpine
commands:
- npm ci
- npm run lint
- npm test
- name: build
image: node:22-alpine
commands:
- npm run build
when:
- branch: main
Jenkins remains the answer when your needs are strange — unusual hardware, legacy integrations, workflows nothing else supports. Its plugin ecosystem is unmatched and is also its liability: plugins are a maintenance and security surface you now own. Pick Jenkins deliberately, not by default.
CI runners execute arbitrary code from your repositories with access to your secrets — by design. Run them on separate hosts from the things they deploy to, give them short-lived credentials rather than long-lived ones, and never let a runner handling pull requests from outside your organisation touch production secrets.
Observability
Don't skip this because the stack is small. The whole point of self-hosting is that you're the on-call engineer, and an on-call engineer without monitoring is just someone who finds out from users.
Start with Uptime Kuma. It takes minutes, has a clean UI, supports HTTP/TCP/DNS checks and a long list of notification integrations, and it will catch the majority of what goes wrong in a small stack — because the majority of what goes wrong is "the thing stopped responding."
Add Prometheus and Grafana when you need to know why, not just that. Prometheus scrapes metrics; Grafana draws them; node_exporter gives you host-level data (disk, memory, CPU) for free. Alert on disk space early — a full disk is the single most common way a self-hosted stack dies, and it kills databases in ugly ways.
Docs, projects, and the rest
- Outline — a fast, polished team wiki with real-time collaboration and good search. The closest self-hosted feel to a commercial product.
- BookStack — documentation organised into shelves, books, chapters, pages. Rigid, which is the point when you want structure enforced rather than hoped for.
- Plane — issue tracking with a modern, Linear-like interface. Actively developing; expect movement.
- Focalboard — Kanban and table views from the Mattermost project.
- Plausible / Umami — privacy-focused, cookie-free web analytics. Umami is the lighter deploy; both spare you a consent banner, which is a real feature.
- Mattermost — team chat when Slack's per-seat pricing or data location has become a problem.
- HashiCorp Vault — secrets management. Worth flagging: Vault is powerful and genuinely operationally demanding, and if you lose your unseal keys you lose your secrets. For a small stack, encrypted environment files with disciplined access are often the more honest choice. Self-hosting Vault to avoid managing secrets can leave you managing something harder.
Security: keep the attack surface small
The instinct is to give every service a public domain. Resist it.
Most of your stack does not need to be on the public internet. Put your wiki, your dashboards, your project tracker, and your admin UIs behind a VPN — Tailscale or WireGuard — and expose only what genuinely needs to be reachable by outsiders, like a status page. This single decision removes most of your exposure, and it's easier than hardening each service individually.
Beyond that, the basics that actually get exploited when skipped: keep images updated (Watchtower automates it, though on anything important you want deliberate upgrades over surprise ones), enforce MFA on every service that supports it, run containers as non-root, never expose a database port to the host, and read the release notes before upgrading anything holding data.
A realistic starting stack
Don't build all of this. Build this, in this order, and stop when it's enough:
| Step | What | Why first | |---|---|---| | 1 | A VPS, Docker Compose, Caddy | The foundation everything sits on | | 2 | Backups + a tested restore | Before you have data worth losing | | 3 | Uptime Kuma | You're on-call now; act like it | | 4 | Forgejo | The highest-value single service | | 5 | CI runner | Once Git is stable, not before | | 6 | Everything else | Only when something actually hurts |
Steps 1–3 before step 4 is the part people invert, and it's why the horror stories exist. Set up the safety net before the trapeze.
The bottom line
Self-hosting in 2026 is more approachable than it's ever been. Containers made deployment reproducible, Caddy made TLS a non-event, and projects like Forgejo and Uptime Kuma are genuinely pleasant software.
But it remains a commitment, not a purchase. Start with one service you'd survive losing for a day. Live with it for a month — feel the upgrades, the backups, the 11pm disk-space alert. Then decide whether to move something that matters. The people who succeed at self-hosting aren't the ones who deployed twenty services in a weekend. They're the ones still running three of them, well, two years later.