# 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: