IPCR Forge v2: Radicle-only with rad-actions

Snapshot of the Radicle-only forge (cib + act, DinD staging registry)
before the Gitea-centric v3 POC replaces it. Also carries the gateway's
IMPORT_PUBLISHER mode, multi-segment IPNS paths and IPCR 1.2.0.
This commit is contained in:
Worph
2026-10-04 18:13:11 +00:00
parent a8a3037cba
commit f82543f518
7 changed files with 509 additions and 462 deletions
+182 -275
View File
@@ -4,35 +4,41 @@ name: ipcr-forge
#
# git push rad main v1.2.3 (developer)
# → radicle-node / radicle-api source of truth, peer-to-peer
# → radicle-gitea-bridge copies repos that commit .gitea/workflows/
# → gitea + gitea-runner GitHub-style Actions build the image
# → gitea registry staging: gitea-<domain>/gitea_admin/<app>:1.2.3
# → ipcr-gateway follows Gitea's registry imports every tag into IPFS, publishes IPNS
# → rad-actions (cib + act) the node's own CI: GitHub-style workflows, run on the
# commit straight from the node's storage
# → rad-actions-registry staging: localhost:5000/<repo>:1.2.3 (internal only)
# → ipcr-gateway follows that registry imports every tag into IPFS, publishes IPNS
# → ipcr-kubo / ipcr-registry
# docker pull ipcr.localhost:4767/ipns/<name>:1.2.3 (anyone)
#
# Radicle is the only place anything originates. There is no mirror, no second forge and no
# bridge: the CI broker reads the node's events and its storage, and writes each result back
# into the repository as a job COB signed by the node. The CI's pages (cib's reports and the run
# logs) are static files the landing page serves under /ci/.
#
# The tile opens ipcr-forge-<domain>: a static landing page (nginx) that explains the pipeline,
# links Radicle and Gitea, and lists every published image with the exact `image:` line to use
# (IPCR's published.json, served as /images.json). <name> is the IPNS key IPCR minted for the
# links Radicle and the CI runs, and lists every published image with the exact `image:` line to
# use (IPCR's published.json, served as /images.json). <name> is the IPNS key IPCR minted for the
# repository (k51…), not the repository's name, which is why the page lists it.
#
# Every service is copied verbatim from its standalone listing (Apps/Radicle, Apps/Gitea,
# Apps/Radicle-Gitea-Bridge, Apps/IPCR), so each keeps its own comments and rationale.md.
# Only the data folders move, one subfolder per part: $AppID/{radicle,gitea,bridge,ipcr}/.
# Radicle and IPCR services are copied verbatim from their standalone listings (Apps/Radicle,
# Apps/IPCR), so each keeps its own comments and rationale.md. Only the data folders move, one
# subfolder per part: $AppID/{radicle,ci,ipcr}/.
#
# Replaces those four apps; it cannot sit beside them. It uses the same container names,
# domains (radicle-, radicle-api-, gitea-) and host ports (8776, 4768, 127.0.0.1:4767).
# Replaces the Radicle and IPCR apps; it cannot sit beside them. It uses the same container
# names, domains (radicle-, radicle-api-) and host ports (8776, 4768, 127.0.0.1:4767).
#
# The handoff from Gitea's registry to IPFS is a pull, run by IPCR: the runner's jobs run in a
# nested Docker that cannot reach ipcr.localhost (the *host's* loopback), so CI never pushes to
# IPCR. Instead ipcr-gateway polls gitea:3000 over gitea-internal, with a read-only token the
# bridge-credentials step mints. Gitea does not know IPCR exists.
# The handoff from CI to IPFS is a pull, run by IPCR: jobs run in the CI's own Docker daemon,
# which cannot reach ipcr.localhost (the *host's* loopback). So a workflow pushes to the staging
# registry, which shares that daemon's network namespace and is therefore `localhost:5000` to
# every job, and ipcr-gateway polls it over ci-internal.
services:
# ═══ Landing page — the tile's web UI ══════════════════════════════════════════
# Static: the page, its icon, and IPCR's list of published images (read-only). It shows only
# links, documentation and image names that are public on IPFS anyway, so it is not behind
# the SSO gate; Gitea keeps its own login, Radicle is public by design.
# Static: the page, its icon, IPCR's list of published images and the CI's pages (read-only).
# It shows only links, documentation, image names that are public on IPFS anyway, and build
# logs of public repositories (the adapter does not run private ones), so it is not behind
# the SSO gate; Radicle is public by design.
ipcr-forge:
image: nginxinc/nginx-unprivileged:1.29.3-alpine
container_name: ipcr-forge
@@ -56,6 +62,8 @@ services:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/web/default.conf:/etc/nginx/conf.d/default.conf:ro
# published.json → /images.json. Read-only; the location in default.conf serves that file only.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/srv/ipcr-state:ro
# The CI broker's report pages and the run logs → /ci/. Read-only.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/srv/ci:ro
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/"]
interval: 30s
@@ -202,143 +210,104 @@ services:
networks:
- radicle-internal
# ═══ Gitea — CI and the staging registry ═══════════════════════════════════════
# Verbatim from Apps/Gitea; only its data folder moves to $AppID/gitea/.
# ═══ CI — Radicle CI broker + act ═════════════════════════════════════════════════
# Gitea itself — git hosting, the Actions controller, and the OCI package
# registry, all in one process. SQLite: a personal forge never has the write
# concurrency that would justify a second container for Postgres.
gitea:
image: gitea/gitea:1.27.3-rootless
container_name: gitea
# cib (radicle-ci-broker) follows the node's events over its control socket, keeps the ones its
# filters allow (changes signed by a delegate, in a repository with a workflow folder — see
# ci/ci-broker.yaml in the IPCR repository), and runs the rad-actions adapter for each. The
# adapter checks the commit out of the node's storage and runs the matching GitHub-style
# workflows with act, against the Docker daemon below. cib writes HTML reports, and the result
# of every run back into the repository as a job COB signed by this node.
rad-actions:
image: ghcr.io/yundera/rad-actions:0.1.0
container_name: rad-actions
user: $PUID:$PGID
# Reaps act's and git's children: cib is not an init.
init: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
cpu_shares: 70
cpu_shares: 20
deploy:
resources:
limits:
memory: 1G
memory: 512M
environment:
TZ: $TZ
# Every URL Gitea hands out — clone URLs, the registry host, and the
# server_url the CI runner passes to jobs — is derived from ROOT_URL, so
# it has to be the address that works from outside as well as inside.
GITEA__server__ROOT_URL: https://gitea-${APP_DOMAIN}/
GITEA__server__DOMAIN: gitea-${APP_DOMAIN}
GITEA__server__HTTP_PORT: "3000"
GITEA__server__PROTOCOL: http
# The address Gitea's own CLI uses to reach the running server's internal
# API. It defaults to localhost, which is right for the server itself and
# useless from the init container that runs
# `gitea actions generate-runner-token`. It has to be set here rather than
# there: the CLI reads it out of the shared app.ini, and only this
# container's entrypoint renders environment into app.ini.
GITEA__server__LOCAL_ROOT_URL: http://gitea:3000/
# Git over SSH would need a published host port of its own. Everything
# here — clone, push, CI, registry — goes over HTTPS instead.
GITEA__server__DISABLE_SSH: "true"
GITEA__server__START_SSH_SERVER: "false"
# Skips the first-run install wizard: every value it would ask for is set
# here, and the admin account is created by an init step below.
GITEA__security__INSTALL_LOCK: "true"
GITEA__database__DB_TYPE: sqlite3
GITEA__database__PATH: /var/lib/gitea/data/gitea.db
GITEA__service__DISABLE_REGISTRATION: "true"
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
GITEA__actions__ENABLED: "true"
# Where a bare `uses: actions/checkout@v4` is resolved from.
GITEA__actions__DEFAULT_ACTIONS_URL: github
GITEA__packages__ENABLED: "true"
# Lets a pull mirror name a repository on this server's own Docker
# network — a Radicle node's git endpoint, for instance. See rationale.md.
GITEA__migrations__ALLOW_LOCALNETWORKS: "true"
# How often a pull mirror is re-fetched. The sync emits the same push
# event a real push would, so this interval is also how quickly CI
# reacts to a commit that arrives from the mirrored source.
GITEA__mirror__DEFAULT_INTERVAL: 10m
GITEA__mirror__MIN_INTERVAL: 10m
# The daemon jobs run in. Its socket is the only thing shared with it.
DOCKER_HOST: unix:///run/dind/docker.sock
# The images act runs a `runs-on:` label in. act's own medium images (Ubuntu with Node and
# the Docker CLI), the ones act users run on their laptops.
RAD_ACTIONS_PLATFORMS: ubuntu-latest=catthehacker/ubuntu:act-latest,ubuntu-24.04=catthehacker/ubuntu:act-24.04,ubuntu-22.04=catthehacker/ubuntu:act-22.04
# `vars` every workflow sees. IMAGE = IMAGE_PREFIX/<repository name>, the name IPCR imports.
RAD_ACTIONS_VARS: REGISTRY=localhost:5000,IMAGE_PREFIX=localhost:5000
# cib's pages link each run to its log there (served by ipcr-forge).
RAD_ACTIONS_LOG_URL: /ci/logs/
# The repository's web page, as the adapter's GitHub API stand-in reports it (html_url, which
# docker/metadata-action turns into the image's org.opencontainers.image.source label).
RAD_ACTIONS_REPO_URL: https://radicle-${APP_DOMAIN}/nodes/radicle-api-${APP_DOMAIN}/{rid}
volumes:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/data:/var/lib/gitea
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/config:/etc/gitea
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
expose:
- 3000
labels:
caddy_0: gitea-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 3000}}"
caddy_1: gitea-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 3000}}"
caddy_2: gitea-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 3000}}"
# The node's home: its control socket (events), its storage (checkouts, job COBs) and its
# key, which signs the job COBs. The same mount radicle-api has.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/
# cib's database, its pages and the run logs, act's action cache, and scratch checkouts.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/state:/ci/state
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/html:/ci/html
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/cache:/ci/cache
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/home:/ci/home
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/work:/ci/work
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind
depends_on:
- radicle-node
- rad-actions-docker
networks:
- pcs
- gitea-internal
# Egress only: act fetches actions (actions/checkout, docker/*) from GitHub.
- ci-internal
# The CI runner: act_runner with its own Docker daemon inside it (dind), so
# workflow jobs and the images they build never touch the host's daemon.
# See rationale.md for what `privileged` buys and what it costs.
gitea-runner:
image: gitea/act_runner:0.6.1-dind
container_name: gitea-runner
restart: unless-stopped
# The daemon jobs run in: Docker-in-Docker, so jobs and the images they build never touch the
# host's daemon. See rationale.md for what `privileged` buys and what it costs. No TCP listener:
# its only API is a unix socket in a folder shared with rad-actions, owned by $PGID.
rad-actions-docker:
image: docker:29.1-dind
container_name: rad-actions-docker
privileged: true
restart: unless-stopped
cpu_shares: 20
command: ["--host=unix:///run/dind/docker.sock", "--group=$PGID"]
deploy:
resources:
limits:
memory: 2G
environment:
TZ: $TZ
# This address is not just how the runner daemon reaches Gitea — it is
# handed to every job as GITHUB_SERVER_URL, and a job runs inside the
# runner's *nested* daemon, on a network where `gitea` does not resolve.
# So it has to be an address that works from the outside: checkout and
# `docker login` from a workflow both go out and back over HTTPS.
GITEA_INSTANCE_URL: https://gitea-${APP_DOMAIN}
GITEA_RUNNER_REGISTRATION_TOKEN_FILE: /data/token
GITEA_RUNNER_NAME: gitea-runner
# Registration-time labels. runner-config/config.yaml repeats them, and
# that copy wins on every start, so an installed runner follows store updates.
GITEA_RUNNER_LABELS: ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04,docker:docker://docker:28.5-cli
CONFIG_FILE: /config/config.yaml
# DOCKER_TLS_CERTDIR is deliberately left at the image's default. Emptying
# it does not turn the nested daemon's TCP listener off — it turns the
# listener's TLS off, and 2375 then answers unauthenticated to anything
# that can reach this container.
# No certificates: with an explicit --host the entrypoint adds no TCP listener to protect.
DOCKER_TLS_CERTDIR: ""
volumes:
# .runner (the registration this runner is identified by) and the
# registration token the init step leaves for it.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner:/data
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config:/config:ro
# Replaces the image's own start-up wait — see the comments in the
# script. It is declared under x-compose-app.files rather than left to
# the seed tree, because it has to arrive executable.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-config/act_runner-run:/etc/s6/act_runner/run:ro
# The inner daemon's image store. Without this every restart re-pulls
# every base image a workflow uses.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/gitea/runner-docker:/var/lib/docker
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/socket:/run/dind
# The daemon's image store. Without it every restart re-pulls every job image.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/docker:/var/lib/docker
healthcheck:
test: ["CMD", "docker", "-H", "unix:///run/dind/docker.sock", "info"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- gitea-internal
- ci-internal
# ═══ Radicle → Gitea bridge ════════════════════════════════════════════════════
# Verbatim from Apps/Radicle-Gitea-Bridge; only its data folder moves to $AppID/bridge/.
radicle-gitea-bridge:
image: ghcr.io/worph/radicle-gitea-bridge:1.0.0
container_name: radicle-gitea-bridge
# The staging registry, in the daemon's network namespace: `localhost:5000` to the daemon and to
# every job (act runs jobs on the host network, which is this namespace), so a plain
# `docker push` or buildx with `network=host` works without TLS or credentials — Docker allows
# plain HTTP to localhost only. ipcr-gateway reaches it as rad-actions-docker:5000. Nothing
# outside ci-internal can.
rad-actions-registry:
image: registry:3.0.0
container_name: rad-actions-registry
user: $PUID:$PGID
# Reads two HTTP APIs and runs git. Needs no capability at all.
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
command: ["bridge"]
network_mode: service:rad-actions-docker
cpu_shares: 20
deploy:
resources:
@@ -346,31 +315,14 @@ services:
memory: 128M
environment:
TZ: $TZ
# git wants a home; the image's is root's and this runs as $PUID.
HOME: /tmp
RAD_API: http://radicle-api:8080
GITEA_URL: http://gitea:3000
# The Gitea account that owns the copies — the one the Gitea app creates.
GITEA_OWNER: gitea_admin
GITEA_TOKEN_FILE: /secrets/gitea-token
# A repository opts in by containing this path at its head.
OPT_IN_PATH: .gitea/workflows
# Seconds between passes. Releases are not latency-sensitive.
INTERVAL: "30"
REGISTRY_HTTP_ADDR: 0.0.0.0:5000
# Staging only: IPCR has its own copy once imported, so old tags may be deleted.
REGISTRY_STORAGE_DELETE_ENABLED: "true"
OTEL_TRACES_EXPORTER: none
volumes:
# One bare repository per Radicle repository: a staging copy between fetch and
# push, so a push only sends what changed. Disposable: rebuilt if deleted.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/cache:/cache
# The Gitea token the init step mints. Read-only here.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/secrets:/secrets:ro
healthcheck:
test: ["CMD-SHELL", "test -s /secrets/gitea-token && curl -fsS -o /dev/null http://radicle-api:8080/api/v1 && curl -fsS -o /dev/null http://gitea:3000/api/healthz"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
networks:
- pcs
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry:/var/lib/registry
depends_on:
- rad-actions-docker
# ═══ IPCR — the IPFS container registry ════════════════════════════════════════
# Verbatim from Apps/IPCR; only its data folder moves to $AppID/ipcr/.
@@ -379,18 +331,13 @@ services:
# ipcrd: TLS front door on 127.0.0.1:4767. Passes /ipfs/<cid> through to the stock nerdctl
# registry below, resolves /ipns/<name>:<tag> itself, and pins every image it serves.
ipcr-gateway:
image: ghcr.io/yundera/ipcr:1.1.0
image: ghcr.io/yundera/ipcr:1.2.0
container_name: ipcr-gateway
# Root for one reason: it writes its CA into the host's Docker trust store (the narrow
# bind below), which is root-owned. See rationale.md.
user: "0:0"
# Root needs no capabilities to write root-owned files it owns; drop them all.
cap_drop: [ALL]
# Forge only: one back, read-only. The import token below is written by bridge-credentials as
# $PUID, mode 0600, in a 0700 folder; a root with no capabilities is bound by those modes like
# anyone else (verified: "permission denied"). DAC_READ_SEARCH bypasses read and search checks
# only, never write, and only on what this container mounts. See rationale.md.
cap_add: [DAC_READ_SEARCH]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
command: ["gateway"]
@@ -405,21 +352,20 @@ services:
# IPCR_IMPORT_REPOS) is copied into IPFS and published as /ipns/<owner-repo>:<tag>;
# a tag is re-imported when it moves. Empty = off. IPCR_IMPORT_AUTH is user:token,
# a read-only (read:package) token is enough.
# Forge: watch this app's own Gitea, over gitea-internal (not through Caddy).
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://gitea:3000}
# Forge: watch the CI's staging registry, over ci-internal. Internal, so no credentials.
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://rad-actions-docker:5000}
IMPORT_AUTH: ${IPCR_IMPORT_AUTH:-}
# Forge: a read:package token the bridge-credentials step mints. Re-read every poll
# and preferred over IMPORT_AUTH, so it may appear after the gateway is up.
IMPORT_AUTH_FILE: /data/secrets/registry-auth
IMPORT_REPOS: ${IPCR_IMPORT_REPOS:-}
# Forge: every repository this node builds is published under one IPNS key, "forge", as
# /ipns/<forge key>/<repo>:<tag>. One name for the whole forge, which a DNSLink domain or an
# ENS name can point at: ipcr.localhost:4767/ipns/example.eth/<repo>:<tag>.
IMPORT_PUBLISHER: ${IPCR_IMPORT_PUBLISHER:-forge}
IMPORT_INTERVAL: 60s
STATE_DIR: /data/state
volumes:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/tls:/data/tls
# What the watcher already imported (tag → manifest digest).
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/data/state
# Forge only: the credential above. Read-only here.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/secrets:/data/secrets:ro
# The host's Docker trust store, so the CA above can be dropped into it: no daemon
# reload, no daemon.json edit. The parent is mounted because Compose cannot create a
# bind source whose name contains a colon (ipcr.localhost:4767). ipcrd writes exactly
@@ -439,8 +385,8 @@ services:
start_period: 10s
networks:
- ipcr-internal
# Forge only: reaches gitea:3000 for the import poller above.
- gitea-internal
# Forge only: reaches the staging registry for the import poller above.
- ci-internal
cpu_shares: 50
deploy:
resources:
@@ -450,7 +396,7 @@ services:
# ── Stock nerdctl IPFS registry ─────────────────────────────────────────────
# `nerdctl ipfs registry serve`, unmodified: translates the registry API into IPFS reads.
ipcr-registry:
image: ghcr.io/yundera/ipcr:1.1.0
image: ghcr.io/yundera/ipcr:1.2.0
container_name: ipcr-registry
# Root because nerdctl run as any other uid assumes rootless mode and refuses to start
# without a rootless containerd — which `ipfs registry serve` never uses. The service has
@@ -531,8 +477,8 @@ networks:
# From Apps/Radicle: egress only, for the node.
radicle-internal:
driver: bridge
# From Apps/Gitea: gitea ↔ runner, nothing else.
gitea-internal:
# CI: rad-actions ↔ its daemon (and staging registry) ↔ ipcr-gateway; egress for builds.
ci-internal:
driver: bridge
# From Apps/IPCR: gateway ↔ registry ↔ kubo, nothing else.
ipcr-internal:
@@ -554,77 +500,90 @@ x-casaos:
title:
en_us: IPCR Forge
tagline:
en_us: A decentralised forge in one install — Radicle, Gitea Actions and IPFS images.
fr_fr: Une forge décentralisée en une installation — Radicle, Gitea Actions et images IPFS.
en_us: A decentralised forge in one install — Radicle, GitHub-style CI and IPFS images.
fr_fr: Une forge décentralisée en une installation — Radicle, CI au format GitHub et images IPFS.
description:
en_us: |
IPCR Forge is the full path from code to running container, without a central service:
• **Radicle** holds your repositories on a peer-to-peer network. You push with `git push rad`.
• **Gitea Actions** builds them with GitHub-style workflows, triggered by a release tag.
• **Radicle CI** on this node builds them with GitHub-style workflows, triggered by a release tag.
• **IPCR** serves the resulting images from IPFS, so anyone can `docker pull` them by name.
A repository joins the pipeline by committing a workflow under `.gitea/workflows/`: there
is nothing to configure per project.
Radicle is the single source of truth: builds read the commit straight from this node, and
their results go back into the repository as Radicle jobs. There is no mirror to keep in sync.
Its own page explains how to publish a project and lists every published image with the
line to put in a compose file.
A repository joins the pipeline by committing a workflow under `.github/workflows/`, as on
GitHub: there is nothing to configure per project.
This app bundles the Radicle, Gitea, Radicle Gitea Bridge and IPCR apps. Install it instead
of them, not beside them.
Its own page explains how to publish a project, links every build and its log, and lists
every published image with the line to put in a compose file.
This app bundles the Radicle and IPCR apps. Install it instead of them, not beside them.
fr_fr: |
IPCR Forge couvre tout le chemin du code au conteneur en production, sans service central :
• **Radicle** garde vos dépôts sur un réseau pair-à-pair. Vous poussez avec `git push rad`.
• **Gitea Actions** les construit avec des workflows au format GitHub, déclenchés par un tag.
• **Radicle CI** sur ce nœud les construit avec des workflows au format GitHub, déclenchés par un tag.
• **IPCR** sert les images produites depuis IPFS : n'importe qui peut les `docker pull`.
Un dépôt rejoint la chaîne en ajoutant un workflow sous `.gitea/workflows/` : rien à
configurer par projet.
Radicle est l'unique source de vérité : les builds lisent le commit directement sur ce nœud,
et leurs résultats retournent dans le dépôt sous forme de jobs Radicle. Aucun miroir à
synchroniser.
Sa page explique comment publier un projet et liste chaque image publiée avec la ligne à
mettre dans un fichier compose.
Un dépôt rejoint la chaîne en ajoutant un workflow sous `.github/workflows/`, comme sur
GitHub : rien à configurer par projet.
Cette application regroupe les applications Radicle, Gitea, Radicle Gitea Bridge et IPCR.
Installez-la à leur place, pas à côté.
Sa page explique comment publier un projet, donne accès à chaque build et à son journal, et
liste chaque image publiée avec la ligne à mettre dans un fichier compose.
Cette application regroupe les applications Radicle et IPCR. Installez-la à leur place, pas à
côté.
tips:
before_install:
en_us: |
**Replaces four apps.** Uninstall Radicle, Gitea, Radicle Gitea Bridge and IPCR first:
this app uses the same addresses and ports.
**Replaces two apps.** Uninstall Radicle and IPCR first: this app uses the same addresses
and ports.
| Address | What |
| --- | --- |
| `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, and every published image |
| `https://ipcr-forge-$APP_DOMAIN` | This app: how to publish, every published image |
| `https://ipcr-forge-$APP_DOMAIN/ci/` | CI runs and their logs (public) |
| `https://radicle-$APP_DOMAIN` | Radicle explorer (public) |
| `https://radicle-api-$APP_DOMAIN` | Radicle API and read-only git (public) |
| `https://gitea-$APP_DOMAIN` | Gitea: builds, logs, staging registry (`gitea_admin`) |
| `ipcr.localhost:4767` | IPCR registry, this server's Docker only |
**Ports.** TCP 8776 (Radicle peers) and 4768 TCP+UDP (IPFS peers) are published.
**Images reach IPFS on their own.** IPCR follows this app's Gitea registry: about a
minute after a build pushes `gitea_admin/<repo>:<tag>`, it can be pulled as
**Who can build.** Only changes signed by one of a repository's delegates trigger a build:
release tags, the default branch and their patches. Builds run in their own Docker daemon.
**Images reach IPFS on their own.** A workflow pushes to `vars.IMAGE` (this node's
staging registry); about a minute later the image can be pulled as
`ipcr.localhost:4767/ipns/<name>:<tag>`, where `<name>` is the IPNS name (`k51…`) IPCR
creates for the repository. The app's page lists each one with the line to copy.
fr_fr: |
**Remplace quatre applications.** Désinstallez d'abord Radicle, Gitea, Radicle Gitea
Bridge et IPCR : cette application utilise les mêmes adresses et ports.
**Remplace deux applications.** Désinstallez d'abord Radicle et IPCR : cette application
utilise les mêmes adresses et ports.
| Adresse | Quoi |
| --- | --- |
| `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, et toutes les images publiées |
| `https://ipcr-forge-$APP_DOMAIN` | Cette application : publier, toutes les images publiées |
| `https://ipcr-forge-$APP_DOMAIN/ci/` | Builds et leurs journaux (public) |
| `https://radicle-$APP_DOMAIN` | Explorateur Radicle (public) |
| `https://radicle-api-$APP_DOMAIN` | API Radicle et git en lecture seule (public) |
| `https://gitea-$APP_DOMAIN` | Gitea : builds, journaux, registre de transit (`gitea_admin`) |
| `ipcr.localhost:4767` | Registre IPCR, pour le Docker de ce serveur uniquement |
**Ports.** TCP 8776 (pairs Radicle) et 4768 TCP+UDP (pairs IPFS) sont publiés.
**Les images arrivent sur IPFS d'elles-mêmes.** IPCR suit le registre Gitea de cette
application : environ une minute après qu'un build a poussé `gitea_admin/<dépôt>:<tag>`,
elle se tire avec `ipcr.localhost:4767/ipns/<nom>:<tag>`, où `<nom>` est le nom IPNS
(`k51…`) qu'IPCR crée pour le dépôt. La page de l'application les liste, prêts à copier.
**Qui peut lancer un build.** Seuls les changements signés par un délégué du dépôt
déclenchent un build : tags de version, branche par défaut et leurs patches. Les builds
tournent dans leur propre démon Docker.
**Les images arrivent sur IPFS d'elles-mêmes.** Un workflow pousse vers `vars.IMAGE`
(le registre de transit de ce nœud) ; environ une minute plus tard, l'image se tire avec
`ipcr.localhost:4767/ipns/<nom>:<tag>`, où `<nom>` est le nom IPNS (`k51…`) qu'IPCR crée
pour le dépôt. La page de l'application les liste, prêts à copier.
index: /
webui_port: 8080
store_app_id: ipcr-forge
@@ -646,29 +605,35 @@ x-compose-app:
- path: /DATA/AppData/$AppID/radicle/caddy
user: $PUID
group: $PGID
# ── Gitea ──
- path: /DATA/AppData/$AppID/gitea/data
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/gitea/config
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/gitea/runner
# ── CI ──
- path: /DATA/AppData/$AppID/ci/state
user: $PUID
group: $PGID
mode: "0700"
- path: /DATA/AppData/$AppID/gitea/runner-config
- path: /DATA/AppData/$AppID/ci/html/logs
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/gitea/runner-docker
- path: /DATA/AppData/$AppID/ci/cache
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/ci/home
user: $PUID
group: $PGID
mode: "0700"
- path: /DATA/AppData/$AppID/ci/work
user: $PUID
group: $PGID
mode: "0700"
# The daemon's socket: created by root, group $PGID (dockerd --group), mode 0660.
- path: /DATA/AppData/$AppID/ci/socket
user: $PUID
group: $PGID
mode: "0750"
- path: /DATA/AppData/$AppID/ci/docker
user: $PUID
group: $PGID
mode: "0710"
# ── Bridge ──
- path: /DATA/AppData/$AppID/bridge/cache
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/bridge/secrets
- path: /DATA/AppData/$AppID/ci/registry
user: $PUID
group: $PGID
mode: "0700"
@@ -680,11 +645,6 @@ x-compose-app:
- path: /DATA/AppData/$AppID/ipcr/init.d
user: $PUID
group: $PGID
# Written by bridge-credentials as $PUID, read by ipcr-gateway (root).
- path: /DATA/AppData/$AppID/ipcr/secrets
user: $PUID
group: $PGID
mode: "0700"
files:
# The landing page follows store updates: re-rendered on every up. None of the three holds a
# dollar-brace sequence, so rendering cannot fail on an unresolved variable.
@@ -709,17 +669,12 @@ x-compose-app:
from: radicle/caddy/Caddyfile.tmpl
ensure: always
mode: "0644"
- path: /DATA/AppData/$AppID/gitea/runner-config/act_runner-run
from: gitea/runner-config/act_runner-run
ensure: always
mode: "0755"
- path: /DATA/AppData/$AppID/ipcr/init.d/01-ipcr-node.sh
from: ipcr/init.d/01-ipcr-node.sh
ensure: always
mode: "0755"
init:
# In order: Radicle's identity before its node starts, then Gitea's admin, then the
# runner token and the bridge's credentials, which both need that admin to exist.
# Radicle's identity before its node starts. The CI needs nothing: it uses the node's.
- name: radicle-identity
image: dirk1980/radicle-seed-node@sha256:78ccee526ad4b4f4d7c79c2323b032a7209858a48e1dc9c077272ff1660050e2
user: $PUID:$PGID
@@ -727,53 +682,5 @@ x-compose-app:
- /DATA/AppData/$AppID/radicle/home:/radicle-home
command: ["sh", "-c", "yes '' | rad auth --alias radicle-${APP_PUBLIC_IP_DASH} --stdin"]
when: absent:/DATA/AppData/$AppID/radicle/home/keys/radicle
- name: gitea-admin
image: gitea/gitea:1.27.3-rootless
user: $PUID:$PGID
phase: post_up
volumes:
- /DATA/AppData/$AppID/gitea/data:/var/lib/gitea
- /DATA/AppData/$AppID/gitea/config:/etc/gitea
command:
- sh
- -c
- |
timeout 240 sh -c 'until gitea admin user list >/dev/null 2>&1; do sleep 3; done'
gitea admin user create --admin --username gitea_admin \
--password "$APP_DEFAULT_PASSWORD" --email "$APP_EMAIL" \
--must-change-password=false \
|| echo "admin account already present, leaving it alone"
when: always
- name: gitea-runner-token
image: gitea/gitea:1.27.3-rootless
user: $PUID:$PGID
phase: post_up
network: $APP_NET
volumes:
- /DATA/AppData/$AppID/gitea/data:/var/lib/gitea
- /DATA/AppData/$AppID/gitea/config:/etc/gitea
- /DATA/AppData/$AppID/gitea/runner:/runner
command:
- sh
- -c
- |
timeout 240 sh -c 'until gitea actions generate-runner-token > /runner/token.new 2>/dev/null; do sleep 3; done'
mv /runner/token.new /runner/token
chmod 600 /runner/token
when: absent:/DATA/AppData/$AppID/gitea/runner/.runner
- name: bridge-credentials
image: ghcr.io/worph/radicle-gitea-bridge:1.0.0
user: $PUID:$PGID
phase: post_up
network: $APP_NET
volumes:
- /DATA/AppData/$AppID/bridge/secrets:/secrets
- /DATA/AppData/$AppID/ipcr/secrets:/ipcr-secrets
# Also mints IPCR's read-only token for following Gitea's registry.
command:
- sh
- -c
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" IPCR_AUTH_FILE=/ipcr-secrets/registry-auth bridge-init
when: absent:/DATA/AppData/$AppID/bridge/secrets/gitea-token
webui-host: ipcr-forge-${domain}
webui-path: /