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

Hosting on a VPS

A VPS-hosted repository is the most flexible option: you control everything, no third-party limits, and the layout matches the spec's conventions exactly. The trade-off is operational overhead — you maintain the host, the TLS cert, scaling, and uptime.

This is appropriate if:

  • You don't want to depend on Cloudflare or GitHub.
  • You're already running a VPS and want to host the repo alongside other services.
  • You need cache headers, custom routing, or other webserver features that R2/Pages don't offer.

Don't pick this option if your alternative is "set up R2 in 15 minutes and forget about it" — R2 wins on cost and reliability for most workloads.

Layout

peipkg-manager produces a repository state at <state_dir>/repo/. Mirror that to a directory the webserver serves, e.g., /var/www/pkgs.example.org/. The directory structure is:

/var/www/pkgs.example.org/
├── repo.json
├── repo.json.sig
├── index/
│   ├── active.json
│   ├── active.json.sig
│   ├── archive.json
│   └── archive.json.sig
├── keys/
│   └── <fingerprint>.pub
└── p/
    └── <name>/<version>/<name>_<version>_<arch>.peipkg

This is peipkg-repo's default output layout, exactly. Per-package URLs in the index use the relative /p/<name>/<version>/<filename> convention — same host, no absolute URLs needed.

Server setup (Debian + nginx)

sudo apt-get install nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/pkgs.example.org
server {
    listen 80;
    listen [::]:80;
    server_name pkgs.example.org;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name pkgs.example.org;

    ssl_certificate     /etc/letsencrypt/live/pkgs.example.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pkgs.example.org/privkey.pem;

    root /var/www/pkgs.example.org;
    index repo.json;

    # Indexes change frequently — short cache.
    location ~ ^/(repo\.json|repo\.json\.sig|index/.*)$ {
        add_header Cache-Control "public, max-age=60";
    }

    # Package files are immutable — cache forever.
    location /p/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # Public keys rarely change.
    location /keys/ {
        add_header Cache-Control "public, max-age=3600";
    }

    # Disable directory listings.
    autoindex off;
}
sudo ln -s /etc/nginx/sites-available/pkgs.example.org \
          /etc/nginx/sites-enabled/
sudo nginx -t
sudo certbot --nginx -d pkgs.example.org

Sync from peipkg-manager to nginx

Set [upload].backend = "none" in peipkg-config.toml and run rsync from a wrapper:

#!/bin/sh
# /usr/local/bin/peipkg-sync-to-nginx
set -eu
STATE_DIR=/var/lib/peipkg-manager
WEB_ROOT=/var/www/pkgs.example.org

# rsync with --delete keeps the web root in sync with the repo state,
# including removals (rare — only happens if pre-release pruning kicks in).
rsync -a --delete --chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r \
  "$STATE_DIR/repo/" "$WEB_ROOT/"

Two options for triggering this:

  1. systemd path watcher monitoring <state_dir>/repo/ for changes, running the script on every modification.
  2. A small post-publish hook invoked by a wrapper around peipkg-manager. Set [upload].backend = "none" and run sync explicitly after peipkg-manager --once returns.

Option 1 is simpler if peipkg-manager is running as a daemon. Option 2 is cleaner if you're driving builds from cron.

systemd path watcher example:

# /etc/systemd/system/peipkg-sync.path
[Unit]
Description=Watch peipkg-manager state for changes
After=peipkg-manager.service

[Path]
PathChanged=/var/lib/peipkg-manager/repo/repo.json

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/peipkg-sync.service
[Unit]
Description=Sync peipkg-manager state to nginx

[Service]
Type=oneshot
ExecStart=/usr/local/bin/peipkg-sync-to-nginx
sudo systemctl enable --now peipkg-sync.path

Atomicity

rsync with --delete doesn't make multi-file updates atomic — there's a brief window during sync where some files are new and others are old. For a single host serving the same files this is rarely visible to consumers (rsync transfers in seconds), but it's the same race the R2 setup has.

If you really care about atomicity, you can rsync into a sibling directory and atomically swap with mv (which is atomic on the same filesystem):

rsync -a --delete "$STATE_DIR/repo/" /var/www/pkgs.example.org.new/
# atomic-swap the directory
sudo mv /var/www/pkgs.example.org{,.old}
sudo mv /var/www/pkgs.example.org.new{,.org}
sudo rm -rf /var/www/pkgs.example.org.old

This requires nginx's root to be a symlink (so the swap doesn't open files in the directory under it), or to disable open-file caching. v0 doesn't recommend this complexity unless you've measured the brief inconsistency window causing real problems.

Backups

The VPS host has the signing key, the published repo state, and (likely) recipes. Back all three up:

  • Signing key: see Signing keys. Offline, separate medium.
  • Published repo state: standard filesystem backup. The state can be regenerated from the recipes + .peipkg files via peipkg-repo publish --rebuild, but having it backed up directly is faster recovery.
  • Recipes: live in a git repo of their own, ideally hosted somewhere other than this VPS.

If the VPS is wiped, restore in this order: signing key (offline backup), recipes (re-clone), peipkg-manager state (filesystem backup or --rebuild from the package files), nginx (re-deploy from your config-management tool of choice).