Compare commits

..

7 commits

Author SHA1 Message Date
fef521909b feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access
Public repos are now browsable/clonable anonymously (HTTPS) and over SSH,
proxied through the Hetzner VPS. Unlike other public apps this is
deliberately not behind the oauth2-proxy/Zitadel gate, since Forgejo
already enforces its own per-repo visibility. Root redirects to the
.profile landing page. Documents the REQUIRE_SIGNIN_VIEW gotcha and the
Zitadel OIDC redirect_uri fix needed whenever FORGEJO_DOMAIN changes.
2026-07-19 18:34:13 +02:00
1c2f4a1c81 fix(forgejo): set SSH_DOMAIN so the UI's clone URL is actually usable
Forgejo's displayed SSH clone URL comes from SSH_DOMAIN, which defaults
to "localhost" (not DOMAIN) when unset — completely unusable, and
DOMAIN itself would've been wrong anyway since it resolves through
Caddy's wildcard CNAME, which SSH can't traverse. Point it at the
dedicated forgejo.home.brianpooe.com A record instead.
2026-07-19 15:17:25 +02:00
fc116653f7 docs(forgejo): document the forgejo.home.brianpooe.com DNS alias
Added a Technitium A record pointing at the Forgejo LXC directly
(SSH can't go through the Caddy wildcard CNAME), so git-over-SSH URLs
read ssh://git@forgejo.home.brianpooe.com/... instead of the raw IP.
All 5 hosts' remotes/SSH config updated to match.
2026-07-19 15:13:56 +02:00
9dfde294cb feat(forgejo): move git-over-SSH to standard port 22
Relocated LXC 114's own admin sshd to port 2200 (systemd ssh.socket
override) to free port 22 for Forgejo's git-SSH, dropping the ugly
:2222 from every clone URL. Updated the 5 hosts' remotes/SSH config
and the corresponding UniFi rules to match.
2026-07-19 15:09:55 +02:00
0ff2df7033 docs(network): add CADDY/IoT -> Forgejo git-over-SSH rules
Switched all internal hosts from HTTPS+token to SSH for git access to
Forgejo, using a shared deploy key. SSH bypasses Caddy (proxying raw
TCP isn't possible), so raspberrypi and Home Assistant each needed a
direct TRUSTED-zone rule to 10.0.10.18:2222.
2026-07-19 15:00:08 +02:00
a7de8cec3a docs(network): document IoT->Caddy path for HA's Forgejo git access
Home Assistant now pulls homelab-blueprint from Forgejo through Caddy,
which needed both a UniFi zone rule (IoT -> CADDY) and a ufw allow on
the Caddy host itself for the IoT subnet — two independent enforcement
layers, both previously missing/misconfigured.
2026-07-19 14:36:00 +02:00
73ff919b9f feat(forgejo): remove gitea-mirror, all repos now first-class
GitHub mirroring is no longer needed now that Forgejo is the primary
remote. Drops the gitea-mirror service, its volume, env vars, Caddy
site, and firewall rule; docs updated to match.
2026-07-19 13:21:05 +02:00
8 changed files with 215 additions and 164 deletions

View file

@ -32,7 +32,7 @@ help:
@echo " deploy-paperless - Deploy Paperless-ngx" @echo " deploy-paperless - Deploy Paperless-ngx"
@echo " deploy-vaultwarden - Deploy Vaultwarden" @echo " deploy-vaultwarden - Deploy Vaultwarden"
@echo " deploy-koffan - Deploy Koffan (grocery list)" @echo " deploy-koffan - Deploy Koffan (grocery list)"
@echo " deploy-forgejo - Deploy Forgejo (self-hosted Git + GitHub mirroring)" @echo " deploy-forgejo - Deploy Forgejo (self-hosted Git)"
@echo "" @echo ""
@echo "Utility Targets:" @echo "Utility Targets:"
@echo " status - Show status of all deployed stacks" @echo " status - Show status of all deployed stacks"

View file

@ -28,7 +28,7 @@ Each stack guide in `docker-compose-files/<stack>/README.md` explains appdata pl
| [Beszel Hub](docker-compose-files/beszel-hub/README.md) | Beszel monitoring dashboard | | [Beszel Hub](docker-compose-files/beszel-hub/README.md) | Beszel monitoring dashboard |
| [Beszel Socket Proxy](docker-compose-files/socket-proxy/README.md) | Restricted Docker API access for Beszel | | [Beszel Socket Proxy](docker-compose-files/socket-proxy/README.md) | Restricted Docker API access for Beszel |
| [Caddy](docker-compose-files/caddy/README.md) | Reverse proxy with Cloudflare DNS-01 | | [Caddy](docker-compose-files/caddy/README.md) | Reverse proxy with Cloudflare DNS-01 |
| [Forgejo](docker-compose-files/forgejo/README.md) | Self-hosted Git service with automatic GitHub mirroring | | [Forgejo](docker-compose-files/forgejo/README.md) | Self-hosted Git service |
| [Gramps Web](docker-compose-files/gramps-web/README.md) | Genealogy web application | | [Gramps Web](docker-compose-files/gramps-web/README.md) | Genealogy web application |
| [Home Assistant](docker-compose-files/homeassistant/README.md) | Home Assistant, Mosquitto, and Zigbee2MQTT | | [Home Assistant](docker-compose-files/homeassistant/README.md) | Home Assistant, Mosquitto, and Zigbee2MQTT |
| [Immich](docker-compose-files/immich/README.md) | Photo and video management with GPU acceleration | | [Immich](docker-compose-files/immich/README.md) | Photo and video management with GPU acceleration |

View file

@ -173,18 +173,20 @@ koffan.{{CADDY_BASE_DOMAIN:-home.example.com}} {
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080 reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080
} }
# Forgejo (LXC 114, .18). Git-over-SSH (port 2222) is not proxyable through # Forgejo (LXC 114, .18) -- publicly exposed at git.brianpooe.com (proxied via
# Caddy — that port is exposed directly on the LXC for SSH clone/push. # the VPS over NetBird for outside visitors) as well as reachable directly on
git.{{CADDY_BASE_DOMAIN:-home.example.com}} { # the LAN via split-horizon DNS. One canonical hostname everywhere, so this
# is NOT under CADDY_BASE_DOMAIN like other internal-only apps.
# Git-over-SSH is not proxyable through Caddy -- forgejo.brianpooe.com
# resolves straight to the LXC (10.0.10.18) instead, bypassing Caddy entirely.
# Root redirects to the .profile landing page instead of the repo explore
# list -- see the Forgejo README's "Public Exposure" section.
git.brianpooe.com {
import common import common
redir / /brianpooe 302
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000 reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000
} }
git-mirror.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:4321
}
# Hermes Agent web dashboard (LXC 113, not docker — systemd service) # Hermes Agent web dashboard (LXC 113, not docker — systemd service)
hermes.{{CADDY_BASE_DOMAIN:-home.example.com}} { hermes.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common import common

View file

@ -1,6 +1,10 @@
# Forgejo # Forgejo
FORGEJO_VERSION=10 FORGEJO_VERSION=10
FORGEJO_HTTP_PORT=3000 FORGEJO_HTTP_PORT=3000
# Defaults to 2222 because the LXC's own system sshd already owns port 22.
# To use the standard port 22 for git-over-SSH instead, first move the LXC's
# own admin sshd off 22 (e.g. via a systemd ssh.socket override setting
# ListenStream to another port), then set FORGEJO_SSH_PORT=22 here.
FORGEJO_SSH_PORT=2222 FORGEJO_SSH_PORT=2222
FORGEJO_BIND_IP=0.0.0.0 FORGEJO_BIND_IP=0.0.0.0
FORGEJO_MEM_LIMIT=1G FORGEJO_MEM_LIMIT=1G
@ -9,6 +13,18 @@ FORGEJO_MEM_LIMIT=1G
FORGEJO_ROOT_URL=https://git.home.example.com FORGEJO_ROOT_URL=https://git.home.example.com
FORGEJO_DOMAIN=git.home.example.com FORGEJO_DOMAIN=git.home.example.com
# Hostname shown in the web UI's SSH clone URL. Must resolve directly to
# this LXC (not through Caddy — SSH can't be proxied), so it needs its own
# DNS A record rather than reusing FORGEJO_DOMAIN's wildcard CNAME to Caddy.
#
# If this instance is publicly exposed, FORGEJO_DOMAIN/FORGEJO_SSH_DOMAIN
# should be the same public-facing names used everywhere (LAN and internet),
# resolved via split-horizon DNS: an internal DNS server answers them
# directly for LAN clients (its own zone/record, not the wildcard CNAME
# above), public DNS points them at your public edge for everyone else. See
# the Forgejo README's "Public Exposure" section for the full pattern.
FORGEJO_SSH_DOMAIN=forgejo.home.example.com
# Generate with: openssl rand -base64 32 # Generate with: openssl rand -base64 32
FORGEJO_DB_PASSWORD=<random-secret> FORGEJO_DB_PASSWORD=<random-secret>
@ -19,35 +35,13 @@ FORGEJO_DATA_ROOT=/mnt/unas/forgejo_data
# false = open self-signup; leave true and create accounts with # false = open self-signup; leave true and create accounts with
# `docker exec forgejo forgejo admin user create ...` instead. # `docker exec forgejo forgejo admin user create ...` instead.
FORGEJO_DISABLE_REGISTRATION=true FORGEJO_DISABLE_REGISTRATION=true
# If this instance is ever exposed publicly with the intent of letting
# outsiders browse public repos anonymously (GitHub-style), this MUST stay
# false. true forces every page -- including /explore and any .profile
# landing page -- through /user/login for anonymous visitors, regardless of
# FORGEJO_LANDING_PAGE. It silently defeats public repo browsing.
FORGEJO_REQUIRE_SIGNIN_VIEW=false FORGEJO_REQUIRE_SIGNIN_VIEW=false
# login = anonymous visitors land on the sign-in page (LAN-only/private use).
# --- gitea-mirror: auto-syncs GitHub repos into Forgejo --- # explore = anonymous visitors land on the public repo listing, like GitHub's
GITEA_MIRROR_VERSION=latest # logged-out homepage. Use explore if this instance is publicly exposed.
GITEA_MIRROR_PORT=4321 FORGEJO_LANDING_PAGE=login
GITEA_MIRROR_BIND_IP=0.0.0.0
GITEA_MIRROR_MEM_LIMIT=256m
# Full URL where gitea-mirror's own UI is reachable
GITEA_MIRROR_PUBLIC_URL=https://git-mirror.home.example.com
# Generate with: openssl rand -base64 32
GITEA_MIRROR_AUTH_SECRET=<random-secret>
# GitHub source — a classic PAT with repo (+ read:org if mirroring orgs) scope
GITHUB_USERNAME=<your-github-username>
GITHUB_TOKEN=<github-personal-access-token>
GITEA_MIRROR_SKIP_FORKS=false
GITEA_MIRROR_PRIVATE_REPOS=true
GITEA_MIRROR_STARRED=false
GITEA_MIRROR_ORGANIZATIONS=false
GITEA_MIRROR_ISSUES=false
GITEA_MIRROR_WIKI=false
# Forgejo destination — GITEA_MIRROR_FORGEJO_TOKEN can only be created after
# Forgejo is up and you've logged in once (Settings -> Applications ->
# Generate New Token, scopes: repo, admin:org). See README "GitHub mirroring".
GITEA_MIRROR_FORGEJO_USERNAME=<your-forgejo-username>
GITEA_MIRROR_FORGEJO_TOKEN=<forgejo-api-token>
GITEA_MIRROR_ORG=github-mirrors
GITEA_MIRROR_ORG_VISIBILITY=private
GITEA_MIRROR_INTERVAL=8h

View file

@ -1,8 +1,6 @@
# Forgejo # Forgejo
Self-hosted Git service (soft fork of Gitea) with Postgres, plus Self-hosted Git service (soft fork of Gitea) with Postgres.
[gitea-mirror](https://github.com/RayLabsHQ/gitea-mirror) to automatically
pull all your GitHub repositories in as mirrors.
## Persistent Data ## Persistent Data
@ -29,9 +27,6 @@ The share binds use `create_host_path: false`, so the stack refuses to start
if the share is not mounted, rather than silently writing into an empty if the share is not mounted, rather than silently writing into an empty
directory that later needs merging back in. directory that later needs merging back in.
`gitea_mirror_data` (local named volume) holds gitea-mirror's own small
SQLite state DB — negligible size, not worth splitting out.
## Installation ## Installation
1. Create `.env` if it does not exist: 1. Create `.env` if it does not exist:
@ -42,8 +37,7 @@ SQLite state DB — negligible size, not worth splitting out.
2. Set the required values in `docker-compose-files/forgejo/.env` — at 2. Set the required values in `docker-compose-files/forgejo/.env` — at
minimum `FORGEJO_ROOT_URL`, `FORGEJO_DOMAIN`, `FORGEJO_DB_PASSWORD`, and minimum `FORGEJO_ROOT_URL`, `FORGEJO_DOMAIN`, `FORGEJO_DB_PASSWORD`, and
`FORGEJO_DATA_ROOT`. Leave the `GITEA_MIRROR_*` GitHub/Forgejo-token vars `FORGEJO_DATA_ROOT`.
for step 6 — Forgejo needs to exist before those tokens can be generated.
3. Add the UNAS share and bind-mount it into the LXC, following 3. Add the UNAS share and bind-mount it into the LXC, following
[nfs-mounts.md](../../docs/unas/nfs-mounts.md) (add `forgejo_data` to the [nfs-mounts.md](../../docs/unas/nfs-mounts.md) (add `forgejo_data` to the
@ -67,53 +61,93 @@ SQLite state DB — negligible size, not worth splitting out.
```bash ```bash
./substitute_env.sh docker-compose-files/forgejo/template.yaml docker-compose.forgejo.yml .env docker-compose-files/forgejo/.env ./substitute_env.sh docker-compose-files/forgejo/template.yaml docker-compose.forgejo.yml .env docker-compose-files/forgejo/.env
docker compose -f docker-compose.forgejo.yml config --quiet docker compose -f docker-compose.forgejo.yml config --quiet
docker compose -f docker-compose.forgejo.yml up -d forgejo-db forgejo docker compose -f docker-compose.forgejo.yml up -d
``` ```
`gitea-mirror` is intentionally left out of the first `up` — see step 6.
5. Open `http://<host-ip>:3000`, create the first (admin) account, and 5. Open `http://<host-ip>:3000`, create the first (admin) account, and
confirm repo creation/push/clone works before wiring up mirroring. confirm repo creation/push/clone works.
## GitHub Mirroring
`gitea-mirror` is a separate small app, not a Forgejo built-in — Forgejo
itself has no config option to bulk-pull an entire GitHub account. Once you
have a Forgejo login:
1. In Forgejo: **Settings → Applications → Generate New Token**, scopes
`repo` + `admin:org` (needed to auto-create the `github-mirrors`
destination org). Put the token in `GITEA_MIRROR_FORGEJO_TOKEN`.
2. Create a GitHub classic PAT with `repo` scope (add `read:org` if you set
`GITEA_MIRROR_ORGANIZATIONS=true`). Put it in `GITHUB_TOKEN`.
3. Start the mirror service:
```bash
docker compose -f docker-compose.forgejo.yml up -d gitea-mirror
```
With `AUTO_IMPORT_REPOS=true`, `AUTO_MIRROR_REPOS=true`, and
`SCHEDULE_ENABLED=true` (all defaulted on in the template), it imports every
repo from `GITHUB_USERNAME` on first startup and re-syncs every
`GITEA_MIRROR_INTERVAL` (default `8h`) after that — no manual per-repo setup.
Fine-tune what gets pulled via `GITEA_MIRROR_PRIVATE_REPOS`,
`GITEA_MIRROR_STARRED`, `GITEA_MIRROR_ORGANIZATIONS`, `GITEA_MIRROR_SKIP_FORKS`.
`CLEANUP_DELETE_IF_NOT_IN_GITHUB` stays `false` by default — a repo you
delete on GitHub won't get deleted in Forgejo automatically. Flip that on
deliberately if you want mirrors pruned to match GitHub exactly.
Its own UI is at `http://<host-ip>:4321` for monitoring sync status and
tweaking mirror settings beyond what's templated here.
## Reverse Proxy ## Reverse Proxy
Add blocks for `git.<domain>` (port `FORGEJO_HTTP_PORT`) and optionally Add a block for `git.<domain>` (port `FORGEJO_HTTP_PORT`) to
`git-mirror.<domain>` (port `GITEA_MIRROR_PORT`) to
`docker-compose-files/caddy/Caddyfile_template`, then `make reload-caddy`. `docker-compose-files/caddy/Caddyfile_template`, then `make reload-caddy`.
Git-over-SSH (`FORGEJO_SSH_PORT`) is not proxyable through Caddy — expose that Git-over-SSH (`FORGEJO_SSH_PORT`) is not proxyable through Caddy — expose that
port directly if you want SSH clone/push from outside the LAN. port directly if you want SSH clone/push from outside the LAN.
`FORGEJO_SSH_PORT` defaults to `2222` because the LXC's own admin sshd
already owns port 22. This instance was later moved to the standard port —
the LXC's admin sshd got relocated to 2200 first (systemd `ssh.socket`
override), freeing 22 for Forgejo's git-SSH. See `.env.sample` for the
one-time steps if you want to do the same elsewhere.
Since SSH bypasses Caddy, this instance instead gets a plain Technitium `A`
record — `forgejo.brianpooe.com``10.0.10.18` — so clone URLs read
`ssh://git@forgejo.brianpooe.com/<owner>/<repo>.git` instead of the raw
LXC IP. Add this in the Technitium admin UI (or via its API) as a normal
zone record; it isn't behind the `*.home.brianpooe.com` → Caddy wildcard.
## Public Exposure
This instance is publicly reachable at `git.brianpooe.com` (HTTPS) and
`ssh://git@forgejo.brianpooe.com` (SSH), proxied through the Hetzner VPS —
see `~/netbird-vps/docs/netbird-acl-playbook.md` and `public-app-sso.md` on
the VPS for the edge side. Unlike other public apps (koffan, IT-Tools), this
one is deliberately **not** behind the oauth2-proxy/Zitadel SSO gate: public
repos need to be browsable/clonable with no login, like GitHub. Forgejo
enforces its own per-repo visibility regardless of network path, so private
repos stay private without an extra gate.
One canonical hostname everywhere — `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` are
`git.brianpooe.com`, not a `.home.` subdomain — resolved via split-horizon
DNS: Technitium answers it directly on the LAN (own zone, `A` record ->
`10.0.15.5`), public DNS (`*.brianpooe.com` wildcard) sends it to the VPS.
Same pattern for `forgejo.brianpooe.com` (`A` record -> `10.0.10.18`
directly, bypassing Caddy, since SSH can't be proxied).
Two settings matter for a public instance and are easy to get wrong:
- `FORGEJO_REQUIRE_SIGNIN_VIEW` **must be `false`**. When `true` it forces
every page (including `/explore` and the `.profile` landing page) through
`/user/login` for anonymous visitors, regardless of `LANDING_PAGE` — it
silently defeats "outsiders can browse public repos."
- `FORGEJO_LANDING_PAGE=explore` so anonymous visitors land on the public
repo listing instead of a login page. On top of that, the Caddy vhost for
`git.brianpooe.com` (`docker-compose-files/caddy/Caddyfile_template`) has
`redir / /brianpooe 302`, so the *root* domain goes straight to the
`.profile` landing page instead of `/explore``/explore` is still one
click away via the nav sidebar. Update/remove that redirect if the
account name or desired landing target ever changes.
**Changing `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` also requires updating the
Zitadel OIDC app's redirect URI**, or sign-in breaks with
`invalid_request: redirect_uri is missing in the client configuration`.
Forgejo's OIDC callback is always `<ROOT_URL>/user/oauth2/zitadel/callback`
— this is Forgejo's own direct Zitadel app (project **Homelab**, app
**Forgejo**), separate from the shared oauth2-proxy app used by
koffan/it-tools. Update it via the Zitadel console or, from the VPS:
```bash
PAT=$(cat ~/netbird-vps/zitadel/zitadel-data/admin.pat)
curl -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-X PUT "https://zitadel.brianpooe.com/management/v1/projects/345934587441971203/apps/382370882418376707/oidc_config" -d '{
"redirectUris": ["https://git.brianpooe.com/user/oauth2/zitadel/callback"],
"postLogoutRedirectUris": ["https://git.brianpooe.com"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER"
}'
```
Public git-over-SSH reaches the LXC via a raw TCP forward on the VPS
(`~/netbird-vps/ssh-forward/`, a `socat` container in `network_mode: host`
listening on the VPS's now-freed port 22) over a NetBird route added
specifically for this (`10.0.10.18/32` on the `homelab-lan` network, routed
through the `pi-caddy` peer — see the VPS's `netbird-acl-playbook.md`
"Model 2" pattern). The VPS's own admin sshd was relocated to port 2200 to
free port 22 for this forward.
## Notes ## Notes
- Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than - Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than
@ -128,7 +162,6 @@ port directly if you want SSH clone/push from outside the LAN.
```bash ```bash
# Tail logs # Tail logs
docker compose -f docker-compose.forgejo.yml logs -f forgejo docker compose -f docker-compose.forgejo.yml logs -f forgejo
docker compose -f docker-compose.forgejo.yml logs -f gitea-mirror
# Create an additional user without open registration # Create an additional user without open registration
docker exec -it forgejo forgejo admin user create --username <name> --email <email> --random-password docker exec -it forgejo forgejo admin user create --username <name> --email <email> --random-password

View file

@ -1,5 +1,4 @@
# Forgejo self-hosted Git service, backed by Postgres, with automatic # Forgejo self-hosted Git service, backed by Postgres.
# mirroring of GitHub repositories via gitea-mirror.
# #
# Storage split (see docs/unas/nfs-mounts.md): # Storage split (see docs/unas/nfs-mounts.md):
# - forgejo_data (local named volume): app config, SSH host keys, session # - forgejo_data (local named volume): app config, SSH host keys, session
@ -68,6 +67,12 @@ services:
FORGEJO__database__PASSWD: "{{FORGEJO_DB_PASSWORD}}" FORGEJO__database__PASSWD: "{{FORGEJO_DB_PASSWORD}}"
FORGEJO__server__ROOT_URL: "{{FORGEJO_ROOT_URL}}" FORGEJO__server__ROOT_URL: "{{FORGEJO_ROOT_URL}}"
FORGEJO__server__DOMAIN: "{{FORGEJO_DOMAIN}}" FORGEJO__server__DOMAIN: "{{FORGEJO_DOMAIN}}"
# SSH_DOMAIN drives the SSH clone URL shown in the web UI. It defaults
# to "localhost" (not DOMAIN), which is unusable here regardless: SSH
# can't be proxied through Caddy, so FORGEJO_DOMAIN's wildcard CNAME
# to the Caddy host would be wrong too. Point it at a dedicated DNS
# record resolving directly to this LXC instead (see README).
FORGEJO__server__SSH_DOMAIN: "{{FORGEJO_SSH_DOMAIN}}"
FORGEJO__server__SSH_PORT: "{{FORGEJO_SSH_PORT:-2222}}" FORGEJO__server__SSH_PORT: "{{FORGEJO_SSH_PORT:-2222}}"
# LANDING_PAGE controls what anonymous visitors see at "/" — set to # LANDING_PAGE controls what anonymous visitors see at "/" — set to
# login so they land on sign-in instead of the marketing homepage. # login so they land on sign-in instead of the marketing homepage.
@ -150,79 +155,6 @@ services:
max-file: "{{DOCKERLOGGING_MAXFILE}}" max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}" max-size: "{{DOCKERLOGGING_MAXSIZE}}"
# Auto-syncs GitHub repos into Forgejo as mirrors. Forgejo itself has no
# "pull all my repos" config option — this is a separate open-source tool
# (https://github.com/RayLabsHQ/gitea-mirror) that provides exactly that,
# driven entirely by env vars below plus a one-time Forgejo API token (see
# README "GitHub mirroring" section — the token can't exist until Forgejo
# has an admin account, so it can't be templated here).
gitea-mirror:
image: ghcr.io/raylabshq/gitea-mirror:{{GITEA_MIRROR_VERSION:-latest}}
container_name: gitea-mirror
restart: unless-stopped
depends_on:
forgejo:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: file:data/gitea-mirror.db
HOST: 0.0.0.0
PORT: "4321"
BASE_URL: "/"
BETTER_AUTH_SECRET: "{{GITEA_MIRROR_AUTH_SECRET}}"
BETTER_AUTH_URL: "{{GITEA_MIRROR_PUBLIC_URL}}"
PUBLIC_BETTER_AUTH_URL: "{{GITEA_MIRROR_PUBLIC_URL}}"
# GitHub source
GITHUB_USERNAME: "{{GITHUB_USERNAME}}"
GITHUB_TOKEN: "{{GITHUB_TOKEN}}"
SKIP_FORKS: "{{GITEA_MIRROR_SKIP_FORKS:-false}}"
PRIVATE_REPOSITORIES: "{{GITEA_MIRROR_PRIVATE_REPOS:-true}}"
MIRROR_STARRED: "{{GITEA_MIRROR_STARRED:-false}}"
MIRROR_ORGANIZATIONS: "{{GITEA_MIRROR_ORGANIZATIONS:-false}}"
MIRROR_ISSUES: "{{GITEA_MIRROR_ISSUES:-false}}"
MIRROR_WIKI: "{{GITEA_MIRROR_WIKI:-false}}"
# Forgejo destination — internal service name, no need to hairpin
# through the reverse proxy.
GITEA_URL: "http://forgejo:3000"
GITEA_TOKEN: "{{GITEA_MIRROR_FORGEJO_TOKEN}}"
GITEA_USERNAME: "{{GITEA_MIRROR_FORGEJO_USERNAME}}"
GITEA_ORGANIZATION: "{{GITEA_MIRROR_ORG:-github-mirrors}}"
GITEA_ORG_VISIBILITY: "{{GITEA_MIRROR_ORG_VISIBILITY:-private}}"
# Auto-import every repo on startup and keep mirroring on a schedule —
# this is the "pull all my GitHub repos via config" behavior.
AUTO_IMPORT_REPOS: "true"
AUTO_MIRROR_REPOS: "true"
SCHEDULE_ENABLED: "true"
GITEA_MIRROR_INTERVAL: "{{GITEA_MIRROR_INTERVAL:-8h}}"
CLEANUP_DELETE_IF_NOT_IN_GITHUB: "false"
CLEANUP_DRY_RUN: "true"
networks:
- forgejo_network
ports:
- "{{GITEA_MIRROR_BIND_IP:-0.0.0.0}}:{{GITEA_MIRROR_PORT:-4321}}:4321"
volumes:
- /etc/localtime:/etc/localtime:ro
- gitea_mirror_data:/app/data
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: "{{GITEA_MIRROR_MEM_LIMIT:-256m}}"
reservations:
memory: 64M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks: networks:
forgejo_network: forgejo_network:
driver: bridge driver: bridge
@ -231,4 +163,3 @@ networks:
volumes: volumes:
forgejo_db_data: forgejo_db_data:
forgejo_data: forgejo_data:
gitea_mirror_data:

View file

@ -99,7 +99,7 @@ Every policy in this table uses:
| `ALLOW CADDY to Bento PDF` | `10.0.10.6` | `8080` | Bento PDF | | `ALLOW CADDY to Bento PDF` | `10.0.10.6` | `8080` | Bento PDF |
| `ALLOW CADDY to Koffan` | `10.0.10.12` | `8080` | Koffan (LXC 111) | | `ALLOW CADDY to Koffan` | `10.0.10.12` | `8080` | Koffan (LXC 111) |
| `ALLOW CADDY to Hermes` | `10.0.10.19` | `9119` | Hermes Agent dashboard (LXC 113) | | `ALLOW CADDY to Hermes` | `10.0.10.19` | `9119` | Hermes Agent dashboard (LXC 113) |
| `ALLOW CADDY to Forgejo` | `10.0.10.18` | `3000,4321` | Forgejo (LXC 114) and gitea-mirror | | `ALLOW CADDY to Forgejo` | `10.0.10.18` | `3000,22` | Forgejo web UI (LXC 114) and git-over-SSH for hosts pulling from raspberrypi's zone |
--- ---
@ -122,6 +122,87 @@ Every policy in this table uses:
--- ---
## 3a. IoT → CADDY Policies (reverse direction)
The Home Assistant host itself also needs to reach Caddy directly — it
pulls `homelab-blueprint` from Forgejo (`git.brianpooe.com`, proxied
through Caddy) to deploy its own docker-compose stack.
**Two independent layers enforce traffic into the Caddy host — both must
allow it, and both were found misconfigured while wiring this up:**
1. **UniFi zone-based firewall** (router-level, VLAN → VLAN):
| Policy name | Source zone | Source IP | Src. Port | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|---|
| `Allow HA to Caddy` | `IoT` | `10.0.30.6` (Home Assistant IP) | `Any` | `CADDY` | `10.0.15.5` (Caddy IP) | `443` | git fetch/pull of homelab-blueprint from Forgejo |
Src. Port must be `Any`, not restricted to HA's own listening ports
(8080/8123) — this is outbound traffic *from* HA, so the source port is
a random ephemeral port, not the port HA listens on. Setting Src. Port
to a "Home Assistant" port object here was the original bug: the rule
never matched, so traffic fell through to `Block All Traffic` below it.
2. **`ufw` on the Caddy host itself** (10.0.15.5) — a second, separate
enforcement point the UniFi rule doesn't touch. It allow-lists inbound
80/443 per source subnet and did not include the IoT VLAN
(`10.0.30.0/24`):
```bash
sudo ufw allow from 10.0.30.0/24 to any port 80,443 proto tcp
```
## 3b. CADDY / IoT → Forgejo git-over-SSH (port 22)
Both raspberrypi (CADDY zone) and Home Assistant (IoT zone) clone
`homelab-blueprint` over SSH directly against the Forgejo LXC
(`10.0.10.18:22`), bypassing Caddy entirely — SSH can't be proxied
through Caddy (HTTP/S only), so this traffic goes straight to the TRUSTED
zone. `10.0.10.18` has no host-level firewall (`ufw` not installed,
`iptables INPUT` policy `ACCEPT`), so only the UniFi zone rule matters
here — no second enforcement layer to fight, unlike the Caddy host above.
Forgejo's git-SSH runs on the standard port 22 on this LXC, not the usual
`2222` (see `docker-compose-files/forgejo/README.md`) — the LXC's own
admin sshd was relocated to port **2200** (systemd `ssh.socket` override)
to free 22 up. Direct admin SSH to this LXC now needs `-p 2200`; `pct exec
114` from the Proxmox host is unaffected and still works normally.
| Policy name | Source zone | Source IP | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|
| `ALLOW CADDY to Forgejo` (existing rule, port added) | `CADDY` | `10.0.15.5` | `TRUSTED` | `10.0.10.18` | `3000,22` | Forgejo web UI + git-over-SSH from raspberrypi |
| `Allow HA to Forgejo SSH` | `IoT` | `10.0.30.6` | `TRUSTED` | `10.0.10.18` | `22` | git-over-SSH from homeassistant |
`ufw status` on that host is the fastest way to check which subnets can
already reach it before assuming a UniFi rule is the problem.
## 3c. Public Exposure of Forgejo (Hetzner VPS)
Forgejo is also reachable from the public internet, at `git.brianpooe.com`
(HTTPS, public repos only — anonymous browsing/cloning, like GitHub) and
`ssh://git@forgejo.brianpooe.com` (SSH, requires a registered account key —
self-registration is disabled, so this is effectively you-only). Full detail
lives in `docker-compose-files/forgejo/README.md` ("Public Exposure") and,
on the VPS itself, `~/netbird-vps/docs/netbird-acl-playbook.md` and
`public-app-sso.md`. Summary of what changed on the LAN side of that path:
- **DNS is split-horizon**, not a new firewall rule: `git.brianpooe.com` and
`forgejo.brianpooe.com` are each their own single-name Technitium zone
(`A` record direct to `10.0.15.5` and `10.0.10.18` respectively) so LAN
clients resolve them locally while the public `*.brianpooe.com` wildcard
sends everyone else to the VPS. No UniFi policy changes were needed for
the HTTPS path — it already flows through the existing `ALLOW CADDY to
Forgejo` rule above.
- **The public SSH path is a new NetBird route**, not a UniFi rule: the VPS
NetBird peer previously only routed to the Pi Caddy (`10.0.15.5/32`).
A second `10.0.10.18/32` resource (routing peer `pi-caddy`, "Model 2" in
the playbook) was added so the VPS can reach Forgejo's git-SSH directly.
The VPS forwards its own (now-freed) public port 22 to `10.0.10.18:22`
via a `socat` container (`~/netbird-vps/ssh-forward/`) — the VPS's own
admin sshd was relocated to port 2200 to make room.
---
## 4. Public DNS Block Policies (per zone) ## 4. Public DNS Block Policies (per zone)
Without these, devices can bypass Technitium by querying public resolvers Without these, devices can bypass Technitium by querying public resolvers

View file

@ -5,6 +5,16 @@ Hetzner VPS as the edge. Currently on this path: Koffan
(`koffan.brianpooe.com`) and IT-Tools (`it-tools.brianpooe.com`); the setup is (`koffan.brianpooe.com`) and IT-Tools (`it-tools.brianpooe.com`); the setup is
built to be reused for any future app. built to be reused for any future app.
> [!NOTE]
> Forgejo (`git.brianpooe.com`) also runs through this VPS edge but is
> deliberately **not** on this SSO path — public repos need to be
> browsable/clonable with no login, like GitHub. It gets its own plain
> `reverse_proxy` vhost (`crowdsec` + `rate_limit`, no `forward_auth`) in
> `caddy/Caddyfile.template`. Forgejo enforces its own per-repo visibility
> regardless of network path, so private repos stay private without an SSO
> gate. Its public git-over-SSH path is separate again — a raw TCP forward,
> not Caddy at all — see the VPS's `docs/netbird-acl-playbook.md`.
> [!NOTE] > [!NOTE]
> This intentionally does **not** use NetBird's built-in reverse-proxy > This intentionally does **not** use NetBird's built-in reverse-proxy
> ("Services") feature. Self-hosted NetBird requires running your own proxy > ("Services") feature. Self-hosted NetBird requires running your own proxy