homelab-blueprint/docker-compose-files/forgejo/template.yaml
Brian Pooe 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

165 lines
6.7 KiB
YAML

# Forgejo self-hosted Git service, backed by Postgres.
#
# Storage split (see docs/unas/nfs-mounts.md):
# - forgejo_data (local named volume): app config, SSH host keys, session
# store, and the Bleve search index. Small and latency-sensitive — same
# reasoning as Paperless's SQLite volume, even though Forgejo itself uses
# Postgres here.
# - forgejo_db_data (local named volume): Postgres data directory. Postgres
# tolerates NFS worse than plain files (locking/fsync semantics), so it
# stays local regardless of what the app-level data is doing.
# - FORGEJO_DATA_ROOT (UNAS bind mount): git repository objects, LFS objects,
# avatars, and attachments — the bulk storage that actually grows over
# time. Git's protocol tolerates NFS latency far better than a database
# does, so this is a safe NFS candidate.
services:
forgejo-db:
image: postgres:17-alpine
container_name: forgejo-db
restart: unless-stopped
environment:
TZ: "{{TZ}}"
POSTGRES_USER: forgejo
POSTGRES_PASSWORD: "{{FORGEJO_DB_PASSWORD}}"
POSTGRES_DB: forgejo
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- /etc/localtime:/etc/localtime:ro
- forgejo_db_data:/var/lib/postgresql/data
networks:
- forgejo_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forgejo -d forgejo"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
forgejo:
image: codeberg.org/forgejo/forgejo:{{FORGEJO_VERSION:-10}}
container_name: forgejo
restart: unless-stopped
environment:
USER_UID: "{{PUID}}"
USER_GID: "{{PGID}}"
TZ: "{{TZ}}"
# Full DB config supplied via env — INSTALL_LOCK skips the web
# installer so a stray visitor can't reconfigure the instance.
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: forgejo-db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: "{{FORGEJO_DB_PASSWORD}}"
FORGEJO__server__ROOT_URL: "{{FORGEJO_ROOT_URL}}"
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}}"
# LANDING_PAGE controls what anonymous visitors see at "/" — set to
# login so they land on sign-in instead of the marketing homepage.
# REQUIRE_SIGNIN_VIEW is a separate, stricter setting (blocks anonymous
# access to literally everything including the API); LANDING_PAGE
# alone is enough for "just show login at the root".
FORGEJO__server__LANDING_PAGE: "{{FORGEJO_LANDING_PAGE:-login}}"
# Self-hosted personal instance — no open signup by default. Create
# additional accounts with `forgejo admin user create` instead.
FORGEJO__service__DISABLE_REGISTRATION: "{{FORGEJO_DISABLE_REGISTRATION:-true}}"
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "{{FORGEJO_REQUIRE_SIGNIN_VIEW:-false}}"
# OIDC login (e.g. Zitadel) still works with registration disabled above —
# these two are independent. ACCOUNT_LINKING=auto links an OIDC login to
# an existing local account by matching email instead of erroring or
# creating a duplicate.
FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "{{FORGEJO_OAUTH2_AUTO_REGISTRATION:-true}}"
FORGEJO__oauth2_client__ACCOUNT_LINKING: "{{FORGEJO_OAUTH2_ACCOUNT_LINKING:-auto}}"
# Some providers (e.g. Zitadel when no separate nickname is set) send
# preferred_username == the full email address, which fails Forgejo's
# username validation (no '@' allowed) before account-linking ever gets
# a chance to run. USERNAME=email derives a valid username from the
# local-part instead, which also lets the email match against an
# existing local account succeed via ACCOUNT_LINKING=auto above.
FORGEJO__oauth2_client__USERNAME: "{{FORGEJO_OAUTH2_USERNAME_SOURCE:-email}}"
# Password login fully disabled — Zitadel OIDC is the only way in via
# the web UI. PATs and SSH keys still work for git/API (unaffected by
# ENABLE_BASIC_AUTHENTICATION, which only blocks raw-password basic
# auth). If Zitadel ever breaks, flip both back to true here and
# redeploy to restore the password login form as a fallback.
FORGEJO__service__ENABLE_INTERNAL_SIGNIN: "{{FORGEJO_ENABLE_INTERNAL_SIGNIN:-false}}"
FORGEJO__service__ENABLE_BASIC_AUTHENTICATION: "{{FORGEJO_ENABLE_BASIC_AUTHENTICATION:-false}}"
depends_on:
forgejo-db:
condition: service_healthy
networks:
- forgejo_network
ports:
- "{{FORGEJO_BIND_IP:-0.0.0.0}}:{{FORGEJO_HTTP_PORT:-3000}}:3000"
- "{{FORGEJO_BIND_IP:-0.0.0.0}}:{{FORGEJO_SSH_PORT:-2222}}:22"
volumes:
- /etc/localtime:/etc/localtime:ro
- forgejo_data:/data
- type: bind
source: {{FORGEJO_DATA_ROOT}}/repositories
target: /data/git/repositories
bind:
create_host_path: false
- type: bind
source: {{FORGEJO_DATA_ROOT}}/lfs
target: /data/git/lfs
bind:
create_host_path: false
- type: bind
source: {{FORGEJO_DATA_ROOT}}/attachments
target: /data/gitea/attachments
bind:
create_host_path: false
- type: bind
source: {{FORGEJO_DATA_ROOT}}/avatars
target: /data/gitea/avatars
bind:
create_host_path: false
healthcheck:
test: ["CMD", "curl", "--fail", "--silent", "--show-error", "--output", "/dev/null", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: "{{FORGEJO_MEM_LIMIT:-1G}}"
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
forgejo_network:
driver: bridge
name: forgejo_network
volumes:
forgejo_db_data:
forgejo_data: