Downstream of registry.gitlab.com/fdroid/docker-executable-fdroidserver:master,
patched for publishing F-Droid repos over IPFS:
- install Debian ipfs-cid so fdroidserver populates ipfsCIDv1 on every
package file (APKs, icons, screenshots), which fdroidclient with an
IPFS-gateway mirror routes via CID natively
- rehash.py wrapper that adds ipfsCIDv1 to entry["index"], renames the
index to a content-addressed filename, and re-signs entry.jar after
`fdroid update` — closes the cache-busting gap on the index file itself
- entrypoint.sh chains rehash automatically after any `fdroid update`,
so existing fdroid-via-docker wrappers need no changes
See SPEC.md for the rationale and Change 2's algorithm; README.md for
end-user usage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.9 KiB
fdroidserver-ipfs
A drop-in replacement for registry.gitlab.com/fdroid/docker-executable-fdroidserver:master
tailored for publishing F-Droid repositories over IPFS.
The upstream image works for HTTP-served repos but has two gaps that break the IPFS workflow. This image patches both.
Problem
Pinning an F-Droid repo to IPFS and serving it through public HTTP gateways (ipfs.io, dweb.link, cloudflare-ipfs.com, …) exposes two upstream limitations:
- The
ipfs-cidPython module isn't installed, so any fdroidserver code path that computes a CIDv1 for repo artifacts fails on the stock image. index-v2.jsonis published under a stable filename. Public IPFS gateways cache aggressively by URL path, so clients keep getting the previous index for hours after a republish even though the underlying CID has changed.
Scope
This repo produces one OCI image — call it fdroidserver-ipfs — built FROM
the upstream docker-executable-fdroidserver:master and applying the minimum
set of changes needed for the IPFS workflow. Everything not listed below is
intentionally inherited unchanged from upstream.
Change 1 — install ipfs-cid
Install the Debian ipfs-cid package (available in Trixie, which the
upstream image is based on):
RUN apt-get update \
&& apt-get install -y --no-install-recommends ipfs-cid \
&& rm -rf /var/lib/apt/lists/*
Note: the PyPI package of the same name is a Python library only and
ships no binary, so pip install ipfs-cid silently leaves CID generation
disabled. fdroidserver auto-detects the binary on PATH
(fdroidserver/common.py::shutil.which('ipfs_cid')) and, when present,
calls calculate_IPFS_cid() for every published file — APKs, source
tarballs, icons, screenshots, etc. — writing the resulting CIDv1 into
each file's ipfsCIDv1 JSON field (fdroidserver/update.py:1661-1663,
:1717-1719). On the stock upstream image the binary is missing, so the
field is silently omitted and the IPFS-routing code path on the client is
never exercised.
Why this matters for the client. fdroidclient has native IPFS support:
org.fdroid.download.MirrorChooser (in libs/download/) reads the
ipfsCIDv1 field on each file and, when a configured mirror is flagged
isIpfsGateway, fetches the file by CID via that gateway instead of by
URL. So once ipfs-cid is installed, every per-package asset (APK, icon,
…) is automatically reachable via any IPFS gateway by any client with one
configured — no patching required.
Change 2 — add ipfsCIDv1 to the index entry + cache-busted filename
Change 1 fixes per-package files but does not fix the index itself.
The index block inside entry.json is built by
fdroidserver/common.py::file_entry(), which only emits name / sha256
/ size and never adds ipfsCIDv1 — even when one could be computed. So
fdroidclient has no CID for index-v2.json to feed into MirrorChooser,
and falls back to the stable HTTP path → public IPFS gateways serve a
stale cached copy for hours after a republish.
The post-process wrapper closes both halves of this gap in one pass:
- Computes the IPFS CIDv1 of
repo/index-v2.json(usingcalculate_IPFS_cidfromfdroidserver.common, which is the same code path fdroidserver itself uses for APKs). - Renames
repo/index-v2.json→repo/index-v2-<h>.json, where<h>is the last 12 hex characters of the SHA-256 of the file's content. This image is IPFS-only — no stable copy is kept alongside. - Rewrites
repo/entry.jsonso thatentry["index"]:- Updates
nameto/index-v2-<h>.json - Adds
ipfsCIDv1with the value computed in step 1 - Leaves
sha256andsizeunchanged (the bytes didn't move)
- Updates
- Re-signs
entry.jarby callingfdroidserver.signindex.sign_index(repodir, "entry.json")— reuses the existingconfig.ymlkeystore +FDROID_KEY_STORE_PASS/FDROID_KEY_PASSenv vars; no extra config surface. - If GPG signing is enabled in
config.yml, regeneratesentry.json.asc. - Applies the same CID/rename/rewrite to any
diff/*.jsonfiles published alongside (each is referenced by its ownnamefield in entry.json).
Two-layer cache strategy, on purpose:
- IPFS-aware clients (any user with an IPFS gateway configured as a
mirror) follow the
ipfsCIDv1and fetch the index by CID. Always fresh, never gateway-cached by URL. - Plain-HTTP clients still hit a new URL on every publish thanks to
the
<h>suffix → public gateway path caches naturally miss → fresh index without waiting for TTLs.
entry.jar itself stays at the fixed /entry.jar URL — gateways will
still cache it, but it's tiny and clients fetch it first to discover both
the current index filename and its CID, which are the cache-busted
references we actually care about.
entry.json does not currently carry an ipfsCIDv1 for the index entry,
but fdroidclient's parser is configured with ignoreUnknownKeys = true
(libs/index/.../IndexParser.kt) and EntryFileV2 already declares the
field (libs/index/.../IndexV2.kt), so adding it is both backwards-safe
and natively consumed by current client versions.
Delivery: a small Python script (fdroidserver-ipfs-rehash, or similar)
installed into the image's PATH that imports fdroidserver.signindex /
fdroidserver.common directly. Either invoked manually after fdroid update,
or wired into a container entrypoint that runs both. Entrypoint design is
deferred until we know how the image gets driven in production.
Out of scope
- Changes to
index-v1.*or the legacyindex.jarflow. - Pinning to IPFS, gateway selection, DNSLink updates — handled by whatever drives the container, not by the image itself.
- Anything unrelated to the two issues above.
Non-goals for this spec
This document captures what the image needs to do and why. The concrete Dockerfile layering, the exact rehash/re-sign script, and the workflow YAML are implementation details and land in follow-up changes.