disclosure-bureau/infra/disclosure-stack/docker-compose.yml
Luiz Gustavo 55cac8a395
Some checks failed
CI / Web — typecheck + lint + build (push) Failing after 1m30s
CI / Scripts — Python smoke (push) Failing after 32s
CI / Web — npm audit (push) Failing after 37s
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI
W0 — security hardening (5 fixes verified live on disclosure.top)
- middleware: gate /api/admin/* same as /admin/* (F1)
- imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2)
- studio: real basic-auth label (bcrypt hash, middleware reference) (F3)
- relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4)
- migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2)

W1 — observability + resilience + autocomplete
- studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck
- compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env
- claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS)
- openrouter.ts: retry with exponential backoff + Retry-After + in-memory
  circuit breaker (promotes FALLBACK after CB_THRESHOLD failures)
- lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper
- middleware: mints correlation_id, stamps x-correlation-id response header,
  emits structured http_request log per /api/* call
- messages/route.ts: switch to structured logger
- 60_meili_index.py: push documents + chunks into Meilisearch
- /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50
- search-autocomplete.tsx: debounced dropdown wired into search-panel

W1.2 — Glitchtip + Forgejo self-hosted
- compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2)
- compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988
- @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts)
- /api/admin/throw smoke endpoint (gated by W0-F1 middleware)
- Synthetic event ingestion verified at glitchtip.disclosure.top
- forgejo.disclosure.top up, repo discadmin/disclosure-bureau created,
  runner registered (labels: ubuntu-latest, docker)
- .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python
  syntax + compose validation

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 18:18:42 -03:00

515 lines
21 KiB
YAML

# Disclosure Bureau — full deployment stack.
# Routed via the host's existing plegal-traefik (network: traefik-public).
# Internal services share the disclosure-internal network and are NOT exposed
# to the host. Public services (web, kong, studio, search) get Traefik labels.
name: disclosure
networks:
internal:
name: disclosure-internal
driver: bridge
traefik:
name: traefik-public
external: true
volumes:
db-data:
storage-data:
meili-data:
hf-cache:
glitchtip-redis-data:
glitchtip-uploads:
forgejo-data:
forgejo-runner-config:
services:
# ─── Database ─────────────────────────────────────────────────────────────
db:
container_name: disclosure-db
image: supabase/postgres:15.8.1.060
restart: unless-stopped
networks: [internal]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost"]
interval: 10s
timeout: 5s
retries: 12
environment:
POSTGRES_HOST: /var/run/postgresql
POSTGRES_PORT: 5432
POSTGRES_DB: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGPASSWORD: ${POSTGRES_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: 3600
POSTGRES_INITDB_ARGS: "--data-checksums"
command:
- postgres
- -c
- shared_buffers=${POSTGRES_SHARED_BUFFERS:-384MB}
- -c
- work_mem=${POSTGRES_WORK_MEM:-12MB}
- -c
- maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-96MB}
- -c
- max_connections=${POSTGRES_MAX_CONNECTIONS:-80}
volumes:
- db-data:/var/lib/postgresql/data
# ─── Auth (GoTrue) ────────────────────────────────────────────────────────
auth:
container_name: disclosure-auth
image: supabase/gotrue:v2.170.0
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: https://${DOMAIN_API}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres?search_path=auth
GOTRUE_SITE_URL: https://${DOMAIN_MAIN}
# Explicit external URL so confirmation links land on the public site
# (Next.js /auth/callback), not on the Kong gateway host.
GOTRUE_API_EXTERNAL_URL: https://${DOMAIN_MAIN}
GOTRUE_URI_ALLOW_LIST: https://${DOMAIN_MAIN},https://www.${DOMAIN_MAIN}
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: 3600
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
# SMTP configured (Spacemail) → email confirmation required for signups.
GOTRUE_MAILER_AUTOCONFIRM: "false"
GOTRUE_MAILER_OTP_EXP: 3600
GOTRUE_SMTP_HOST: ${SMTP_HOST}
GOTRUE_SMTP_PORT: ${SMTP_PORT}
GOTRUE_SMTP_USER: ${SMTP_USER}
GOTRUE_SMTP_PASS: ${SMTP_PASS}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_FROM}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_FROM_NAME}
GOTRUE_MAILER_URLPATHS_INVITE: /auth/callback
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/callback
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/callback
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/callback
# ─── PostgREST ────────────────────────────────────────────────────────────
rest:
container_name: disclosure-rest
image: postgrest/postgrest:v12.2.8
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: 3600
# ─── Realtime ─────────────────────────────────────────────────────────────
realtime:
container_name: disclosure-realtime
image: supabase/realtime:v2.30.34
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PORT: 4000
DB_HOST: db
DB_PORT: 5432
DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: postgres
DB_ENC_KEY: ${VAULT_ENC_KEY}
API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: "true"
RUN_JANITOR: "true"
# ─── Storage ──────────────────────────────────────────────────────────────
storage:
container_name: disclosure-storage
image: supabase/storage-api:v1.14.3
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
rest: { condition: service_started }
imgproxy: { condition: service_started }
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
volumes:
- storage-data:/var/lib/storage
imgproxy:
container_name: disclosure-imgproxy
image: darthsim/imgproxy:v3.8.0
restart: unless-stopped
networks: [internal]
environment:
IMGPROXY_BIND: ":5001"
# W0-F2: tighten filesystem root from "/" (whole VPS) to the Storage
# backend mount only. Imgproxy never reads outside Storage objects.
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /var/lib/storage
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
volumes:
- storage-data:/var/lib/storage
# ─── pg-meta + Studio ─────────────────────────────────────────────────────
meta:
container_name: disclosure-meta
image: supabase/postgres-meta:v0.83.2
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: db
PG_META_DB_PORT: 5432
PG_META_DB_NAME: postgres
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
studio:
container_name: disclosure-studio
image: supabase/studio:20241202-71e5240
restart: unless-stopped
networks: [internal, traefik]
depends_on:
meta: { condition: service_started }
environment:
# W1: Next.js standalone server binds to the container hostname by
# default, leaving 127.0.0.1 unreachable — the Docker healthcheck
# (fetch 127.0.0.1:3000/api/profile) then loops on ECONNREFUSED and
# the service never goes healthy. HOSTNAME=0.0.0.0 forces it to bind
# on all interfaces so both the loopback and the docker IP respond.
HOSTNAME: 0.0.0.0
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: "Disclosure Bureau"
DEFAULT_PROJECT_NAME: "disclosure"
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: https://${DOMAIN_API}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-studio.rule=Host(`${DOMAIN_STUDIO}`)
- traefik.http.routers.disclosure-studio.entrypoints=websecure
- traefik.http.routers.disclosure-studio.tls=true
- traefik.http.routers.disclosure-studio.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-studio.loadbalancer.server.port=3000
# W0-F3: real basic auth (was effectively disabled with usersfile=/dev/null).
# The user/password is DASHBOARD_USERNAME / DASHBOARD_PASSWORD from .env;
# the bcrypt hash below was generated with $$ doubled for compose escaping.
# Rotate by regenerating: htpasswd -nbB <user> <pass> (then double every $).
- traefik.http.middlewares.disclosure-studio-auth.basicauth.users=admin:$$2b$$05$$tFLAMGNWX7xDbVyQ/O0G1.ruLwm3Le1.ErgdUTB9IYeJeH2FHd4ha
- traefik.http.routers.disclosure-studio.middlewares=disclosure-studio-auth@docker
# ─── Kong API gateway ─────────────────────────────────────────────────────
kong:
container_name: disclosure-kong
image: kong:2.8.1
restart: unless-stopped
networks: [internal, traefik]
depends_on:
auth: { condition: service_started }
rest: { condition: service_started }
realtime: { condition: service_started }
storage: { condition: service_started }
environment:
KONG_DATABASE: "off"
# Read rendered config (envsubst happens at startup, see entrypoint below)
KONG_DECLARATIVE_CONFIG: /tmp/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
# Kong declarative config does NOT do env substitution by itself. We
# render the template into /tmp/kong.yml at container start so the JWT
# keys land literally in the config Kong actually loads.
user: root
entrypoint:
- /bin/sh
- -c
- |
apk add --no-cache gettext >/dev/null 2>&1 || true
envsubst < /usr/local/kong/kong.yml.tmpl > /tmp/kong.yml
chown kong:kong /tmp/kong.yml
exec /docker-entrypoint.sh kong docker-start
volumes:
- ./kong.yml:/usr/local/kong/kong.yml.tmpl:ro
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-api.rule=Host(`${DOMAIN_API}`)
- traefik.http.routers.disclosure-api.entrypoints=websecure
- traefik.http.routers.disclosure-api.tls=true
- traefik.http.routers.disclosure-api.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-api.loadbalancer.server.port=8000
# ─── Meilisearch ──────────────────────────────────────────────────────────
meilisearch:
container_name: disclosure-meili
image: getmeili/meilisearch:v1.10
restart: unless-stopped
networks: [internal, traefik]
environment:
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
MEILI_NO_ANALYTICS: "true"
MEILI_ENV: production
MEILI_MAX_INDEXING_MEMORY: ${MEILI_MAX_INDEXING_MEMORY:-512MB}
volumes:
- meili-data:/meili_data
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-search.rule=Host(`${DOMAIN_SEARCH}`)
- traefik.http.routers.disclosure-search.entrypoints=websecure
- traefik.http.routers.disclosure-search.tls=true
- traefik.http.routers.disclosure-search.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-search.loadbalancer.server.port=7700
# ─── Next.js web (Disclosure Bureau frontend) ─────────────────────────────
web:
container_name: disclosure-web
build:
context: /data/disclosure/web # rsynced from laptop, see scripts/sync-data.sh
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
restart: unless-stopped
networks: [internal, traefik]
depends_on:
kong: { condition: service_started }
environment:
NODE_ENV: production
NODE_OPTIONS: ${NEXT_NODE_OPTIONS:---max-old-space-size=768}
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
UFO_ROOT: /data/ufo
# W1-TD#10: bump pg pool from default 5 to 20 (chat agent + hybrid_search
# can saturate the smaller pool under concurrent load).
PG_POOL_MAX: ${PG_POOL_MAX:-20}
# Chat agent (W1-F8: CLAUDE_CODE_OAUTH_TOKEN only injected when the
# provider actually uses it — default provider is openrouter, so the token
# stays absent from this container's env unless CHAT_PROVIDER=claude-code).
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN_FOR_WEB:-}
CLAUDE_CODE_MODEL: ${CLAUDE_CODE_MODEL}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
OPENROUTER_MODEL: ${OPENROUTER_MODEL}
OPENROUTER_FALLBACK_MODEL: ${OPENROUTER_FALLBACK_MODEL}
CHAT_PROVIDER: ${CHAT_PROVIDER}
# Meilisearch (used by /api/search)
MEILISEARCH_URL: http://meilisearch:7700
MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY}
# Embed service (used by /lib/retrieval)
EMBED_SERVICE_URL: http://embed:8000
# pgvector + chunks (hybrid_search)
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
# W1.2 — Glitchtip error monitoring (DSN issued by manage.py bootstrap)
SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
NEXT_PUBLIC_SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
volumes:
- ${DATA_WIKI}:/data/ufo/wiki:ro
- ${DATA_PROCESSING}:/data/ufo/processing:ro
- ${DATA_RAW}:/data/ufo/raw:ro
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-web.rule=Host(`app.${DOMAIN_MAIN}`) || Host(`${DOMAIN_MAIN}`) || Host(`www.${DOMAIN_MAIN}`)
- traefik.http.routers.disclosure-web.entrypoints=websecure
- traefik.http.routers.disclosure-web.tls=true
- traefik.http.routers.disclosure-web.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-web.loadbalancer.server.port=3000
# www → apex redirect
- traefik.http.middlewares.disclosure-www-redir.redirectregex.regex=^https?://www\.${DOMAIN_MAIN}/(.*)
- traefik.http.middlewares.disclosure-www-redir.redirectregex.replacement=https://${DOMAIN_MAIN}/$${1}
- traefik.http.middlewares.disclosure-www-redir.redirectregex.permanent=true
# ─── BGE-M3 embedding + reranker service (CPU only) ───────────────────────
embed:
container_name: disclosure-embed
build:
context: ../embed-service
restart: unless-stopped
networks: [internal]
environment:
DEVICE: cpu
EMBED_MODEL: BAAI/bge-m3
RERANK_MODEL: BAAI/bge-reranker-v2-m3
HF_HUB_DOWNLOAD_TIMEOUT: 600
volumes:
- hf-cache:/cache
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 180s
deploy:
resources:
limits:
memory: 3g
# ─── Glitchtip — self-hosted Sentry-compatible error monitor (W1.2) ───────
glitchtip-redis:
container_name: disclosure-glitchtip-redis
image: redis:7-alpine
restart: unless-stopped
networks: [internal]
volumes:
- glitchtip-redis-data:/data
command: redis-server --appendonly yes
glitchtip-web:
container_name: disclosure-glitchtip-web
image: glitchtip/glitchtip:v4.2
restart: unless-stopped
networks: [internal, traefik]
depends_on:
db: { condition: service_healthy }
glitchtip-redis: { condition: service_started }
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
REDIS_URL: redis://glitchtip-redis:6379/0
PORT: "8080"
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
EMAIL_URL: consolemail://
ENABLE_USER_REGISTRATION: "false" # bootstrap admin via manage.py
ENABLE_ORGANIZATION_CREATION: "false"
CELERY_WORKER_AUTOSCALE: "1,3"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
volumes:
- glitchtip-uploads:/code/uploads
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-glitchtip.rule=Host(`glitchtip.disclosure.top`)
- traefik.http.routers.disclosure-glitchtip.entrypoints=websecure
- traefik.http.routers.disclosure-glitchtip.tls=true
- traefik.http.routers.disclosure-glitchtip.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-glitchtip.loadbalancer.server.port=8080
glitchtip-worker:
container_name: disclosure-glitchtip-worker
image: glitchtip/glitchtip:v4.2
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
glitchtip-redis: { condition: service_started }
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
REDIS_URL: redis://glitchtip-redis:6379/0
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
EMAIL_URL: consolemail://
CELERY_WORKER_AUTOSCALE: "1,3"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
volumes:
- glitchtip-uploads:/code/uploads
command: ./bin/run-celery-with-beat.sh
# ─── Forgejo — self-hosted Git + Actions CI (W1.2) ────────────────────────
forgejo:
container_name: disclosure-forgejo
image: codeberg.org/forgejo/forgejo:9
restart: unless-stopped
networks: [internal, traefik]
depends_on:
db: { condition: service_healthy }
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__service__DISABLE_REGISTRATION: "true" # admin invites only
FORGEJO__actions__ENABLED: "true"
FORGEJO__security__INSTALL_LOCK: "true"
volumes:
- forgejo-data:/data
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-forgejo.rule=Host(`forgejo.disclosure.top`)
- traefik.http.routers.disclosure-forgejo.entrypoints=websecure
- traefik.http.routers.disclosure-forgejo.tls=true
- traefik.http.routers.disclosure-forgejo.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-forgejo.loadbalancer.server.port=3000
forgejo-runner:
container_name: disclosure-forgejo-runner
image: code.forgejo.org/forgejo/runner:6
restart: unless-stopped
networks: [internal]
# GID of the docker group on the host — lets the runner (uid 1000) talk
# to the docker socket without running as root.
group_add:
- "988"
depends_on:
forgejo: { condition: service_started }
environment:
FORGEJO_INSTANCE_URL: http://forgejo:3000
FORGEJO_RUNNER_REGISTRATION_TOKEN: ${FORGEJO_RUNNER_TOKEN}
FORGEJO_RUNNER_NAME: disclosure-runner
volumes:
- forgejo-runner-config:/data
- /var/run/docker.sock:/var/run/docker.sock
command:
- sh
- -c
- |
sleep 10
if [ ! -f /data/.runner ]; then
forgejo-runner register --no-interactive --instance "$$FORGEJO_INSTANCE_URL" --token "$$FORGEJO_RUNNER_REGISTRATION_TOKEN" --name "$$FORGEJO_RUNNER_NAME" --labels 'ubuntu-latest:docker://node:20-bookworm,docker:host'
fi
forgejo-runner daemon