Dockerfile 4.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. # Minimal static-musl image for the d-OAuth OAuth 2.1 authorization server.
  2. #
  3. # 4 stages: chef -> planner -> builder -> runtime.
  4. # - `chef`: pinned rust toolchain + cargo-chef + musl targets. No DB needed:
  5. # `SQLX_OFFLINE=true` builds against the committed `.sqlx/` query cache.
  6. # - `planner`: captures the dependency recipe so dependency layers cache.
  7. # - `builder`: cooks dependencies, then builds the locked, stripped,
  8. # fully-static musl binary (rustls `ring` provider only; no
  9. # aws-lc-sys/OpenSSL anywhere in the graph).
  10. # - `runtime`: pinned-digest distroless `static-debian12:nonroot` (no shell,
  11. # no package manager, CA bundle + tzdata + nonroot /etc/passwd entry
  12. # included). Only the stripped binary is copied in.
  13. #
  14. # BuildKit cache mounts keep the cargo registry/git cache and the target
  15. # directory warm across rebuilds without baking them into any layer.
  16. # Pinned bases (multi-arch index digests; buildx selects per-platform):
  17. # - rust 1.99-bookworm (matches rust-toolchain.toml stable; rustc 1.99.0)
  18. # - distroless static-debian12:nonroot (UID:GID 65532, no shell)
  19. ARG RUST_BASE=docker.io/library/rust:1.99-bookworm@sha256:114c7a4425406451c2866b6aafe69fe29b1b298832db1277d411ac73c82d04d6
  20. ARG RUNTIME_BASE=gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab
  21. FROM ${RUST_BASE} AS chef
  22. WORKDIR /app
  23. # Reproducible builds: fixed epoch (overridden by bake) and no incremental.
  24. ARG SOURCE_DATE_EPOCH=0
  25. ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
  26. SQLX_OFFLINE=true \
  27. CARGO_INCREMENTAL=0 \
  28. CARGO_NET_RETRY=10
  29. RUN cargo install cargo-chef --locked --version 0.1.78 \
  30. && apt-get update \
  31. && apt-get install -y --no-install-recommends musl-tools \
  32. && rm -rf /var/lib/apt/lists/* \
  33. && rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
  34. FROM chef AS planner
  35. COPY . .
  36. RUN cargo chef prepare --recipe-path recipe.json
  37. FROM chef AS builder
  38. ARG TARGETARCH
  39. COPY --from=planner /app/recipe.json recipe.json
  40. # Cook (compile) third-party dependencies only: this layer reuses the cache
  41. # unless Cargo.toml/Cargo.lock change.
  42. RUN --mount=type=cache,target=/usr/local/cargo/registry \
  43. --mount=type=cache,target=/usr/local/cargo/git \
  44. --mount=type=cache,target=/app/target \
  45. sh -c 'if [ "$TARGETARCH" = "arm64" ]; then export RUST_TARGET=aarch64-unknown-linux-musl; else export RUST_TARGET=x86_64-unknown-linux-musl; fi && \
  46. rustup target add "$RUST_TARGET" && \
  47. cargo chef cook --locked --release --target "$RUST_TARGET" --recipe-path recipe.json --bin d-oauth-server'
  48. COPY . .
  49. # Locked, offline release build of the server binary only. The target dir is
  50. # a cache mount, so the binary is copied out to /app/dist in the same layer.
  51. RUN --mount=type=cache,target=/usr/local/cargo/registry \
  52. --mount=type=cache,target=/usr/local/cargo/git \
  53. --mount=type=cache,target=/app/target \
  54. sh -c 'if [ "$TARGETARCH" = "arm64" ]; then export RUST_TARGET=aarch64-unknown-linux-musl; else export RUST_TARGET=x86_64-unknown-linux-musl; fi && \
  55. rustup target add "$RUST_TARGET" && \
  56. cargo build --locked --release --target "$RUST_TARGET" --bin d-oauth-server && \
  57. mkdir -p /app/dist && \
  58. cp "/app/target/$RUST_TARGET/release/d-oauth-server" /app/dist/d-oauth-server'
  59. FROM ${RUNTIME_BASE} AS runtime
  60. # Only the stripped static binary (release profile: lto thin, strip symbols).
  61. COPY --from=builder /app/dist/d-oauth-server /d-oauth-server
  62. # Non-root, exec-form entrypoint (no shell in the image to interpret one).
  63. USER 65532:65532
  64. # Public TLS listener (default bind_addr 0.0.0.0:8080). The plaintext health
  65. # listener (default 0.0.0.0:8081, pod IP) is intentionally NOT published: it
  66. # is probed by kubelet/HEALTHCHECK from inside the pod network only.
  67. EXPOSE 8080
  68. # Shell-less self-probe: the `healthcheck` subcommand speaks raw HTTP/1.1
  69. # over loopback to the plaintext health listener (no shell, no TLS trust).
  70. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD ["/d-oauth-server", "healthcheck"]
  71. ENTRYPOINT ["/d-oauth-server", "serve"]
  72. LABEL org.opencontainers.image.title="d-oauth-server" \
  73. org.opencontainers.image.description="OAuth 2.1-only authorization server (static musl, non-root, no shell)" \
  74. org.opencontainers.image.licenses="MIT OR Apache-2.0" \
  75. org.opencontainers.image.source="https://github.com/Syavne/d-OAuth-Server" \
  76. org.opencontainers.image.revision="unknown" \
  77. org.opencontainers.image.vendor="d-oauth"