Single hostname resolves both from browsers (RFC 6761) and inside docker (DNS alias). Fixes artifact upload ECONNREFUSED.
265 lines
12 KiB
YAML
265 lines
12 KiB
YAML
name: forgejo-stack
|
|
|
|
services:
|
|
db:
|
|
image: ${POSTGRES_IMAGE}
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_DB: ${POSTGRES_DB}
|
|
POSTGRES_USER: ${POSTGRES_USER}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
volumes:
|
|
- ./data/db:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
networks: [internal]
|
|
|
|
forgejo:
|
|
image: ${FORGEJO_IMAGE}
|
|
restart: unless-stopped
|
|
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: ${POSTGRES_DB}
|
|
FORGEJO__database__USER: ${POSTGRES_USER}
|
|
FORGEJO__database__PASSWD: ${POSTGRES_PASSWORD}
|
|
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
|
|
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_SSH_DOMAIN}
|
|
FORGEJO__server__ROOT_URL: ${FORGEJO_ROOT_URL}
|
|
# LOCAL_ROOT_URL is the URL Forgejo propagates to act_runner job
|
|
# containers for in-cluster API calls (artifact uploads, status
|
|
# updates, etc.). ROOT_URL is user-facing (http://localhost:3000/);
|
|
# job containers in a separate Docker container can't reach the host's
|
|
# localhost. Point this at the Forgejo container's hostname on the
|
|
# shared internal network instead.
|
|
FORGEJO__server__LOCAL_ROOT_URL: http://forgejo:3000/
|
|
FORGEJO__server__SSH_PORT: ${FORGEJO_SSH_PORT}
|
|
FORGEJO__server__START_SSH_SERVER: "false"
|
|
FORGEJO__service__DISABLE_REGISTRATION: "true"
|
|
FORGEJO__security__INSTALL_LOCK: "true"
|
|
FORGEJO__security__SECRET_KEY: ${FORGEJO_SECRET_KEY}
|
|
FORGEJO__security__INTERNAL_TOKEN: ${FORGEJO_INTERNAL_TOKEN}
|
|
FORGEJO__oauth2__JWT_SECRET: ${FORGEJO_JWT_SECRET}
|
|
FORGEJO__server__LFS_JWT_SECRET: ${FORGEJO_LFS_JWT_SECRET}
|
|
FORGEJO__webhook__ALLOWED_HOST_LIST: "*"
|
|
FORGEJO__actions__ENABLED: "true"
|
|
ports:
|
|
- "${FORGEJO_HTTP_PORT}:3000"
|
|
- "${FORGEJO_SSH_PORT}:22"
|
|
volumes:
|
|
- ./data/forgejo:/data
|
|
- /etc/timezone:/etc/timezone:ro
|
|
- /etc/localtime:/etc/localtime:ro
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/v1/version"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 12
|
|
# Network alias so job containers can resolve the same hostname browsers
|
|
# use. *.localhost is special: per RFC 6761 every modern browser resolves
|
|
# it to 127.0.0.1 without an /etc/hosts entry, so users still reach
|
|
# Forgejo through the host port forward. Docker DNS resolves it inside
|
|
# the internal network to this container's IP. One hostname, two
|
|
# working resolution paths. Avoids the ACTIONS_RUNTIME_URL mismatch
|
|
# that breaks artifact uploads when ROOT_URL is host-facing localhost.
|
|
networks:
|
|
internal:
|
|
aliases:
|
|
- forgejo.localhost
|
|
|
|
runner:
|
|
image: ${RUNNER_IMAGE}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
forgejo:
|
|
condition: service_healthy
|
|
environment:
|
|
FORGEJO_INSTANCE_URL: http://forgejo:3000
|
|
# Token written by bootstrap into data/runner/.runner-token
|
|
DOCKER_HOST: unix:///var/run/docker.sock
|
|
RUNNER_NAME: ${RUNNER_NAME:-local-runner}
|
|
volumes:
|
|
- ./data/runner:/data
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
# Mounted here only so `docker compose up` materialises the named volume;
|
|
# job containers consume it through `container.options` below (the host
|
|
# docker daemon sees the volume under its fixed name). Path must match
|
|
# AGENT_TOOLSDIRECTORY in catthehacker images (/opt/hostedtoolcache) so
|
|
# actions/setup-* find tools written by prior jobs.
|
|
- hostedtoolcache:/opt/hostedtoolcache
|
|
# Add the host's docker group GID so the in-container user (uid 1000) can
|
|
# talk to /var/run/docker.sock. Override via DOCKER_GID in .env if your
|
|
# host's docker group is not 984.
|
|
group_add:
|
|
- "${DOCKER_GID:-984}"
|
|
networks: [internal]
|
|
# Override entrypoint so the container waits for the registration token
|
|
# file to appear (written by bootstrap-forgejo.sh on first run), then
|
|
# registers and runs.
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
cd /data
|
|
# Image used for `ubuntu-latest` jobs. Defaults to the locally-built
|
|
# forgejo-stack/job:latest (catthehacker runner-22.04 + yarn classic);
|
|
# see runner-image/Dockerfile and setup.sh which builds it.
|
|
JOB_IMAGE="$${RUNNER_JOB_IMAGE:-forgejo-stack/job:latest}"
|
|
if [ ! -f .runner ]; then
|
|
echo "waiting for /data/.runner-token from bootstrap..."
|
|
until [ -s /data/.runner-token ]; do sleep 2; done
|
|
forgejo-runner register \
|
|
--no-interactive \
|
|
--instance "$$FORGEJO_INSTANCE_URL" \
|
|
--token "$$(cat /data/.runner-token)" \
|
|
--name "$${RUNNER_NAME:-local-runner}" \
|
|
--labels "docker:docker://$$JOB_IMAGE,ubuntu-latest:docker://$$JOB_IMAGE"
|
|
rm -f /data/.runner-token
|
|
fi
|
|
# Pin job containers to the compose network so actions/checkout can
|
|
# resolve "forgejo:3000". Default per-task bridge networks can't.
|
|
# Network name matches `name: forgejo-stack` at the top of this file —
|
|
# override with RUNNER_JOB_NETWORK in .env if the project name changes.
|
|
# Also overrides labels (image map) and capacity for already-registered
|
|
# runners without re-registration.
|
|
JOB_NET="$${RUNNER_JOB_NETWORK:-forgejo-stack_internal}"
|
|
CAPACITY="$${RUNNER_CAPACITY:-6}"
|
|
# Extra `docker run` args forwarded to every job container. Default
|
|
# mounts a shared toolcache so `actions/setup-node`, `setup-go`, etc.
|
|
# hit cache on the second job instead of re-downloading. Mount path
|
|
# must be AGENT_TOOLSDIRECTORY (=/opt/hostedtoolcache in catthehacker
|
|
# images); mounting on /opt/acttoolcache is a dead end because
|
|
# setup-* never writes there. Volume name is fixed via `name:` at the
|
|
# bottom of this file so renaming the compose project doesn't orphan
|
|
# the cache.
|
|
JOB_OPTIONS="$${RUNNER_JOB_OPTIONS:--v forgejo-stack-hostedtoolcache:/opt/hostedtoolcache}"
|
|
# Named volumes referenced from JOB_OPTIONS must be allow-listed here or
|
|
# forgejo-runner silently drops the mount with "is not a valid volume,
|
|
# will be ignored". Defaults match the cache volume above; override with
|
|
# RUNNER_VALID_VOLUMES (comma-separated) if you add more.
|
|
VALID_VOLUMES="$${RUNNER_VALID_VOLUMES:-forgejo-stack-hostedtoolcache}"
|
|
# ACTIONS_RUNTIME_URL / ACTIONS_RESULTS_URL: Forgejo 10's actions
|
|
# service emits ROOT_URL (http://localhost:3000/) as the runtime URL
|
|
# it gives to job containers. Inside a job container, localhost:3000
|
|
# is the container itself — nothing listens, artifact uploads fail
|
|
# with ECONNREFUSED. Inject the in-cluster URL via runner.envs so
|
|
# job containers see the correct host (the forgejo service on the
|
|
# shared internal network) regardless of what Forgejo emits.
|
|
# Override with RUNNER_INTERNAL_FORGEJO_URL in .env if you renamed
|
|
# the forgejo service.
|
|
INTERNAL_FORGEJO_URL="$${RUNNER_INTERNAL_FORGEJO_URL:-http://forgejo:3000/}"
|
|
cat > /data/config.yaml <<CONFIG
|
|
runner:
|
|
capacity: $$CAPACITY
|
|
envs:
|
|
ACTIONS_RUNTIME_URL: "$$INTERNAL_FORGEJO_URL"
|
|
ACTIONS_RESULTS_URL: "$$INTERNAL_FORGEJO_URL"
|
|
labels:
|
|
- "docker:docker://$$JOB_IMAGE"
|
|
- "ubuntu-latest:docker://$$JOB_IMAGE"
|
|
container:
|
|
network: $$JOB_NET
|
|
options: "$$JOB_OPTIONS"
|
|
valid_volumes:
|
|
CONFIG
|
|
# Append one YAML list entry per comma-separated volume name.
|
|
printf '%s\n' "$$VALID_VOLUMES" | tr ',' '\n' | while IFS= read -r v; do
|
|
[ -n "$$v" ] && echo " - \"$$v\"" >> /data/config.yaml
|
|
done
|
|
exec forgejo-runner daemon --config /data/config.yaml
|
|
|
|
# Watches the consumer repo's yarn.lock and rebuilds forgejo-stack/job:latest
|
|
# whenever it changes, so the in-image yarn offline cache stays current
|
|
# without manual intervention. Polls every RUNNER_REFRESH_INTERVAL seconds
|
|
# (default 300); a single iteration is ~1s when nothing changed, ~90s when
|
|
# rebuilding. Idle if RUNNER_CACHE_SEED_REPOS is unset.
|
|
runner-refresh:
|
|
image: docker:cli
|
|
restart: unless-stopped
|
|
depends_on:
|
|
forgejo:
|
|
condition: service_healthy
|
|
environment:
|
|
FORGEJO_BASE_URL: http://forgejo:3000
|
|
FORGEJO_ADMIN_TOKEN: ${FORGEJO_ADMIN_TOKEN}
|
|
RUNNER_CACHE_SEED_REPOS: ${RUNNER_CACHE_SEED_REPOS:-}
|
|
RUNNER_JOB_IMAGE: ${RUNNER_JOB_IMAGE:-forgejo-stack/job:latest}
|
|
INTERVAL: ${RUNNER_REFRESH_INTERVAL:-300}
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
# Project dir mounted RW so the refresh script can update
|
|
# runner-image/cache-seed/ and feed it back into `docker build`.
|
|
- .:/stack
|
|
working_dir: /stack
|
|
networks: [internal]
|
|
entrypoint:
|
|
- /bin/sh
|
|
- -c
|
|
- |
|
|
set -e
|
|
# docker:cli is alpine-based and lacks bash + curl by default; the
|
|
# refresh script needs both. Install once per container lifetime.
|
|
apk add --no-cache bash curl >/dev/null
|
|
if [ -z "$$RUNNER_CACHE_SEED_REPOS" ]; then
|
|
echo "runner-refresh: RUNNER_CACHE_SEED_REPOS unset; idling"
|
|
exec sleep infinity
|
|
fi
|
|
while true; do
|
|
bash /stack/scripts/refresh-runner-image.sh || \
|
|
echo "runner-refresh: iteration failed; retrying in $$INTERVAL s"
|
|
sleep "$$INTERVAL"
|
|
done
|
|
|
|
forgejo-mcp:
|
|
image: ${FORGEJO_MCP_IMAGE}
|
|
restart: unless-stopped
|
|
depends_on:
|
|
forgejo:
|
|
condition: service_healthy
|
|
environment:
|
|
# The ronmi/forgejo-mcp image reads FORGEJOMCP_SERVER / FORGEJOMCP_TOKEN
|
|
# (single underscore — verified via `forgejo-mcp --help`).
|
|
FORGEJOMCP_SERVER: http://forgejo:3000
|
|
FORGEJOMCP_TOKEN: ${FORGEJO_MCP_TOKEN}
|
|
# IMPORTANT: ronmi/forgejo-mcp is a `FROM scratch` image — it has no shell,
|
|
# no `ls`, just `/forgejo-mcp`. The plan's `sleep infinity` entrypoint is
|
|
# therefore impossible. We instead run the binary in HTTP mode, which is
|
|
# long-lived and can be smoke-tested with curl. Claude Code itself uses
|
|
# stdio mode via a separate `docker run --rm -i ... stdio` invocation
|
|
# registered in ~/space/.mcp.json (see bootstrap-forgejo.sh).
|
|
command:
|
|
- http
|
|
- --address
|
|
- ":8080"
|
|
ports:
|
|
- "${FORGEJO_MCP_HTTP_PORT:-8181}:8080"
|
|
networks: [internal]
|
|
|
|
volumes:
|
|
# Shared `actions/setup-*` toolcache across all job containers. First run
|
|
# downloads, subsequent jobs hit cache. Fixed name keeps the volume stable
|
|
# if the compose project is renamed. Path inside containers must match
|
|
# AGENT_TOOLSDIRECTORY (=/opt/hostedtoolcache for catthehacker images).
|
|
hostedtoolcache:
|
|
name: forgejo-stack-hostedtoolcache
|
|
|
|
networks:
|
|
internal:
|
|
driver: bridge
|
|
# MTU for the internal bridge. Default 1500 works on bare-metal LAN. Lower
|
|
# this when the host's default route goes through a VPN/tunnel (WireGuard,
|
|
# OpenVPN, etc.) — otherwise large TCP packets get black-holed and any
|
|
# tarball-style downloads inside job containers (yarn install, pip, apt)
|
|
# silently hang. Check your tunnel MTU with:
|
|
# ip link show $(ip route get 1.1.1.1 | grep -oP 'dev \K\S+') | grep mtu
|
|
driver_opts:
|
|
com.docker.network.driver.mtu: "${INTERNAL_NETWORK_MTU:-1500}"
|