# ~$ 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]