commit 06f8e8812ea538d5bc54fc5dd1835d4a25e76fee Author: Maximilian Grimm Date: Sun Aug 16 19:29:58 2026 +0200 Initial stack: Caddy, Forgejo, Outline, Vaultwarden, Uptime Kuma, n8n diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..a8c3617 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,30 @@ +# ~$ selfhostedops — Caddyfile +# Automatic HTTPS for every subdomain. Nothing to renew, ever. + +{ + email {$ACME_EMAIL} +} + +git.{$DOMAIN} { + reverse_proxy forgejo:3000 +} + +wiki.{$DOMAIN} { + reverse_proxy outline:3000 +} + +vault.{$DOMAIN} { + reverse_proxy vaultwarden:80 +} + +status.{$DOMAIN} { + reverse_proxy uptime-kuma:3001 +} + +n8n.{$DOMAIN} { + reverse_proxy n8n:5678 +} + +vpn.{$DOMAIN} { + reverse_proxy wg-easy:51821 +} diff --git a/HARDENING.md b/HARDENING.md new file mode 100644 index 0000000..5636eb4 --- /dev/null +++ b/HARDENING.md @@ -0,0 +1,94 @@ +# ~$ Hardening checklist + +This is the non-negotiable baseline before real company data touches the box. +Every item is copy-paste ready for Debian/Ubuntu. + +## 1. SSH: keys only, no root + +```bash +adduser ops && usermod -aG sudo,docker ops +mkdir -p /home/ops/.ssh && cp ~/.ssh/authorized_keys /home/ops/.ssh/ \ + && chown -R ops:ops /home/ops/.ssh && chmod 700 /home/ops/.ssh +``` + +`/etc/ssh/sshd_config.d/hardening.conf`: + +``` +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +``` + +```bash +systemctl restart ssh +# TEST A NEW SSH SESSION BEFORE CLOSING THIS ONE. +``` + +## 2. Firewall — four ports, nothing else + +```bash +apt install -y ufw +ufw default deny incoming +ufw allow 80/tcp 443/tcp 443/udp # caddy (http/https/h3) +ufw allow 2222/tcp # git ssh +ufw allow 51820/udp # wireguard (only if using --profile vpn) +ufw allow 22/tcp # your ssh (move it if you want) +ufw enable +``` + +Note: Docker's published ports bypass ufw INPUT rules — that's exactly why this +compose file publishes only caddy, git-ssh and wireguard, and keeps postgres/redis +on the `internal` network with no ports at all. Don't add `ports:` to internal +services "for debugging" and forget them. + +## 3. Automatic security updates + +```bash +apt install -y unattended-upgrades +dpkg-reconfigure -plow unattended-upgrades +``` + +## 4. fail2ban (ssh brute force) + +```bash +apt install -y fail2ban # default ssh jail is enough to start +``` + +## 5. Vaultwarden specifics + +- `VAULTWARDEN_SIGNUPS_ALLOWED=false` the moment your team is on board. +- Use an argon2 `ADMIN_TOKEN` (`vaultwarden hash` / see wiki), not a plain string. +- The admin panel (`/admin`) is for setup — consider blocking it in Caddy afterwards: + +``` +vault.{$DOMAIN} { + @admin path /admin* + respond @admin 404 + reverse_proxy vaultwarden:80 +} +``` + +## 6. Pin your images + +`latest` is fine on day one, dangerous on day 300. Pin majors at minimum +(`caddy:2-alpine`, `forgejo:11`, `uptime-kuma:1` already are). Watchtower blindly +auto-updating a password manager at 3 a.m. is how you get incidents — update +deliberately: `docker compose pull && docker compose up -d` after reading release notes. +n8n and Outline move fast; Vaultwarden occasionally has breaking web-vault pairs. + +## 7. Backups (minimum viable, today) + +```bash +# stop, snapshot volumes, start — cron it nightly until Episode 10 (borgmatic offsite) +docker compose stop +tar czf /backup/stack-$(date +%F).tgz -C /var/lib/docker/volumes . +docker compose start +``` + +Plus: enable your provider's server snapshots. Two clicks, saves your company. + +## 8. What this list is NOT + +Not covered here, on purpose (own episodes): SSO everywhere, 2FA enforcement +per service, Netbird zero-trust mesh, central logging, CrowdSec, offsite borg. +Baseline first. Perfect later. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eae7fdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 SelfHostedOps + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d81db3b --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# ~$ selfhostedops-stack + +**The €15/month self-hosted company stack.** Git hosting, wiki, password manager, +monitoring, automation and VPN — one server, one compose file, no per-seat pricing. + +From the video: **[Self-Hosted Company Stack for €15/Month (Full Setup)](https://youtube.com/@selfhostedops)** +— Series *Self-Hosted Company OS*, Episode 1. + +``` + ┌──────────────────────────────────────┐ + │ caddy :80/:443 │ + │ automatic HTTPS for *.DOMAIN │ + └──┬─────┬───────┬───────┬──────┬──────┘ + │ │ │ │ │ + git.─────► │ wiki.──► │ status.──► │ + │ │ │ │ │ + ┌────▼─┐ ┌─▼────┐ ┌▼─────┐ ┌▼───┐ ┌▼────┐ + │forge │ │outline│ │vault │ │kuma│ │ n8n │ + │ jo │ │+pg+redis│warden│ │ │ │ │ + └──────┘ └──────┘ └──────┘ └────┘ └─────┘ + also: wg-easy (optional, --profile vpn) +``` + +| Service | URL | Replaces | Typical SaaS price | +|---|---|---|---| +| [Forgejo](https://forgejo.org) | `git.…` | GitHub Team | $4/user/mo | +| [Outline](https://www.getoutline.com) | `wiki.…` | Notion | $10/user/mo | +| [Vaultwarden](https://github.com/dani-garcia/vaultwarden) | `vault.…` | 1Password | $8/user/mo | +| [Uptime Kuma](https://github.com/louislam/uptime-kuma) | `status.…` | Pingdom & co. | $20+/mo | +| [n8n](https://n8n.io) | `n8n.…` | Zapier | $30+/mo | +| [wg-easy](https://github.com/wg-easy/wg-easy) | `vpn.…` | Tailscale | $6/user/mo | + +All images are multi-arch (amd64 + arm64) — a €14 ARM cloud box (e.g. Hetzner +CAX31) runs the whole thing with room to spare. + +## Quickstart + +```bash +# 0. A Debian/Ubuntu server with Docker: +curl -fsSL https://get.docker.com | sh + +# 1. DNS: ONE wildcard record *.ops.example.com -> your server IP (A + AAAA) + +# 2. Configure +git clone https://github.com/selfhostedops/selfhostedops-stack.git +cd selfhostedops-stack +cp .env.example .env +nano .env # domain, email, 3x openssl rand -hex 32 + +# 3. Launch +docker compose up -d +``` + +Ninety seconds later: `git.` `wiki.` `vault.` `status.` `n8n.` — all with valid HTTPS. + +## First-run order (5 minutes) + +1. **Forgejo** (`git.…`): create the admin account on the setup page. + Registration is already disabled via env — this is a company server. +2. **Forgejo → SSO for the wiki:** Site Administration → Applications → + Create OAuth2 app. Redirect URI: `https://wiki./auth/oidc.callback`. + Put the client id/secret into `.env`, then `docker compose up -d outline`. +3. **Outline** (`wiki.…`): click "Continue with Forgejo". Your git server is + your SSO provider — one account, both tools. +4. **Vaultwarden** (`vault.…`): create your account, install the Bitwarden + apps against `https://vault.`. When the team is on board: + `VAULTWARDEN_SIGNUPS_ALLOWED=false` and re-up. +5. **Uptime Kuma** (`status.…`): create admin, add HTTPS monitors for the + other five subdomains. Meta, but correct. +6. **n8n** (`n8n.…`): create the owner account. This thing becomes the brain + of the stack in later episodes (AI agents incoming). + +## FAQ + +**Why no Netbird? You said mesh VPN.** +Full self-hosted Netbird (management plane, signal, TURN, IdP) deserves its own +video — that's Episode 5. `wg-easy` gets you a working WireGuard VPN today with +one env var. Skip both if you don't need remote access yet. + +**Backups?** +`docker compose down`, snapshot/borg the named volumes, `up -d`. A proper +3-2-1 setup (borgmatic + offsite) is Episode 10. Until then: at minimum enable +your host provider's snapshot backups. + +**Can I run only some services?** +Yes — comment out what you don't need in `compose.yml` and remove its Caddyfile +block. Everything is independent except Outline → Forgejo (SSO) and +Outline → its postgres/redis. + +**Is `latest` okay for images?** +For a homelab: pragmatic. For production: pin versions and read HARDENING.md. + +## Hardening + +Before you put real company data on this box, read **[HARDENING.md](HARDENING.md)**. +The short version: SSH keys only, firewall (80/443/2222/51820 only), unattended +security updates, fail2ban, pinned image tags, offsite backups. + +--- + +MIT — do whatever. If it saved you money, a YouTube subscription is the price: +**[@selfhostedops](https://youtube.com/@selfhostedops)** diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..db6cafd --- /dev/null +++ b/compose.yml @@ -0,0 +1,165 @@ +# ~$ selfhostedops — The €15/Month Self-Hosted Company Stack +# Series A · Episode 1 — https://youtube.com/@selfhostedops +# +# One server. One file. Your whole company stack: +# caddy reverse proxy + automatic HTTPS +# forgejo git hosting + CI (also acts as SSO/OIDC provider) +# outline team wiki (logs in via Forgejo SSO) +# vaultwarden password manager (Bitwarden-compatible) +# uptime-kuma monitoring + alerts +# n8n automation hub +# wg-easy WireGuard VPN (optional — start with: --profile vpn) +# (full Netbird mesh gets its own episode) +# +# Quickstart: +# 1. cp .env.example .env && edit it (domain, email, secrets) +# 2. Point *.${DOMAIN} (wildcard A/AAAA record) at this server +# 3. docker compose up -d +# +# Every image below is multi-arch (amd64 + arm64) — runs on a €14 ARM box. + +name: selfhostedops-stack + +networks: + proxy: + internal: + +volumes: + caddy_data: + caddy_config: + forgejo_data: + outline_data: + outline_pg: + vaultwarden_data: + kuma_data: + n8n_data: + wg_data: + +services: + caddy: + image: caddy:2-alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" # HTTP/3 + environment: + DOMAIN: ${DOMAIN} + ACME_EMAIL: ${ACME_EMAIL} + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: [proxy] + + forgejo: + image: codeberg.org/forgejo/forgejo:11 + restart: unless-stopped + environment: + USER_UID: "1000" + USER_GID: "1000" + FORGEJO__server__DOMAIN: git.${DOMAIN} + FORGEJO__server__ROOT_URL: https://git.${DOMAIN}/ + FORGEJO__server__SSH_PORT: "2222" + FORGEJO__service__DISABLE_REGISTRATION: "true" + FORGEJO__openid__ENABLE_OPENID_SIGNIN: "false" + ports: + - "2222:22" # git over SSH + volumes: + - forgejo_data:/data + networks: [proxy] + + outline-postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: outline + POSTGRES_PASSWORD: ${OUTLINE_PG_PASSWORD} + POSTGRES_DB: outline + volumes: + - outline_pg:/var/lib/postgresql/data + networks: [internal] + + outline-redis: + image: redis:7-alpine + restart: unless-stopped + networks: [internal] + + outline: + image: outlinewiki/outline:latest + restart: unless-stopped + depends_on: [outline-postgres, outline-redis] + environment: + URL: https://wiki.${DOMAIN} + PORT: "3000" + SECRET_KEY: ${OUTLINE_SECRET_KEY} + UTILS_SECRET: ${OUTLINE_UTILS_SECRET} + DATABASE_URL: postgres://outline:${OUTLINE_PG_PASSWORD}@outline-postgres:5432/outline + PGSSLMODE: disable + REDIS_URL: redis://outline-redis:6379 + FILE_STORAGE: local + FILE_STORAGE_LOCAL_ROOT_DIR: /var/lib/outline/data + FORCE_HTTPS: "false" # TLS terminates at caddy + # --- SSO via your own Forgejo (set up the OAuth2 app first, see README) --- + OIDC_CLIENT_ID: ${OUTLINE_OIDC_CLIENT_ID} + OIDC_CLIENT_SECRET: ${OUTLINE_OIDC_CLIENT_SECRET} + OIDC_AUTH_URI: https://git.${DOMAIN}/login/oauth/authorize + OIDC_TOKEN_URI: https://git.${DOMAIN}/login/oauth/access_token + OIDC_USERINFO_URI: https://git.${DOMAIN}/login/oauth/userinfo + OIDC_LOGOUT_URI: https://git.${DOMAIN}/logout + OIDC_DISPLAY_NAME: Forgejo + OIDC_SCOPES: openid profile email + volumes: + - outline_data:/var/lib/outline/data + networks: [proxy, internal] + + vaultwarden: + image: vaultwarden/server:latest + restart: unless-stopped + environment: + DOMAIN: https://vault.${DOMAIN} + ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN} + SIGNUPS_ALLOWED: "${VAULTWARDEN_SIGNUPS_ALLOWED:-true}" # set to false after your team joined! + volumes: + - vaultwarden_data:/data + networks: [proxy] + + uptime-kuma: + image: louislam/uptime-kuma:1 + restart: unless-stopped + volumes: + - kuma_data:/app/data + networks: [proxy] + + n8n: + image: docker.n8n.io/n8nio/n8n:latest + restart: unless-stopped + environment: + N8N_HOST: n8n.${DOMAIN} + N8N_PORT: "5678" + N8N_PROTOCOL: https + WEBHOOK_URL: https://n8n.${DOMAIN}/ + GENERIC_TIMEZONE: ${TZ:-Europe/Berlin} + TZ: ${TZ:-Europe/Berlin} + volumes: + - n8n_data:/home/node/.n8n + networks: [proxy] + + # Optional starter VPN — enable with: docker compose --profile vpn up -d + # (The proper Netbird mesh deployment is Episode 5.) + wg-easy: + image: ghcr.io/wg-easy/wg-easy:14 + profiles: [vpn] + restart: unless-stopped + cap_add: [NET_ADMIN, SYS_MODULE] + sysctls: + net.ipv4.ip_forward: 1 + net.ipv4.conf.all.src_valid_mark: 1 + environment: + WG_HOST: vpn.${DOMAIN} + PASSWORD_HASH: ${WGEASY_PASSWORD_HASH} + ports: + - "51820:51820/udp" + volumes: + - wg_data:/etc/wireguard + networks: [proxy]