pronounce: /bwin/
bouine is a cloud-native HTTP cache in Go — RFC 9111 compliant, zero-alloc hit path, gossip clustering, no external K/V store. It targets the same problem space as a classic HTTP cache but is designed from day one for Kubernetes, multi-instance clustering, and first-class observability.
Status: v1.0-rc — core caching, clustering, negative caching, jittered TTLs, soft-purge, and the Go SDK are shipped. Validated on k3s with 3-node gossip cluster.
make conformancescores 349/365 (95.6%) onhttp-tests/cache-tests. SeeCHANGELOG.mdfor what's shipped and what's deferred (prefetching, HTTP/3, VCL shim, and AI insights are not yet implemented).
- Protocols: HTTP/1.1 only on the data plane (fasthttp, ADR-0034); the admin port runs on the same stack. HTTP/2 and HTTP/3 are not supported.
- Embedded storage: sharded in-RAM hot tier + mmap warm tier. No external KV.
- Clustering: gossip membership + consistent hash + peer fetch. K8s StatefulSet friendly.
- Compliance: 95.6 % on
http-tests/cache-tests(349/365). Covers RFC 9111 freshness, stale-while-revalidate, stale-if-error, CDN-Cache-Control, heuristic caching, Vary, conditional requests, andmust-understand. - Performance: zero-alloc hit path, benchmark-gated CI.
- Observability: Prometheus, OpenTelemetry, slog, pprof.
- Migration: NGINX and Varnish migration guides included.
| bouine | Varnish | NGINX | |
|---|---|---|---|
| Architecture | Reverse-proxy cache | Reverse-proxy cache | Reverse proxy + cache |
| Clustering | Gossip + consistent hash (built-in) | Commercial (Varnish Plus) or external orchestration | Upstream hash (no cluster state) |
| Cache invalidation | Purge + ban (predicate) + soft-purge | Purge + ban | Purge only (no ban) |
| Observability | Prometheus + OTel + slog (built-in) | Vmod-based | Third-party or NGINX plus |
| Config | YAML (declarative) | VCL (imperative) | NGINX.conf |
| Kubernetes | StatefulSet + Helm chart, first-class | Sidecar / external | Ingress controller pattern |
┌─────────────────────────────────────────┐
Client ── HTTP──► │ bouine │ ──► Origin
│ ┌───────────┐ ┌───────────┐ │
│ │ Hot tier │ │ Warm tier │ │
│ │ (RAM) │ │ (mmap disk)│ │
│ └───────────┘ └───────────┘ │
│ ┌────────────────────────────┐ │
│ │ Cache engine (RFC 9111) │ │
│ │ Vary · SWR · SIE · purge │ │
│ └────────────────────────────┘ │
└──────────┬──────────┬──────────┬────────┘
gossip peer fetch metrics / traces
│ │ │
┌────────▼───┐ │ ┌────▼─────┐
│ peers │◄────┘ │ Prom │
│ (cluster) │ │ OTel │
└────────────┘ └──────────┘
Run bouine with Docker and cache a request in under a minute.
Create a minimal config:
cat > bouine.yaml <<EOF
listen:
http: ":8080"
admin: ":9000"
upstream_pools:
- name: origin
targets: ["httpbin.org:80"]
routes:
- match: {}
pool: origin
cache:
ttl_default: 60s
EOFRun bouine:
docker run -d --name bouine -p 8080:8080 -p 9000:9000 \
-v "$PWD/bouine.yaml:/etc/bouine/config.yaml" \
bouinecache/bouine:latest serve --config /etc/bouine/config.yamlTest it — first request is a MISS, second is a HIT:
curl -s -I http://localhost:8080/get | grep x-cache
# X-Cache: MISS
curl -s -I http://localhost:8080/get | grep x-cache
# X-Cache: HITCheck the admin endpoint:
curl -s http://localhost:9000/healthz
# okDemo: first request is a MISS, second is a HIT
$ curl -sI http://localhost:8080/get
HTTP/1.1 200 OK
Content-Type: application/json
X-Cache: MISS
Age: 0
$ curl -sI http://localhost:8080/get
HTTP/1.1 200 OK
Content-Type: application/json
X-Cache: HIT
Age: 1
# Homebrew (macOS)
brew install bouine-cache/bouine/bouine
# Prebuilt binary (from releases)
curl -L https://github.com/bouine-cache/bouine/releases/latest/download/bouine-v0.4.1-linux-amd64 -o bouine
chmod +x bouine
# or via Go
go install github.com/bouine-cache/bouine/cmd/bouine@latest
# or Docker
docker pull bouinecache/bouine:latestgit clone https://github.com/bouine-cache/bouine.git
cd bouine
make build # -> ./bin/bouine
./bin/bouine serve --config bouine.yamlSee CONTRIBUTING.md for the full development setup.
Every admin-plane subcommand (status, purge, ban, refresh,
cluster peers, config get/set/diff, keys find/top/recent)
accepts the global flags
--server, --token, and
--format text|json. Values resolve flag > environment > default, so
scripts usually export the connection once:
export BOUINE_SERVER=127.0.0.1:9000 # admin listener
export BOUINE_TOKEN=<admin bearer token> # prefer env: never lands in shell history
bouine purge http://example.com/img.png
bouine cluster peers --format jsonOne-glance overview. bouine status renders five sections — node
(version, uptime, readiness), cache (hit ratio, tier fill), origins
(per-target health and probe streaks), cluster (peers, peer-fetch
stats), and insights (the dashboard's evaluated rules) — in one
round-trip to GET /v1/status:
bouine status # node-local; no peer traffic
bouine status --cluster # adds per-peer request counters (one peer fan-out)
bouine status --watch # fullscreen TUI on a terminal; Ctrl-C aborts the in-flight fetchOn a terminal, one-shot status prints the styled frame inline
(colors, tier bars) and exits; --watch enters a fullscreen
alternate-screen TUI with q/Esc/Ctrl-C (quit), r (refresh),
and c (toggle cluster) keys. Piped output and --format json
never take the TUI path, so scripts never see escape sequences.
Tuning a running daemon. bouine config reads and changes
hot-config knobs without a restart. Runtime changes are ephemeral —
lost at the next restart — so every set prints a warning and
config diff shows what is at risk:
bouine config get # every knob: value + source (file/default/runtime)
bouine config set cluster.ban_ttl 90m # validated point mutation, applied live
bouine config set cluster.ban_ttl 90m --dry-run # validate and report, change nothing
bouine config set pool:api.health.passive.consecutive_5xx 10 # per-pool scoped knob
bouine config diff # knobs a restart would lose
bouine config diff prod.yaml staging.yaml # deploy preview: effective-config diffOn a strong-mode cluster an applied set is broadcast to every live peer (the CLI reports the acknowledged count), so runtime overrides stay identical across the fleet.
Reloading a whole file (experimental). With
experimental.config_reload: true, bouine config reload [--file]
applies a config file's hot-appliable changes at once and reports
what needs a restart — parse failures change nothing:
bouine config reload
# reloaded /etc/bouine/config.yaml
# applied cluster.ban_ttl: 24h0m0s -> 45m0s
# NEEDS RESTART (restart required to activate):
# listen.http: 127.0.0.1:9000 -> 127.0.0.1:9001Cache-key queries. bouine keys answers cache questions without
storage enumeration: find computes the key for a URL exactly the
way the data plane does and reports the object's state on this node,
and top/recent replay the URL ring's sampled non-HIT traffic —
what the node is missing on lately:
bouine keys find http://example.com/pages/p?x=1 # key + tier/size/TTL/ban state
bouine keys find http://example.com/pages/p --peer # cluster: also ask the ring owner
bouine keys top --limit 20 # most-requested prefixes, result breakdown
bouine keys top --class miss # keep only prefixes with misses
bouine keys recent # latest-active prefixesThe listing is a request-traffic view, not a storage inventory: the
ring samples non-HIT requests (url_ring_sample_rate), keeps the
first three path segments per prefix, and holds at most 512
prefixes. URL→key is deterministic; key→URL is impossible (the hash
is irreversible), which is why there is no object listing. find
and the listings are node-local — target named pods for per-node
answers.
Targeting a pod in Kubernetes. The admin port is deliberately never
exposed by a LoadBalancer (the admin Service is ClusterIP, and the image
is distroless so there is no shell to kubectl exec into). Use
port-forward against a named pod:
kubectl -n cache port-forward pod/bouine-0 9000:9000 &
export BOUINE_SERVER=127.0.0.1:9000
export BOUINE_TOKEN="$(kubectl -n cache get secret bouine-admin-token \
-o jsonpath='{.data.token}' | base64 -d)"
bouine cluster peersCaveats:
- Port-forward works because
listen.adminbinds:9000on all interfaces; RBAC onpods/portforwardgates access. - The ClusterIP admin Service picks a random pod per connection —
fine for broadcast operations (
purge,ban,refreshpropagate cluster-wide) but wrong for node-local output. Target named pods via port-forward or the StatefulSet headless DNS (bouine-0.bouine-headless.<namespace>.svc:9000). - Do not expose the admin port via Ingress/LoadBalancer: token-only auth is too thin for a public listener.
All releases are cryptographically signed using cosign with keyless signing (Sigstore, via GitHub Actions OIDC). The private key is never stored on any distribution server — signatures are generated in CI using ephemeral credentials tied to the GitHub workflow.
Verify a binary release:
# Download the release assets
curl -L -O https://github.com/bouine-cache/bouine/releases/latest/download/SHA256SUMS
curl -L -O https://github.com/bouine-cache/bouine/releases/latest/download/SHA256SUMS.sig
curl -L -O https://github.com/bouine-cache/bouine/releases/latest/download/SHA256SUMS.pem
# Verify the checksums signature
cosign verify-blob \
--certificate SHA256SUMS.pem \
--signature SHA256SUMS.sig \
--certificate-identity https://github.com/bouine-cache/bouine/.github/workflows/release.yml@refs/tags/v0.4.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
SHA256SUMS
# Verify the binary matches the checksums
sha256sum -c SHA256SUMSVerify the Docker image:
cosign verify --certificate-identity-regexp "https://github.com/bouine-cache/bouine/.github/workflows/release.yml@refs/tags/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
bouinecache/bouine:latestThe signing certificate's identity is tied to the release.yml workflow
in the bouine-cache/bouine repository, so only releases built by the
official CI pipeline can produce valid signatures.
helm repo add bouine https://charts.bouine.org
helm repo update
helm install bouine bouine/bouine \
--namespace bouine --create-namespace \
--set "config.upstream_pools[0].name=app" \
--set "config.upstream_pools[0].targets[0]=app.default.svc:8080" \
--set "config.routes[0].pool=app" \
--set config.cluster.enabled=trueThe chart deploys a 3-replica StatefulSet with gossip clustering,
a headless Service for peer discovery, and a PodDisruptionBudget.
See deploy/helm/bouine/values.yaml
for all options.
📖 Full documentation: bouine.org (getting started, configuration, operations, guides). The in-repo docs below are the canonical source the site is built from.
| Topic | Where |
|---|---|
| Getting started & install | bouine.org/docs/getting-started |
| Architecture reference | docs/architecture.md |
| Roadmap | ROADMAP.md |
| Configuration reference | bouine.org/docs/configuration |
| Migration from NGINX | docs/migration/nginx.md |
| Migration from Varnish | docs/migration/varnish.md |
| Contributing | CONTRIBUTING.md |
| Governance | GOVERNANCE.md |
| Changelog | CHANGELOG.md |
| Security policy & disclosure | SECURITY.md |
| Discussions & community | GitHub Discussions |
All contributors are bound by AGENTS.md. Humans should
start at CONTRIBUTING.md; AI agents start at
AGENTS.md. Security issues go through
SECURITY.md, never public issues.
- GitHub Discussions — ask questions, share configs, and discuss design decisions.
- GitHub Issues — bug reports and feature requests (use the issue templates).
- Security advisories — private vulnerability reporting.
