Sign inSign up

teyfix/cache-proxy

By teyfix

•Updated 4 days ago

YAML-configured NGINX caching, routing and structured access logs.

Image
0

1.0K

teyfix/cache-proxy repository overview

⁠Cache proxy

teyfix/cache-proxy turns mounted YAML route descriptions into an NGINX caching proxy. The shared base uses CUE to validate inputs/defaults and resolve environment/file references; gomplate renders the native configuration, and nginx -t checks it. Process Compose starts NGINX only after configuration succeeds and owns shutdown. There is no runtime dependency on another tools container.

⁠Build and run

From the repository root:

docker buildx bake cache-proxy --load
docker buildx bake cache-proxy-smoke
mkdir -p .cache/cache-proxy/responses
mkdir -p .state/cache-proxy/logs
docker compose -f svc/cache-proxy/example/compose.yaml up -d
curl http://localhost:8080/healthz

The example reads TMDB_AUTHORIZATION from the environment (a complete value such as Bearer ...) and provides /tmdb/ and /images/ routes. It uses a project-local cache bind, preserved when Compose removes disposable containers. Copy/adapt the YAML and Compose wiring into a consuming project; rendering code stays in the image. The local dev tag is built above. Once publishing is enabled, consumers can select the explicit image build tags described in the repository README.

⁠Configuration

Mount a directory of .yaml and/or .yml files at /config, read-only. Each file contains exactly one document with a nonempty routes list. Routes are combined in stable filename order; there is no override/last-wins rule. Duplicate route names or hostname/match pairs fail startup, including across files. Nested prefixes are supported; NGINX selects the longest matching prefix within the selected hostname. Unknown fields fail validation.

routes:
  - name: artwork
    upstream: https://image.tmdb.org
    prefix: /images/
    strip_prefix: true
    cache:
      ttl: 365d
      ttl_mode: respect_upstream
      inactive: 365d
      max_size: 5g
    upstream_headers:
      User-Agent: my-project
    response_headers:
      Access-Control-Allow-Origin: "*"
FieldBehavior/default
nameRequired unique lowercase name; letters, digits, _, -
hostsOmitted or []: catch-all host. Otherwise a nonempty list of exact hostnames; matching is case-insensitive and ignores the incoming Host port
upstreamRequired HTTP(S) origin: DNS/IPv4 hostname and optional port; no credentials, path, query, or fragment
matchprefix (default), exact, or regex; exact uses prefix, regex requires an anchored pattern and cannot strip a prefix
prefix/ by default; prefix matches must end with /; exact matches may name a single path
timeoutsconnect: 10s, read: 60s, send: 60s
cache.status_ttlPer-status fallback TTL map, e.g. {"404": 1h, "429": 10s}
cache.locktimeout: 5s, age: 5s; raise both for expensive inference
cache.staleList of native error/status conditions that permit stale responses; empty by default
cache.background_updatefalse; optional for GET/HEAD, unavailable for POST
cache.keyOptional namespace, additional headers, body: true, and idempotency_header
strip_prefixfalse; if true, /images/a.jpg?v=1 becomes /a.jpg?v=1 upstream
max_body_size1m; request-size limit and body buffer, positive size with k, m, or g
cache.methods[GET, HEAD]; add POST explicitly for read-only endpoints
cache.enabledtrue; false makes the route pass-through
cache.ttl1h; positive whole number plus s, m, h, d, or w
cache.ttl_moderespect_upstream; configured TTL is a fallback for responses without upstream freshness directives
cache.inactiveSame as ttl; evict entries not accessed for this duration, independently of freshness
cache.max_size1g per route; positive whole number plus k, m, or g
cache.shared_credentialsfalse; explicitly permit sharing responses fetched with a configured Authorization/Cookie header
upstream_headersHeader overrides sent to the origin; other client request headers are forwarded normally
response_headersReplace/add headers returned to clients, including error responses

Hostname and prefix match together. With the default prefix: /, a route matches every path for its hostname:

routes:
  - name: tmdb
    hosts: [tmdb.localhost]
    upstream: https://api.themoviedb.org
    # prefix defaults to /; keep the existing cache/header settings here.
  - name: artwork
    hosts: [artwork.localhost]
    upstream: https://image.tmdb.org

For example, curl -H 'Host: tmdb.localhost' http://localhost:8080/3/movie/550 selects the first route. Adding prefix: /tmdb/ requires both that hostname and that prefix; strip_prefix: true still removes the matched prefix upstream. Different hosts may reuse the same prefix. Aliases in hosts select the same route; each incoming hostname has its own cache identity.

Exact hostname selection happens first. Routes without hosts belong only to the default server for unrecognized hosts; they are not inherited by named hosts. An unmatched path returns 404. /healthz remains local on every host. Configuration hostnames accept DNS labels/IPv4, without ports, trailing dots, schemes, wildcards or regular expressions. This selects incoming HTTP hosts; it does not provision DNS or terminate incoming TLS.

Header values are literal printable ASCII strings, {from_env: VARIABLE_NAME}, or {from_file: /run/secrets/header}. Unset referenced variables fail startup; empty values are allowed (an empty upstream header removes it). Quotes and backslashes are escaped internally. Dollar signs/NGINX expressions and line breaks are rejected. Header names are case-insensitive; case-only duplicates fail. Connection, Content-Length, Transfer-Encoding, and X-Cache-Status are reserved. Host defaults to the origin's authority and may be explicitly overridden. HTTPS uses CA verification and SNI for the origin hostname. This initial interface supports DNS names and IPv4, not bracketed IPv6 origins or upstream base paths.

cache.ttl_mode: override applies the declared TTL instead of upstream Cache-Control, Expires, or X-Accel-Expires freshness. It still refuses to store responses marked private/no-store/no-cache or carrying Set-Cookie. It does not turn private responses into shared cache entries.

GET/HEAD responses with status 200, 301, or 302 are eligible for caching. Set cache.methods: [GET, HEAD, POST] to also cache POST responses on a route intended for read-only requests. Other methods and statuses pass through unless a status TTL is configured. NGINX always permits GET/HEAD when caching is enabled. Requests with Authorization, Cookie, Cache-Control no-cache/no-store, Pragma no-cache, or X-Cache-Bypass: 1 bypass reading and writing the cache. Incoming authorization/cookies always bypass, even when shared_credentials is enabled. Configured service credentials also bypass unless that option is enabled; only enable it for responses intended to be shared between callers. Route-specific API-key headers are not automatically classified: disable caching for private routes using other authentication schemes.

The cache key contains a configuration fingerprint, route identity, incoming hostname, upstream, request method, full incoming URI/query, Accept, Accept-Language, Content-Type, and a SHA-256 hash of the complete request body. The njs module from the pinned upstream image hashes the body and the JSON-encoded request-identity tuple, keeping large bodies out of the native cache key and preserving boundaries between header values. max_body_size defaults to 1m and also sets the in-memory body buffer. Requests exceeding it return 413; requests buffered to a temporary file bypass caching rather than share an empty-body key. NGINX also honors upstream Vary. Changing configuration or referenced credential values starts a fresh cache namespace; existing files expire normally. A restart with unchanged configuration can reuse its cache. Credentials are not written into the cache key or access log by the wrapper. Generated configuration containing resolved headers is private to root under /run/cache-proxy.

⁠Specialized policies

Exact locations win first; otherwise NGINX evaluates regex routes in declaration order before falling back to the longest prefix. Regex matching uses native PCRE, validated by nginx -t. A configuration fingerprint includes cache.key.namespace, so a model/version change invalidates the previous response namespace without deleting its cache files.

routes:
  - name: transcription
    upstream: http://transcriber:8080
    match: exact
    prefix: /transcribe
    timeouts: { read: 65s, send: 65s }
    cache:
      methods: [POST]
      lock: { timeout: 60s, age: 60s }
      key:
        namespace: model-v2
        headers: [X-Model]
        body: false
        idempotency_header: X-Idempotency-Key

Body hashing stays enabled by default. Disabling it for POST requires an idempotency header; a missing header bypasses both cache reads and writes. With this option the consumer owns uniqueness of that identity across different request bodies.

graphql.enabled: true uses the GraphQL parser to cache only a selected query operation. Add POST to cache.methods for POST queries. Mutations, subscriptions, ambiguous operations, malformed input, JSON batches and spilled bodies pass upstream without caching. graphql.anonymous_cookies: true additionally ignores/removes response Set-Cookie only for anonymous parsed queries; client credentials and explicit bypass retain the upstream cookies. Private/no-store response directives still prohibit storage. This is an explicit provider policy, not the default for GraphQL.

follow_redirects: true follows HTTP redirects through native NGINX internal redirects, caches final successful bytes under the original URL, and strips Authorization/Cookie on the redirected request. Only GET/HEAD are supported. Absolute and origin-root-relative Location values are supported; NGINX's internal redirect limit bounds loops. Redirect responses themselves are not cached. Keep this on trusted download routes; it can follow URLs to other origins.

origin: path accepts /http/<host[:port]>/<path> or /https/<host[:port]>/<path> and preserves its query string. Omit upstream; this mode requires the root prefix and permits only GET/HEAD. Native NGINX performs DNS, TLS verification and caching, with credentials stripped upstream. Like Animalux's image-source endpoint, it is an internal arbitrary-origin fetch interface: give it a dedicated internal hostname/network, not an unrestricted public ingress.

⁠Runtime interface

  • 8080: proxy HTTP port. /healthz is reserved and reports local readiness after successful rendering and NGINX validation; it does not probe upstreams.
  • /config: input YAML; alternative path via CACHE_PROXY_CONFIG_DIR.
  • /var/cache/nginx: reusable cache; mount project .cache/cache-proxy/responses here. Override with top-level paths.cache. Entries live under <cache>/<hostname>/<route>; the default server uses _default, and NGINX stores MD5-keyed files beneath each route.
  • /run/cache-proxy: disposable generated configuration and process state.
  • /var/log/nginx: log volume (override paths.log); mount project .state/cache-proxy/logs here and expose it read-only to an external Vector collector. Access logs are newline-delimited JSON in cache-proxy-access.jsonl; native text errors go to cache-proxy-error.log and stderr. Bootstrap/Process Compose diagnostics stay on stdout/stderr. Both files are mode 0644 for non-root collectors; generated configuration stays private. No collector or exporter is bundled.

JSON access logs retain TMDB Embed's time, remote_addr, method, uri, args, status, bytes_sent, proxy_host, rt, uct, uht, urt, cache, and user_agent fields, and add host and route. Durations are in seconds; upstream timings remain strings because retries can yield multiple values and cache hits can yield -. status, bytes_sent, and rt are numeric. Strings are JSON-escaped. Health checks are excluded. Authorization/Cookie headers are not logged; args contains the query string, so consumers must keep credentials out of query parameters. X-Cache-Status reports the same cache status to clients.

Vector owns collection and parsing. File retention/rotation belongs to the consumer; the image does not rotate logs. After renaming log files, reopen them with docker compose -f svc/cache-proxy/example/compose.yaml exec cache-proxy nginx -s reopen -c /run/cache-proxy/nginx.conf.

DNS uses Docker's embedded resolver (127.0.0.11); run on a Compose/user-defined Docker network. Unavailable upstreams produce proxy errors without preventing startup. Configuration changes take effect on container restart; live reload is not part of this first contract. No application state or upstream content is deleted by restarting the image.

⁠Verification

The smoke target adds Python only to its test stage and exercises a real HTTP origin through the shipped Process Compose startup. It checks routing, headers, query/Vary cache separation, TTL fallback/override, bypass and privacy behavior, credential changes across restart, hostname/prefix selection and cache isolation, POST body/type separation, large bodies, custom paths, JSON log files, readiness without an upstream, invalid input, and shutdown. CI runs it on amd64 and arm64. The runtime image contains no Python or test server.

Bake's check-only mode cannot currently resolve the target:base dependency. docker buildx bake --check checks the default tools targets; the proxy workflow validates its Bake graph and then performs a real build and smoke test.


Maintained from the source documentation⁠. See published tags⁠ and the service integration guide⁠.

Tag summary

Content type

Image

Digest

sha256:243121839…

Size

89 MB

Last updated

4 days ago

docker pull teyfix/cache-proxy:0.2.0-build.local.20261017055747