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/: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/: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-: 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). 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/, 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/ through to the stock nerdctl # registry below, resolves /ipns/: 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//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/:; # 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//:. One name for the whole forge, which a DNSLink domain or an # ENS name can point at: ipcr.localhost:4767/ipns/example.eth/:. 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/:`, where `` 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/:`, où `` 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: /