Files
ipcr-forge/Apps/IPCR-Forge/docker-compose.yml
T
Worph f82543f518 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.
2026-10-04 18:13:11 +00:00

687 lines
29 KiB
YAML

name: ipcr-forge
# IPCR Forge — the whole decentralised forge in one app:
#
# git push rad main v1.2.3 (developer)
# → radicle-node / radicle-api source of truth, peer-to-peer
# → 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 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.
#
# 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 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 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, 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
user: $PUID:$PGID
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
cpu_shares: 80
deploy:
resources:
limits:
memory: 32M
environment:
TZ: $TZ
tmpfs:
# nginx-unprivileged keeps its pid file and temp paths here.
- /tmp
volumes:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/site:/usr/share/nginx/html:ro
- ${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
timeout: 5s
retries: 3
start_period: 10s
expose:
- 8080
labels:
caddy_0: ipcr-forge-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 8080}}"
caddy_1: ipcr-forge-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 8080}}"
caddy_2: ipcr-forge-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 8080}}"
networks:
- pcs
# ═══ Radicle — the source of truth ═════════════════════════════════════════════
# Verbatim from Apps/Radicle; only its data folder moves to $AppID/radicle/.
# The web UI: the Radicle Explorer bundle served as static files by Caddy.
# It is a pure client-side app — every request it makes goes from the visitor's
# browser straight to the API below, which is why that one needs its own domain.
radicle:
image: dirk1980/radicle-explorer-container@sha256:0e31485c38a765e83a10b1a4bf71b12d4f9fec630881d171bba736a4a56a16ce
container_name: radicle
user: $PUID:$PGID
restart: unless-stopped
cpu_shares: 80
deploy:
resources:
limits:
memory: 128M
environment:
TZ: $TZ
# Caddy writes its autosaved config and its (unused here) certificate store
# under these two paths. The image points them at /config and /data, which are
# root-owned, so as $PUID the process cannot create them and refuses to start.
XDG_CONFIG_HOME: /tmp/caddy-config
XDG_DATA_HOME: /tmp/caddy-data
volumes:
# Replaces the image's own :80 Caddyfile — see the comments in that file.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8080/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
expose:
- 8080
labels:
caddy_0: radicle-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 8080}}"
caddy_1: radicle-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 8080}}"
caddy_2: radicle-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 8080}}"
networks:
- pcs
# radicle-httpd — the node's read-only JSON API, on its own domain.
#
# This domain is deliberately NOT behind the SSO gate: the explorer above is a
# static bundle running in the visitor's browser, so it is the browser that calls
# this API, cross-origin. The daemon serves public repositories only (private ones
# are filtered out upstream) and has no write endpoints at all. See rationale.md.
radicle-api:
image: dirk1980/radicle-httpd@sha256:5e95be30e4bde55b509a65fd56f8b825cafd0f5cd752c1a13e79aad75b958895
container_name: radicle-api
user: $PUID:$PGID
restart: unless-stopped
cpu_shares: 70
deploy:
resources:
limits:
memory: 512M
command: ["--listen", "0.0.0.0:8080"]
environment:
TZ: $TZ
volumes:
# Same Radicle home as the node: the daemon reads the repository storage
# directly and talks to the node over the control socket inside it.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/
healthcheck:
# curl, not wget: this image carries curl and no busybox.
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/api/v1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
expose:
- 8080
labels:
caddy_0: radicle-api-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 8080}}"
caddy_1: radicle-api-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 8080}}"
caddy_2: radicle-api-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 8080}}"
networks:
- pcs
# The node itself: speaks the Radicle peer-to-peer protocol (gossip, fetch,
# announce) on TCP 8776. No HTTP, so no Caddy route and no place on the shared
# network — it only needs egress, which the app-internal bridge provides.
radicle-node:
image: dirk1980/radicle-seed-node@sha256:78ccee526ad4b4f4d7c79c2323b032a7209858a48e1dc9c077272ff1660050e2
container_name: radicle-node
user: $PUID:$PGID
restart: unless-stopped
cpu_shares: 50
deploy:
resources:
limits:
memory: 1G
environment:
TZ: $TZ
volumes:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/
ports:
# Radicle peer-to-peer protocol. Other nodes dial in here to fetch the
# repositories this node seeds; without it the node still syncs outbound,
# but nobody can fetch from it. Hence the needs-public-ip tag.
- "8776:8776"
healthcheck:
# `rad node status` round-trips the node's control socket, which is the only
# liveness signal this container has — but it also renders the peer table, and
# that takes 3-8s on an idle node and longer while the node is syncing. Hence
# the unusually generous timeout: a 10s one fails during the first sync and
# flaps the container between healthy and unhealthy.
test: ["CMD", "rad", "node", "status"]
interval: 60s
timeout: 30s
retries: 3
start_period: 90s
networks:
- radicle-internal
# ═══ CI — Radicle CI broker + act ═════════════════════════════════════════════════
# 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: 20
deploy:
resources:
limits:
memory: 512M
environment:
TZ: $TZ
# 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:
# 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:
# Egress only: act fetches actions (actions/checkout, docker/*) from GitHub.
- ci-internal
# 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
# No certificates: with an explicit --host the entrypoint adds no TCP listener to protect.
DOCKER_TLS_CERTDIR: ""
volumes:
- ${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:
- ci-internal
# 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
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
network_mode: service:rad-actions-docker
cpu_shares: 20
deploy:
resources:
limits:
memory: 128M
environment:
TZ: $TZ
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:
- ${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/.
# ── The registry Docker talks to ────────────────────────────────────────────
# 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.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]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
command: ["gateway"]
environment:
TZ: $TZ
TLS_DIR: /data/tls
# Docker trusts certs.d/<host:port>/ca.crt and re-reads it on every pull.
CA_EXPORT: /certs.d/ipcr.localhost:4767/ca.crt
# Pin every image this host pulls, so the node keeps — and re-shares — what it runs.
AUTO_PIN: "true"
# Optional: follow a registry. Every tag of every repository it lists (or of
# 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 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:-}
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
# 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
# one file here. See rationale.md.
- /etc/docker/certs.d:/certs.d
ports:
# Loopback only: the client is the host's Docker daemon, which resolves ipcr.localhost
# to 127.0.0.1 (RFC 6761). Not reachable from the network, and not a web UI.
- "127.0.0.1:4767:4767"
depends_on:
- ipcr-registry
healthcheck:
test: ["CMD", "ipcrd", "health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- ipcr-internal
# Forge only: reaches the staging registry for the import poller above.
- ci-internal
cpu_shares: 50
deploy:
resources:
limits:
memory: 128M
# ── Stock nerdctl IPFS registry ─────────────────────────────────────────────
# `nerdctl ipfs registry serve`, unmodified: translates the registry API into IPFS reads.
ipcr-registry:
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
# no volumes and no host access; capabilities are all dropped. See rationale.md.
user: "0:0"
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
command: ["registry"]
environment:
TZ: $TZ
expose:
- 5050
depends_on:
- ipcr-kubo
healthcheck:
test: ["CMD", "ipcrd", "health", "http://127.0.0.1:5050/v2/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- ipcr-internal
cpu_shares: 50
deploy:
resources:
limits:
memory: 256M
# ── Kubo, the IPFS node ─────────────────────────────────────────────────────
ipcr-kubo:
image: ipfs/kubo:v0.43.1
container_name: ipcr-kubo
user: $PUID:$PGID
restart: unless-stopped
# --enable-gc: blocks nobody pinned are collected; pinned images are never touched.
command: ["daemon", "--migrate=true", "--agent-version-suffix=docker", "--enable-gc"]
environment:
TZ: $TZ
# Applied at `ipfs init` only: DHT client mode, small connection manager. Tuned for an
# always-on small box (see the Kubo store app for measurements).
IPFS_PROFILE: lowpower
GOMEMLIMIT: 768MiB
# Read by seed/init.d/01-ipcr-node.sh on every start.
IPCR_SWARM_PORT: "4768"
IPFS_STORAGE_MAX: 20GB
volumes:
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/kubo/:/data/ipfs
# Kubo's entrypoint runs every *.sh here before starting the daemon.
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/init.d/:/container-init.d:ro
expose:
# RPC API: no auth, so it never leaves the app's own network.
- 5001
ports:
# libp2p swarm — the peer protocol, not HTTP, so Caddy cannot carry it. Without it the
# node still pulls, but nobody can fetch the images this host pins. 4001 is left to the
# Kubo store app so both can be installed side by side.
- "4768:4768/tcp"
- "4768:4768/udp"
healthcheck:
test: ["CMD-SHELL", "ipfs --api=/ip4/127.0.0.1/tcp/5001 id > /dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
networks:
- ipcr-internal
cpu_shares: 50
deploy:
resources:
limits:
memory: 1024M
networks:
pcs:
name: ${APP_NET:-pcs}
external: true
# From Apps/Radicle: egress only, for the node.
radicle-internal:
driver: bridge
# 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:
driver: bridge
x-casaos:
architectures:
- amd64
main: ipcr-forge
author: Yundera
developer: Yundera
category: Developer
tags: ["developer", "git", "ci", "needs-public-ip"]
icon: icon.png
screenshot_link:
- screenshot-1.png
- screenshot-2.png
- screenshot-3.png
title:
en_us: IPCR Forge
tagline:
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`.
• **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.
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.
A repository joins the pipeline by committing a workflow under `.github/workflows/`, as on
GitHub: there is nothing to configure per project.
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`.
• **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`.
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.
Un dépôt rejoint la chaîne en ajoutant un workflow sous `.github/workflows/`, comme sur
GitHub : rien à configurer par projet.
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 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, 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) |
| `ipcr.localhost:4767` | IPCR registry, this server's Docker only |
**Ports.** TCP 8776 (Radicle peers) and 4768 TCP+UDP (IPFS peers) are published.
**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 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, 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) |
| `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.
**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
x-compose-app:
schema_version: 2
folders:
# ── Landing page ──
- path: /DATA/AppData/$AppID/site
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/web
user: $PUID
group: $PGID
# ── Radicle ──
- path: /DATA/AppData/$AppID/radicle/home
user: $PUID
group: $PGID
- path: /DATA/AppData/$AppID/radicle/caddy
user: $PUID
group: $PGID
# ── CI ──
- path: /DATA/AppData/$AppID/ci/state
user: $PUID
group: $PGID
mode: "0700"
- path: /DATA/AppData/$AppID/ci/html/logs
user: $PUID
group: $PGID
- 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"
- path: /DATA/AppData/$AppID/ci/registry
user: $PUID
group: $PGID
mode: "0700"
# ── IPCR ──
- path: /DATA/AppData/$AppID/ipcr/kubo
user: $PUID
group: $PGID
mode: "0700"
- path: /DATA/AppData/$AppID/ipcr/init.d
user: $PUID
group: $PGID
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.
- path: /DATA/AppData/$AppID/site/index.html
from: site/index.html
ensure: always
mode: "0644"
- path: /DATA/AppData/$AppID/site/icon.png
from: site/icon.png
ensure: always
mode: "0644"
- path: /DATA/AppData/$AppID/web/default.conf
from: web/default.conf
ensure: always
mode: "0644"
# Each entry and its reason are in the standalone listing it comes from.
- path: /DATA/AppData/$AppID/radicle/home/config.json
from: radicle/home/config.json.tmpl
ensure: always
mode: "0644"
- path: /DATA/AppData/$AppID/radicle/caddy/Caddyfile
from: radicle/caddy/Caddyfile.tmpl
ensure: always
mode: "0644"
- path: /DATA/AppData/$AppID/ipcr/init.d/01-ipcr-node.sh
from: ipcr/init.d/01-ipcr-node.sh
ensure: always
mode: "0755"
init:
# 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
volumes:
- /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
webui-host: ipcr-forge-${domain}
webui-path: /