selfhostedops-stack/compose.yml

251 lines
8.2 KiB
YAML

# ~$ 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
# ntfy push notifications for those alerts (episode 6)
# n8n automation hub
# wg-easy WireGuard VPN (optional — start with: --profile vpn)
# (the full NetBird mesh is in netbird/ — episode 5)
#
# 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:
driver_opts:
# Docker 28+ firewalls direct access to container IPs from other
# interfaces (raw PREROUTING drop per container). This lets the NetBird
# mesh interface reach the stack containers - see netbird/ (episode 5).
com.docker.network.bridge.trusted_host_interfaces: "wt0"
ci:
internal:
volumes:
caddy_data:
caddy_config:
forgejo_data:
outline_data:
outline_pg:
vaultwarden_data:
kuma_data:
ntfy_data:
ntfy_cache:
n8n_data:
wg_data:
docker_certs:
dind_data:
runner_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:16
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"
FORGEJO__actions__ENABLED: "true"
FORGEJO__repository__DEFAULT_REPO_UNITS: "repo.code,repo.issues,repo.pulls,repo.releases,repo.packages,repo.actions"
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!
# --- Single sign-on via your own Forgejo (episode 4) ---
# Create the OAuth2 app in Forgejo FIRST: Site Administration ->
# Applications -> OAuth2. Its redirect URI must be exactly:
# https://vault.${DOMAIN}/identity/connect/oidc-signin
# That path is not documented in the UI. Getting it wrong fails only
# AFTER the Forgejo login, which is why it costs people twenty minutes.
SSO_ENABLED: "true"
# Just the base URL. Vaultwarden reads Forgejo's discovery document
# (/.well-known/openid-configuration) from here on the first login.
SSO_AUTHORITY: https://git.${DOMAIN}
SSO_CLIENT_ID: ${VAULTWARDEN_SSO_CLIENT_ID}
SSO_CLIENT_SECRET: ${VAULTWARDEN_SSO_CLIENT_SECRET}
# Vaultwarden prepends "openid" itself, so listing it here again shows up
# duplicated on the Forgejo consent screen. Harmless, but untidy.
SSO_SCOPES: "profile email"
# Attach an SSO login to the existing account with the same mail address
# instead of creating a duplicate. Turn this on BEFORE people log in.
SSO_SIGNUPS_MATCH_EMAIL: "true"
volumes:
- vaultwarden_data:/data
networks: [proxy]
uptime-kuma:
image: louislam/uptime-kuma:1
restart: unless-stopped
volumes:
- kuma_data:/app/data
networks: [proxy]
# Push notifications for the monitoring alerts (episode 6).
# Uptime Kuma publishes to the topic "alerts", your phone subscribes to it.
ntfy:
image: binwiederhier/ntfy:v2.28.0
restart: unless-stopped
command: serve
environment:
NTFY_BASE_URL: https://ntfy.${DOMAIN}
NTFY_BEHIND_PROXY: "true"
NTFY_AUTH_DEFAULT_ACCESS: deny-all # nobody reads or writes without a user
NTFY_AUTH_FILE: /var/lib/ntfy/user.db
NTFY_CACHE_FILE: /var/cache/ntfy/cache.db
NTFY_ATTACHMENT_CACHE_DIR: /var/cache/ntfy/attachments
NTFY_ENABLE_LOGIN: "true"
volumes:
- ntfy_data:/var/lib/ntfy
- ntfy_cache:/var/cache/ntfy
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]
# --- CI (episode 2) -- start with: docker compose --profile ci up -d ---
docker-in-docker:
image: data.forgejo.org/oci/docker:dind
# The hostname MUST be "docker": the TLS certificates dind generates are
# only valid for that name. Rename it and you get a handshake error that
# reads like a network problem and is not one.
hostname: docker
profiles: [ci]
privileged: true
restart: unless-stopped
environment:
DOCKER_TLS_CERTDIR: /certs
volumes:
- docker_certs:/certs
- dind_data:/var/lib/docker
networks: [ci]
forgejo-runner:
image: data.forgejo.org/forgejo/runner:13.0.0
profiles: [ci]
restart: unless-stopped
depends_on: [docker-in-docker]
environment:
DOCKER_HOST: tcp://docker:2376
DOCKER_CERT_PATH: /certs/client
DOCKER_TLS_VERIFY: "1"
volumes:
- runner_data:/data
- docker_certs:/certs
- ./runner-config.yml:/data/config.yml:ro
networks: [ci, proxy]
command: forgejo-runner --config /data/config.yml daemon