No description
  • Go 93.2%
  • Vue 3.1%
  • TypeScript 2%
  • Shell 0.8%
  • JavaScript 0.5%
  • Other 0.4%
Find a file
AlexRus1234 373d8a49c1
All checks were successful
Зеркалирование Khrazhevnik / sync-mirrors (push) Successful in 12s
release: v1.0.3
2026-09-13 20:08:22 +03:00
.forgejo/workflows integration+ci: ranged proxy e2e, mandatory fedora leg with range proof 2026-09-13 18:02:59 +03:00
bench bench: k6 load scenarios + CPU/RAM samplers, perf reference in README and func docs 2026-09-12 07:14:38 +03:00
cmd/khrazhevnik api: cache stats reset endpoint with audit 2026-09-08 20:00:58 +03:00
deploy ci: per-registry OCI push flags, image namespace alexrus1234 2026-09-10 21:24:24 +03:00
docs docs: ranged serving wave (Range-206) 2026-09-13 20:01:00 +03:00
internal mariadb: retry transient autoinc race 1467 on bootstrap 2026-09-13 19:20:51 +03:00
migrations db: cache_stats snapshot table and StatsStore port 2026-09-08 19:26:43 +03:00
scrn docs: image registry pull list (5 mirrors) + UI screenshots in README 2026-09-10 21:57:44 +03:00
test integration+ci: ranged proxy e2e, mandatory fedora leg with range proof 2026-09-13 18:02:59 +03:00
web ui: fix dashboard txn panel height, definite section height for internal scroll 2026-09-12 16:35:45 +03:00
.dockerignore Containerfile: node stage for web 2026-08-26 22:27:38 +03:00
.gitattributes chore: enforce lf line endings via gitattributes 2026-08-22 17:24:33 +03:00
.gitignore bench: k6 load scenarios + CPU/RAM samplers, perf reference in README and func docs 2026-09-12 07:14:38 +03:00
.golangci.yaml test: shared contract suites 2026-08-26 05:29:06 +03:00
CHANGELOG.EN.md release: v1.0.3 2026-09-13 20:08:22 +03:00
CHANGELOG.md release: v1.0.3 2026-09-13 20:08:22 +03:00
CHANGELOG.old.md docs: changelog to Keep a Changelog format, archive pre-1.0.0 history 2026-09-12 08:14:20 +03:00
go.mod db: postgres and mariadb adapters, migrations 2026-08-26 05:30:07 +03:00
go.sum db: postgres and mariadb adapters, migrations 2026-08-26 05:30:07 +03:00
LICENSE init: scaffold, tooling, docs 2026-08-17 05:27:30 +03:00
Makefile ci: dogfood — dnf build-test/oci через rpm-ремоуты Хражевника; Containerfile: GOPROXY/npm через Nora, apk через инстанс, step-ca root качает builder (не коммитится) 2026-09-06 17:07:12 +03:00
README.en.md bench: k6 load scenarios + CPU/RAM samplers, perf reference in README and func docs 2026-09-12 07:14:38 +03:00
README.md bench: k6 load scenarios + CPU/RAM samplers, perf reference in README and func docs 2026-09-12 07:14:38 +03:00
SECURITY.md auth: admin-scoped tokens require live admin owner 2026-09-04 19:18:51 +03:00

English | Русский |

Khrazhevnik

Caching proxy, mirror, and hosting for Linux package repositories

The name is a blend of the Russian verbs «Хранить» (to store) and «Отражать» (to mirror): the roots «Хран» and «раж» are fused on the shared syllable «-ра-».

Khrazhevnik is a self-contained server for caching and mirroring Linux package repositories. The Go binary and the built-in web admin UI (Vue 3) are combined into a single executable; the object store is a local directory or any S3-compatible storage, the catalog is SQLite, PostgreSQL, or MariaDB, selected via TOML configuration without rebuilding. The primary distribution is an OCI container from scratch (non-root, read-only rootfs) run under a rootless podman quadlet.

License: AGPL-3.0 Go Vue Vite Platform CI

Transparency for clients. Upstream metadata is served byte-for-byte — not a single byte is rewritten: signatures and checksums remain valid, and client keyrings do not change:

# /etc/apt/sources.list.d/khrazhevnik.list
deb http://<Khrazhevnik>:29202/apt/debian stable main

Khrazhevnik only adds a cache on top (packages — permanently, indexes — revalidation by ETag/Last-Modified), background mirrors, and personal signed user repositories.

Screenshots

The web admin UI (/ui/).

Dashboard — cache statistics and live tasks:

Dashboard — cache stats and tasks

Repositories — list, upload, reindex:

Repositories — upload and reindex


Contents

Extended documentation on deployment, configuration, the REST API, ecosystem clients, and personal repositories is available in docs/func/EN/. This file provides a system overview and initial setup instructions.

The project was developed according to a predefined architecture; an AI assistant was used while preparing the source code.1


Features

Caching proxy

  • Transparent pull-through for the supported package managers: the first request for an object goes upstream, the response is stored in the cache, and subsequent requests are served locally (X-Cache: HIT)
  • Object classification by ecosystem rules: packages and content-addressed objects are immutable (kept permanently); indexes (dists/, repodata/, APKINDEX, {repo}.db, narinfo) are mutable with conditional revalidation by ETag/Last-Modified and a TTL
  • stale-if-error (optional) and negative caching of 404/5xx in memory: an upstream failure is not passed through to clients
  • Singleflight per key: concurrent requests for the same object do not hit the upstream; a cacheable object size limit (cache.max_object_size)

Mirror

  • A complete local copy of an upstream repository (mode = mirror): background sync with a worker pool, retries, and a bandwidth limit (token bucket)
  • Idempotent resume: every run compares the upstream listing with the objects already present and downloads the missing ones; progress is kept in the catalog (files=N;bytes=M), task state survives a restart
  • Per-remote scheduler: sync_interval plus a random jitter; manual runs via the API or the web admin UI (409 on duplicate, 429 on the worker limit)
  • Include filters: apt — dists and components (stable, stable/main); pacman — repo/arch; apk — architectures

Personal repositories

  • Package uploads by users: streamed directly into the store with a mandatory Content-Length and on-the-fly byte verification (an abort cleans up tmp/)
  • RBAC: admins — everywhere; the repository owner and a scoped token repo:<id>:write — upload/delete/reindex/list; reading is public without authentication
  • Per-repository quotas bytes/files, a single-object limit, overwriting an existing key returns 409 (force — admin only, audited)
  • Index generation by the background reindex task (apt: Packages + .gz, by-hash/SHA256/*, Release)
  • Signing with the instance key (OpenPGP ed25519): InRelease (cleartext) and Release.gpg (detached); for nix — narinfo re-signing (only the Sig field is replaced, the rest is byte-exact); when the signer is unavailable, repositories keep working without signatures
  • The public key is served at GET /repo/<name>/key.asc on the public port

Ecosystems

Ecosystem Clients URL prefix Mirror
apt Debian, Ubuntu /apt/ yes (+ include filter)
rpm-md dnf, Zypper /rpm/ yes
pacman Arch Linux /pacman/ yes (+ include filter)
apk Alpine Linux /apk/ yes (+ include filter)
nix binary cache /nix/ no — pull-through on use only

Metadata parsers of all ecosystems (deb822, repomd/primary XML, tar.zst {repo}.db, APKINDEX.tar.gz, narinfo) are streaming, with a decompression limit and fuzzing since the first adapter.

Interfaces and security

  • Two listeners: the public :29202 (package serving without authentication + /healthz), the admin :30202 (/api/v1, /metrics, SPA /ui). The default for :30202 is all interfaces (startup writes a warning to the log); loopback is provided by the quadlet (PublishPort=127.0.0.1:30202:30202)
  • Web admin UI (Vue 3, Russian/English): a dashboard with cache statistics and live tasks, remotes, repositories with upload/reindex, users and scoped tokens, the audit log, public keys with ready-made client lines (signed-by=…, rpm --import, pacman-key --add, …)
  • Authentication: JWT sessions (bcrypt, rate limit 10/min on login, revocation on logout) and scoped API tokens (only sha256 is stored, shown once); role and token_version are checked against the DB on every request
  • Audit of all mutations (actor/action/object/result/detail) with keyset pagination
  • Prometheus metrics (/metrics, behind auth): hits/misses/stale/ negative per ecosystem, bytes from upstream/to clients, histograms of durations and object sizes
  • A single path-traversal check point for all request paths

A detailed description is provided in docs/func/EN/.


Performance

Measured on a live homelab instance (S3 + PostgreSQL on a separate NAS, VM 4 vCPU / 8 GB; methodology and full tables — in docs/func/EN/benchmarks.md, scenarios — in bench/):

Metric Value
Instance CPU at saturation ≤1.6% (of 4 vCPU) at ~340 req/s and ~300 MB/s delivery
RAM 26 MB idle; peak 981 MB (memory.peak cgroup) under maximum load
Reliability 0 5xx responses over 269 GB delivered (~150k requests, all runs)
apt clients 50 parallel machines: a full apt update + install in ~5 s/machine; 200 machines saturate the network path (~2.4 Gbit/s)

The profile's bottleneck is the S3 read path (RustFS: 8591% CPU at saturation, sublinear stream scaling), not Khrazhevnik itself: the PostgreSQL catalog sustains hundreds of thousands of SELECTs at ≤1.8% CPU.


Quick start

Requirements

Component Version Purpose
podman 4+ Rootless container, quadlet
systemd --user Quadlet generator
OpenSSL JWT secret generation
Go 1.26+ Building from source (optional)
Node.js 22+ Building the Web UI (optional)

Installation (container)

The image is published to mirror registries (identical content):

docker pull git.alexrus1234.ru/alexrus1234/khrazhevnik:latest   # primary
docker pull ghcr.io/alexrus1234/khrazhevnik:latest
docker pull docker.io/alexrus1234/khrazhevnik:latest

The full registry list (codeberg.org, quay.io) — in docs/func/EN/deploy.md.

# 1. Quadlet — into the user systemd generator path.
mkdir -p ~/.config/containers/systemd
cp deploy/quadlet/khrazhevnik.container ~/.config/containers/systemd/

# 2. The JWT secret — a Podman Secret (not exposed in env or `systemctl show`).
podman secret create jwt-secret "$(openssl rand -hex 32)"

# 3. Data directory: the container runs under UID 65534 (nobody).
sudo mkdir -p /var/lib/khrazhevnik
sudo chown 65534:65534 /var/lib/khrazhevnik

# 4. Start.
systemctl --user daemon-reload
systemctl --user start khrazhevnik.service
curl -s http://localhost:29202/healthz   # → ok

Bootstrap and the first remote

When the users table is empty, the web admin UI at http://127.0.0.1:30202/ui/ offers to create the first administrator; upstreams and personal repositories are then configured from the UI. The same steps via the API:

# The first administrator (once, while the users table is empty).
curl -s -X POST http://127.0.0.1:30202/api/v1/setup \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<password>"}'

# Login → JWT; register the Debian caching proxy.
TOKEN=$(curl -s -X POST http://127.0.0.1:30202/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"<password>"}' | jq -r .token)

curl -s -X POST http://127.0.0.1:30202/api/v1/remotes \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"debian","ecosystem":"apt","base_url":"https://deb.debian.org/debian","mode":"proxy","enabled":true}'

A headless alternative for init scripts (without starting the server):

podman exec khrazhevnik /khrazhevnik -add-remote apt/debian=https://deb.debian.org/debian

Client setup

On any Debian/Ubuntu machine, point the client at Khrazhevnik instead of the upstream:

echo 'deb http://<Khrazhevnik>:29202/apt/debian stable main' \
  > /etc/apt/sources.list.d/khrazhevnik.list
apt-get update && apt-get install hello

Signatures and checksums remain valid: upstream metadata is served byte-for-byte. Cache check: a repeated request for dists/…/Packages.gz returns X-Cache: HIT. dnf/zypper, pacman, apk, and nix clients — in docs/func/EN/ecosystems/.

Building from source

# The Web UI and the server part, in the order used by CI:
make web-build
make build        # the executable bin/khrazhevnik

No CGO is required (SQLite — modernc.org/sqlite), the executable is static. A build for the target environment (as in CI):

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
  go build -trimpath -ldflags="-s -w \
  -X main.Version=1.0.0" \
  -o khrazhevnik ./cmd/khrazhevnik

For package main the linker accepts only -X main.Version=…; the full import path (khrazhevnik/cmd/khrazhevnik.Version) is silently not applied — the version stays dev (as in CI and the Containerfile).

Release binaries (khrazhevnik-<version>-linux-amd64 + .sha256) and the OCI image are published manually from CI to the Packages and Releases of the connected registries. For development — build from source.

A step-by-step guide is in docs/func/EN/quickstart.md.


Configuration

khrazhevnik.toml (the -config flag; an empty value means defaults + env only):

[server]
public_listen = ":29202"        # package serving + /healthz
admin_listen  = ":30202"        # /api/v1, /metrics, /ui

[storage]
driver = "fs"                   # fs | s3
[storage.fs]
path = "/var/lib/khrazhevnik/store"
[storage.s3]
endpoint = "" ; region = "" ; bucket = "" ; path_style = true

[database]
driver = "sqlite"               # sqlite | postgres | mariadb
dsn    = "/var/lib/khrazhevnik/khrazhevnik.db"

[auth]
jwt_secret   = ""               # NOT in a production file: env/file (required)
session_ttl  = "8h"
setup_token  = ""               # optional protection for the first-admin bootstrap

[cache]
stale_if_error  = true
max_object_size = "20GiB"
negative_ttl_404 = "5m" ; negative_ttl_5xx = "30s"

[mirror]
workers = 4 ; interval_jitter = "10m"

[publish]
max_object_size    = "1GiB"     # a single uploadable object limit
default_quota_bytes = "5GiB"    # new repository quota (0 = no limit)
default_quota_files = 10000

[signing]
keys_dir = "/var/lib/khrazhevnik/keys"   # instance ed25519 key
# passphrase = ""              # optional: env KHRZ_SIGNING__PASSPHRASE

[metrics]
enabled = true

[ecosystem.apt]                 # apt | rpm-md | pacman | apk | nix;
enabled = true                  # the section is only needed for overrides

Application layers: defaults → TOML → env. The env prefix is KHRZ_, path segments are joined with __ in upper case: KHRZ_AUTH__JWT_SECRET, KHRZ_STORAGE__S3__SECRET_ACCESS_KEY, KHRZ_ECOSYSTEM__RPM_MD__ENABLED. Values of the form file:///run/secrets/x (env or TOML) are read from a file — quadlet Secret support. Validation is fail-fast with a list of all problems at once. The full scheme is in docs/func/EN/config.md.


Deployment and security

The primary distribution is an OCI image from scratch: the binary + a CA bundle, USER 65534:65534, a read-only rootfs; writable is only the volume /var/lib/khrazhevnik (SQLite, the fs-store, signing keys). The binary is PID 1, there are no subprocesses (OpenPGP is in-process), so no zombie reaper is needed; graceful shutdown: SIGTERM → HTTP 5s → background tasks 30s.

Port Access Purpose
29202 public package serving (/<eco>/<remote>/<path>, /repo/<name>/*), /healthz
30202 all interfaces (default; a warning at startup — loopback via the quadlet's PublishPort) admin API /api/v1, /metrics, web admin UI /ui

Rootless mode: both ports are ≥1024; publishing on 80/443 is done via a reverse proxy on the host (Caddy/Traefik/nginx — docs/func/EN/reverse-proxy.md). AutoUpdate=registry in the quadlet enables image auto-updates via podman auto-update. The default (fs + sqlite) is for a homelab; for production — S3 + postgres/mariadb (an example quadlet — deploy/quadlet/khrazhevnik-s3.container, recommendations — in docs/func/EN/storage-db.md).

An alternative to the container is a bare binary under systemd or any process supervisor; the JWT secret is passed via the env variable KHRZ_AUTH__JWT_SECRET=file:///run/secrets/jwt-secret.

The full deployment guide is in docs/func/EN/deploy.md.


API and web admin UI

Interface Summary Details
REST API /api/v1: setup/login, remotes, repos (+objects/perms/reindex), users, api-tokens, tasks, cache/stats, audit; errors — {"error":"snake_case"} docs/func/EN/api.md
Web admin UI /ui/ on the admin port; RU/EN; embedded into the binary (go:embed) docs/func/EN/ui.md
Metrics /metrics (Prometheus exposition, behind auth) docs/func/EN/api.md
Personal repositories upload by token, reindex, signing, client setup docs/func/EN/personal-repos.md

Admin API authentication: Authorization: Bearer <jwt> (browser) or a scoped API token Bearer khz_... (CI scripts: admin, repo:<id>:write).


Project structure

.
├── cmd/khrazhevnik/          # Entry point: main.go (~40 lines), wire.go —
│                             # the only wiring (compile-time registry)
├── internal/
│   ├── core/
│   │   ├── port/             # Contracts: Storage, Ecosystem, Catalog*, Signer, Clock, Rand, HTTP
│   │   ├── domain/           # Models + typed errors (stdlib only)
│   │   ├── config/           # Layers: defaults → TOML → env KHRZ_* (+file:// secrets)
│   │   ├── dbtalk/           # Catalog SQL dialect shim (placeholder/upsert)
│   │   ├── engine/           # Usecase logic: cache, mirror, publish, auth
│   │   ├── registry/         # Compile-time module registry
│   │   └── web/              # chi routers, middleware, TaskRegistry, embedded SPA
│   ├── mod/                  # Modules (registered in init()): ecosystem/
│   │                         # {apt, rpmmmd, pacman, apk, nix}, storage/{fs, s3},
│   │                         # db/{sqlite, postgres, mariadb}, sign/{openpgp, ed25519}
│   ├── testutil/             # Shared test doubles (FixedClock, FakeStorage, …)
│   └── contract/             # Catalog/storage contract suites (integration)
├── migrations/<driver>/      # Embedded goose migrations (per-DBMS directory)
├── web/                      # Vue 3 + Vite + TypeScript SPA (bundle → core/web/assets)
├── deploy/                   # Containerfile (node → golang → scratch) + quadlet/
├── bench/                    # k6 load scenarios + CPU/RAM samplers (methodology — docs/func/EN/benchmarks.md)
├── test/                     # integration/ (in-process + binary-smoke), smoke/
├── docs/                     # ARCHITECTURE/SPECIFICATION/TESTING/ROADMAP/HISTORY; func/EN/
└── .forgejo/workflows/       # CI: build, tests, e2e, OCI

The business logic (core/domain, core/engine) does not import os, syscall, net, net/http, or concrete modules — all I/O goes through the core/port interfaces; the core and the modules do not know about each other, wiring happens only in wire.go. The rules are enforced by the depguard linter.


Technology stack

Backend: Go 1.26 · chi v5 · pelletier/go-toml/v2 · modernc.org/sqlite (no CGO) · jackc/pgx/v5 · go-sql-driver/mysql · pressly/goose/v3 · golang-jwt/jwt/v5 · golang.org/x/crypto (bcrypt) · golang.org/x/sync (singleflight) · golang.org/x/time (rate) · ProtonMail/go-crypto (OpenPGP) · minio/minio-go/v7 · klauspost/compress (zstd) · prometheus/client_golang · log/slog · go:embed.

Frontend: Vue 3 (Composition API) · vue-router 4 · Vite 7 · TypeScript 5.9 (vue-tsc).

Infrastructure and quality: Forgejo Actions (CI) · golangci-lint (strict config, layer depguard) · go vet / gofmt · unit / integration / binary-smoke test levels · fuzzing of third-party format parsers · Playwright E2E (optional) · podman (OCI from scratch, multi-arch).


Development

Command Purpose
make lint golangci-lint run ./... (strict config)
make vet go vet ./...
make test go test ./...
make test-race go test -race ./...
make test-integration go test -race -tags integration ./test/integration/...
make cover Coverage report
make build Build bin/khrazhevnik
make web-build Vite build of the Web UI into internal/core/web/assets/
make web-dev Vite dev server proxying /api to :30202
make image OCI image from deploy/Containerfile (PLATFORMS, TAG)
make smoke Live container smoke test (locally, before a release)
make clean Remove bin/, coverage/, restore the web-assets stub

Load testing of a live instance (k6 scenarios, CPU/RAM samplers, methodology) lives in bench/ (runbook in Russian); reference results — in docs/func/EN/benchmarks.md.

On a fresh clone, the web-assets stub takes effect first (Go commands build without the frontend); the real bundle — make web-build. All tests run in CI (Forgejo Actions): contract suites on postgres/mariadb/minio, binary-smoke of the built artifact, optional -race and Playwright E2E.

Developer documentation (reading order before making changes):

  1. docs/ARCHITECTURE.md — layers, import rules, engine invariants.
  2. docs/SPECIFICATION.md — requirements, REST API, DB schema.
  3. docs/TESTING.md — testing strategy.
  4. docs/ROADMAP.md — the plan (guide); the stage history is in docs/HISTORY.md.

Plans

The nearest work after the v1.0.0 release is ecosystem expansion: XBPS and pkg (their "directory + index" model repeats already solved tasks), then Guix (the nix protocol), Flatpak last. Beyond that, without a fixed order: autonomous offline mirror export, instance federation, cache eviction and cleanup, OIDC/OAuth2, notifications, the CLI (khzr-cli), and global package search.

The full guide with details and design boundaries is in docs/ROADMAP.md; the history of completed stages is in docs/HISTORY.md.


License

The project is distributed under the GNU Affero General Public License v3.0 or later.

Khrazhevnik — caching proxy and mirror for Linux package repositories
Copyright (C) 2026  AlexRus1234

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

  1. The source code was developed with an AI assistant according to the predefined project architecture; architectural decisions, result verification, and final integration were performed by the author. ↩︎