company_dns

V4 container packaging: image size, data bundling, secret injection

Status: Superseded (2026-09-30) by v4-deployment.md. This doc’s content (image size audit, the secret-injection mismatch, data-bundling options, binary-size reduction plan) has been fully consumed into that doc’s §1–§3, alongside a new dev/staging/prod deployment-environments design raised immediately afterward. Kept here, unchanged, as the original research record — v4-deployment.md is the canonical, actively-updated source for all of this going forward; don’t edit this file for new decisions.


Status (original, as of 2026-09-29): Planned. Audited against real measurements (binary size, actual data-file size, existing Dockerfile/CI), not yet spiked or implemented. Owner: michael.hay@mediumroast.io Scope: two problems raised together, kept as two problems rather than conflated into one (§1 explains why): (1) how MEDIUMROAST_SHARED_SECRET (docs/plans/v4-security-hardening.md §3.5) gets from a GitHub Actions secret into a form the real deployment can use, without undoing that doc’s already-decided runtime-secret design; (2) why the V4 Rust server’s release binary is far larger than expected, and how SIC/EDGAR .feather data should reach a running container. Referenced from v4-security-hardening.md’s §3.5 (raised while designing that section’s rolling shared secret).


0. Why this doc

Raised directly, while working on v4-security-hardening.md §3.5’s rolling shared-secret mechanism: “For managing this secret, we can look at something like docker secrets to keep the secret sealed and inject the secret into image when built and keep the secret as a github secret referred to only by variable during the build.” Additionally: “I’ve noticed that the rust binary appears to be almost 200MB in size this is huge but we should consider how we bundle in the feather files to a docker container so that the binary remains as small as possible.” Both are real, but auditing the actual numbers (§1) shows they’re independent problems with independent fixes, not one problem — worth saying up front since the second request’s framing (“bundle feather files… binary remains small”) implies the data might be inflating the binary, and it isn’t.

1. Current state (audited 2026-09-29)

1.1 Binary size — measured, not assumed

1.2 Feather/data-file size — measured, and small

1.3 Existing container/CI state

2. A mismatch worth flagging before designing §3.1

Two genuinely different things both get called “secret” across the two requests that opened this doc, and treating them as one would undo a decision v4-security-hardening.md §3.5 already made deliberately.

Build-time secrets (BuildKit --secret / “Docker secrets” in the sense raised here; docker/build-push-action’s secrets: input): mounted into the build container’s ephemeral filesystem only for the RUN step that needs them, and — the actual property that makes them worth using — never written into any image layer or the image’s layer history. docker history on the resulting image shows nothing. Right tool for a secret genuinely needed only during docker build — e.g. a private crate-registry auth token, or credentials to fetch SIC feather data from an internal source as part of the build if §3.2 lands on that option.

Runtime secrets — specifically MEDIUMROAST_SHARED_SECRET, which v4-security-hardening.md §3.5 already designed: the running server process reads it at startup and recomputes an hourly rolling HMAC against it on every incoming request. This is not a build-time need. Baking it into the image via a build-time secret mechanism would be a regression from what §3.5 already decided, for three concrete reasons: (1) rotation would require rebuilding and redeploying the image instead of just updating a K8s Secret and rolling pods — §3.5’s own “Rotation” paragraph already assumes the lighter-weight path is available; (2) a built image is typically pulled by more environments and cached more widely than a deploy-time secret store, so baking a secret into every image pull widens exposure versus injecting it only where the pod actually runs; (3) it conflates the build artifact (should be identical across dev/staging/prod) with environment-specific config (the secret differs, or must be rotatable independently, per environment) — the same reasoning that already keeps SIC_DATA_PATH/EDGAR_CATALOG_PATH/PORT as runtime env vars (v4/README.md’s “Running it” section) rather than baked in.

Recommendation: keep MEDIUMROAST_SHARED_SECRET exactly as §3.5 already decided — a runtime env var sourced from company_dns’s own K8s Secret, never touched by the image build at all. Reserve BuildKit --secret for a genuine build-time need, if one turns out to exist once §3.2 is decided. This doc is not proposing to reopen §3.5’s design — it’s flagging that one reading of “inject the secret into the image when built” would, and recommending against that reading specifically.

3. Design

3.1 Build-time secrets, for whichever actual build-time need exists

Pattern, following the existing workflow’s own shape (.github/workflows/main.yml:29-37):

- name: Build and push
  uses: docker/build-push-action@v4
  with:
    context: .
    push: true
    secrets: |
      SOME_BUILD_SECRET=$
RUN --mount=type=secret,id=SOME_BUILD_SECRET \
    SOME_BUILD_SECRET=$(cat /run/secrets/SOME_BUILD_SECRET) cargo build ...

No concrete build-time secret is identified yet — this section exists so the mechanism is documented and ready the moment §3.2 lands on an option that needs one (e.g. private-source credentials to fetch feather data during the image build). Not built until there’s a real secret to protect this way.

3.2 Getting SIC/EDGAR feather data into a running container

Three options, real tradeoffs, not yet decided:

Leaning (b) or (c) over (a), given the existing ingest-edgar/ server separation and the monthly EDGAR-refresh cadence already implying data changes on a different schedule than code — but not decided here; real production data size (§6) and actual deployment tooling available (docs/plans/onprem-k8s-migration.md) should settle it, not this doc alone.

3.3 Binary size reduction — independent levers, each needs its own spike before committing to numbers

  1. [profile.release] tuning.
    • strip = true — already confirmed to save ~44MB (26%) via this doc’s own manual test (§1.1). Free, no known downside for a server binary (no need for symbols in a shipped image).
    • lto = "thin" or "fat" — not yet measured. Real Rust binaries with heavy generic-monomorphization-prone dependencies (Arrow’s columnar/generic code is exactly this shape) often see meaningful size reduction from LTO, but it has to be measured on this dependency graph, not assumed from general Rust folklore — §4.
    • codegen-units = 1 — smaller/faster binaries at the cost of slower builds; worth measuring alongside LTO since both trade build time for binary size.
    • panic = "abort" — explicitly NOT recommended without further study, flagged here so it isn’t reached for reflexively as a “free” size win. Tokio’s per-task unwind isolation is what keeps a single panicking request handler from taking down the whole server today (the panic unwinds out of that task, the connection it was handling fails, the rest of the process keeps running). panic = "abort" removes unwinding entirely — the same panic would abort the whole process instead of just that one request. That’s an availability regression for an HTTP service, not a harmless build tweak, and it isn’t worth chasing without first confirming nothing in the request path actually depends on per-task unwind recovery.
  2. DataFusion feature trimming — split into two very different risk categories, not one flat “unused, cut it” list. default-features = false on the workspace datafusion dependency, then:
    • File-format/codec support — the safe part to cut. parquet, avro-adjacent datasource support, and the compression codecs (bzip2/flate2/zstd/liblzma via compression) gate what storage formats DataFusion can read/write. They’re orthogonal to SQL expressiveness — cutting them has no effect on what a ctx.sql() query can say, only on what file formats a CREATE EXTERNAL TABLE/register_* call could point at. §1.1 already confirmed no Parquet API use anywhere in company_dns’s own code, and the SIC data source is .feather (Arrow IPC), not Parquet/Avro — this part of the trim looks genuinely safe.
    • Expression-function libraries — deliberately NOT trimmed by default, kept conservative. crypto_expressions, datetime_expressions, encoding_expressions, regex_expressions, string_expressions, unicode_expressions, nested_expressions are a different thing entirely: they gate what a ctx.sql() query can actually express — date/time functions, regex matching, string manipulation, array/nested functions. Raised directly as a forward-looking concern: more expressive SQL may well be needed later (the whole point of moving SIC/EDGAR lookups onto a real SQL engine rather than hand-rolled filtering), and cutting these now to save binary size risks silently blocking a future query from working, discovered only when someone writes it and it fails to compile against a trimmed engine. Decided: keep the full expression-function feature set enabled by default in the first pass of this trim, and only reconsider cutting specific ones later, with an explicit tradeoff conversation at that point, if binary size is still a real problem after the format/codec cut and the other levers in this section (§3.3.1, §3.3.3) are already applied.
    • sql itself stays on regardless (§1.1: genuinely required, ctx.sql() used throughout crates/sic).
    • Real savings from the format/codec-only cut not yet measured; needs a spike (§4) that actually builds with that narrower trimmed feature list and confirms both that it still compiles/passes tests and how much smaller the binary gets. Expect a more modest number than trimming everything, since the expression-function libraries are being kept — that’s the accepted tradeoff here.
  3. Multi-stage Docker build. A Rust build stage (full toolchain, large) should never be the runtime image — the final stage should carry only the compiled binary plus whatever minimal base a strip+trimmed binary actually needs (debian:bookworm-slim, or a distroless/scratch-style base if the binary ends up fully static via musl — needs checking whether reqwest’s rustls-tls feature, already chosen project-wide over native-tls, is compatible with a static musl target). This doesn’t shrink the binary itself, but avoids compounding an already-large binary with an unnecessarily large base image on top of it.

4. Research spike (before any implementation)

Following this project’s established pattern (experiments/ spikes before promoting into v4/ proper — most recently experiments/rate-limit-spike/):

  1. Add strip = true, try lto = "thin" and lto = "fat", codegen-units = 1, in various combinations — measure real binary size and build-time cost for each, don’t guess.
  2. Trim DataFusion’s default-features down to the format/codec-only cut §3.3.2 outlines (parquet, avro-adjacent support, compression codecs — the expression-function libraries stay on, deliberately), confirm cargo build/cargo test still pass across crates/sic, crates/edgar, crates/server — measure the resulting binary size delta. Don’t widen the cut to the expression-function features as part of this spike even if the format/codec-only savings turn out small — that’s a separate, harder tradeoff §3.3.2 deliberately deferred, not something to fold in opportunistically.
  3. Prototype a real multi-stage Dockerfile using the smallest binary §4.1/§4.2 produce, measure the final image size end to end (not just the binary in isolation) — including confirming whether a static-musl build is realistic given reqwest’s rustls-tls feature choice, or whether a glibc-slim base is the practical floor.
  4. Prototype whichever data-bundling option §3.2 lands on, once it’s decided — not blocked on the size work above, can run in parallel.

5. Implementation steps (once spike confirms numbers)

  1. v4/Cargo.toml: add [profile.release] with whatever §4.1 confirms (at minimum strip = true, which already has a confirmed number behind it).
  2. v4/Cargo.toml: trim datafusion’s features per §4.2’s confirmed safe list.
  3. Add a real v4/Dockerfile (multi-stage, per §3.3.3/§4.3) — first Rust Dockerfile this repo will have.
  4. Wire §3.2’s chosen data-bundling approach into that Dockerfile and/or the deployment manifests it needs alongside it.
  5. Extend .github/workflows/ (or a new V4-specific workflow) to build and push the V4 image, following the existing main.yml template (docker/build-push-action@v4, GHCR, multi-platform) — decide whether V4 replaces V3’s workflow outright or runs alongside it during a transition period (not decided here, v4-server-prototype.md scope question).
  6. If §3.1’s build-time secret mechanism ends up needed for §3.2’s chosen data source, wire it in following §3.1’s pattern.
  7. Update v4/README.md with a “Building the container” section, matching the existing “## API docs”/”## Endpoints” style.

6. Open questions

7. Explicitly out of scope for this doc