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.
165 lines
6.7 KiB
YAML
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:
|