These docs are under active development and cover the v0.20 Kobicha security model.
On this page
How-to 4 min read

Hosting on Cloudflare R2

Cloudflare R2 is the recommended hosting backend for the official Peios repository. R2's free egress means consumer downloads cost nothing in bandwidth, the API surface is S3-compatible (so rclone sync works directly), and Cloudflare's CDN is in front for free.

This page walks through bucket setup, custom domain, and rclone configuration. The end state is peipkg-manager's [upload].backend = "rclone" syncing to a bucket served at https://pkgs.example.org.

Cost

R2 pricing as of early 2026:

  • Storage: $0.015/GB-month, first 10 GB free.
  • Class B operations (reads): $0.36/million, first 10 million free.
  • Class A operations (writes): $4.50/million, first 1 million free.
  • Egress: free.

For the official Peios repo at v1 scale, "free tier" is sufficient. Even at Debian-class scale (~5,000 packages, ~500 GB stored, ~100M reads/month) the bill is ~$40/month. See the cost discussion in the project notes for ranges.

Create the bucket

  1. In the Cloudflare dashboard, navigate to R2 → "Create bucket".
  2. Name it pkgs.example.org (or whatever — the bucket name doesn't have to match the eventual public domain, but matching is convenient).
  3. Region: leave as auto unless you have a specific reason.
  4. Note the bucket name; rclone will use it.

Generate API credentials

Cloudflare R2 uses S3-compatible auth. From the R2 dashboard:

  1. "Manage API tokens" → "Create API token".
  2. Permissions: "Object Read & Write" scoped to just this bucket. Don't grant account-wide write.
  3. Save the access key ID and secret access key — you can't view the secret again afterwards.

Also note your account ID (visible in the R2 dashboard URL or the API token endpoint URL).

Configure rclone

sudo rclone config create r2 s3 \
  provider=Cloudflare \
  endpoint=https://<account-id>.r2.cloudflarestorage.com \
  access_key_id=<key> \
  secret_access_key=<secret> \
  region=auto

Test it:

sudo rclone lsd r2:pkgs.example.org   # should list nothing on a fresh bucket
sudo rclone copy /etc/hosts r2:pkgs.example.org/test.txt
sudo rclone ls r2:pkgs.example.org    # test.txt should appear
sudo rclone delete r2:pkgs.example.org/test.txt

If any of these fail, fix the credentials before going further. peipkg-manager will surface upload errors in its logs but won't try to debug them for you.

Set up the custom domain

Out of the box, R2 buckets are reachable at https://<account-id>.r2.cloudflarestorage.com/<bucket>/... — not a URL you want consumers to type. To serve at pkgs.example.org:

  1. In the R2 dashboard, open the bucket → Settings → "Public Access" → "Custom Domains" → "Connect Domain".
  2. Enter pkgs.example.org.
  3. Cloudflare creates the necessary DNS records if the domain is on a Cloudflare zone. If the domain is hosted elsewhere, follow Cloudflare's instructions for the CNAME plus TXT records.
  4. Wait for the certificate to provision (a few minutes).

After this, https://pkgs.example.org/ serves the bucket's contents directly. peipkg-manager's peipkg-repo init step will write repo.json at the bucket root, and pkgs.example.org/repo.json resolves to it.

peipkg-manager config

[upload]
backend = "rclone"
remote = "r2:pkgs.example.org"

peipkg-manager runs rclone sync <state_dir>/repo/ r2:pkgs.example.org after every successful publish. Bucket contents are mirrored to whatever's in the local repo state.

Cache headers

R2 uses object-level metadata for HTTP cache headers. The recommended split:

  • Indexes (repo.json, index/active.json, index/archive.json and their .sig siblings): short TTL so consumers see new content quickly. Cache-Control: public, max-age=60.
  • .peipkg files: long TTL — once published, hash is in the index, file is forever. Cache-Control: public, max-age=31536000, immutable.

rclone sync doesn't set headers automatically. Either:

  • Run a one-off rclone copyto with --header-upload after each publish to set the right Cache-Control. Wrap it in a small shell script, set [upload].backend = "none" in peipkg-manager's config, and have your wrapper script invoke peipkg-manager publish semantics manually.
  • Or set bucket-level defaults via Cloudflare's "Page Rules" and live with rclone sync being headerless.

For v0 the second option is simpler and adequate.

Atomicity of multi-file uploads

rclone sync uploads files in roughly arbitrary order. Between the moments active.json lands and the new .peipkg it references lands, a consumer fetching the index sees the new entry but 404s the package. The window is brief (seconds to minutes for typical packages on R2) and consumers re-attempting after a transient error will succeed.

If the brief window matters, the right approach is a two-pass upload: pass 1 uploads everything except index files, pass 2 uploads index files last. The split is mechanical but peipkg-manager's v0 doesn't implement it. Workaround: wrap peipkg-manager with --upload.backend = "none" and a custom sync script that does the two-pass upload yourself.

Operational notes

  • Set up Cloudflare alerts for bucket size and request count. R2's free tier is generous but you want to know when you're approaching the limits.
  • Keep the rclone credentials away from CI. They're long-lived write tokens to your repository; treat them like the signing key.
  • Don't enable public-bucket "list" mode. Consumers fetch known URLs; nobody needs to enumerate the bucket.