Dockerfile 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. # syntax=docker/dockerfile:1.7
  2. # Minimal static-musl image for the d-OAuth-Client BFF + Leptos UI.
  3. #
  4. # Stages: web -> chef -> planner -> builder -> runtime.
  5. # - `web`: builds the Leptos CSR/WASM bundle with a pinned, prebuilt Trunk
  6. # (which fetches its pinned wasm-bindgen/tailwindcss tools), producing
  7. # `/app/crates/web/dist`.
  8. # - `chef`: pinned rust toolchain + cargo-chef + musl targets.
  9. # - `planner`: captures the dependency recipe so dependency layers cache.
  10. # - `builder`: cooks dependencies, then builds the locked, fully-static musl
  11. # `server` binary (rustls `ring` provider only; no aws-lc-sys/OpenSSL).
  12. # - `runtime`: pinned-digest distroless `static-debian12:nonroot` (no shell,
  13. # no package manager, CA bundle + nonroot /etc/passwd entry included). It
  14. # carries the BFF binary and the built WASM assets.
  15. #
  16. # BuildKit cache mounts keep the cargo registry/git cache and the target
  17. # directory warm across rebuilds without baking them into any layer.
  18. # syntax=docker/dockerfile:1.7
  19. # Pinned bases (multi-arch index digests; buildx selects per-platform):
  20. # - rust 1.99-bookworm (matches rust-toolchain.toml stable; rustc 1.99.0)
  21. # - distroless static-debian12:nonroot (UID:GID 65532, no shell)
  22. ARG RUST_BASE=docker.io/library/rust:1.99-bookworm@sha256:59037199c44290f2befcdd58dcc540164763fc296950255aaefeef096a1866b0
  23. ARG RUNTIME_BASE=gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab
  24. ARG TRUNK_VERSION=0.21.14
  25. # ---------------------------------------------------------------------------
  26. # web: prebuilt Trunk builds the CSR/WASM UI. Trunk downloads its pinned
  27. # wasm-bindgen (0.2.129) and tailwindcss (4.3.3) tools at build time.
  28. # ---------------------------------------------------------------------------
  29. FROM ${RUST_BASE} AS web
  30. ARG TARGETARCH
  31. ARG TRUNK_VERSION
  32. WORKDIR /app
  33. COPY . .
  34. RUN set -eux; \
  35. rustup target add wasm32-unknown-unknown; \
  36. case "${TARGETARCH}" in \
  37. arm64) trunk_target=aarch64-unknown-linux-musl ;; \
  38. *) trunk_target=x86_64-unknown-linux-musl ;; \
  39. esac; \
  40. curl -fsSL -o /tmp/trunk.tar.gz \
  41. "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-${trunk_target}.tar.gz"; \
  42. tar -xzf /tmp/trunk.tar.gz -C /usr/local/bin; \
  43. rm /tmp/trunk.tar.gz; \
  44. cd crates/web && trunk build --release
  45. FROM ${RUST_BASE} AS chef
  46. WORKDIR /app
  47. # Reproducible builds: fixed epoch (overridden by bake) and no incremental.
  48. ARG SOURCE_DATE_EPOCH=0
  49. ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
  50. CARGO_INCREMENTAL=0 \
  51. CARGO_NET_RETRY=10
  52. RUN cargo install cargo-chef --locked --version 0.1.71 \
  53. && apt-get update \
  54. && apt-get install -y --no-install-recommends musl-tools \
  55. && rm -rf /var/lib/apt/lists/* \
  56. && rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl wasm32-unknown-unknown
  57. FROM chef AS planner
  58. COPY . .
  59. RUN cargo chef prepare --recipe-path recipe.json
  60. FROM chef AS builder
  61. ARG TARGETARCH
  62. COPY --from=planner /app/recipe.json recipe.json
  63. # Cook (compile) third-party dependencies only: this layer reuses the cache
  64. # unless Cargo.toml/Cargo.lock change.
  65. RUN --mount=type=cache,target=/usr/local/cargo/registry \
  66. --mount=type=cache,target=/usr/local/cargo/git \
  67. --mount=type=cache,target=/app/target \
  68. sh -c 'if [ "$TARGETARCH" = "arm64" ]; then export MUSL_TARGET=aarch64-unknown-linux-musl; else export MUSL_TARGET=x86_64-unknown-linux-musl; fi && \
  69. rustup target add "$MUSL_TARGET" && \
  70. cargo chef cook --locked --release --target "$MUSL_TARGET" --recipe-path recipe.json --bin server'
  71. COPY . .
  72. # Locked, offline release build of the server binary only. The target dir is
  73. # a cache mount, so the binary is copied out to /app/dist in the same layer.
  74. RUN --mount=type=cache,target=/usr/local/cargo/registry \
  75. --mount=type=cache,target=/usr/local/cargo/git \
  76. --mount=type=cache,target=/app/target \
  77. sh -c 'if [ "$TARGETARCH" = "arm64" ]; then export MUSL_TARGET=aarch64-unknown-linux-musl; else export MUSL_TARGET=x86_64-unknown-linux-musl; fi && \
  78. rustup target add "$MUSL_TARGET" && \
  79. cargo build --locked --release --target "$MUSL_TARGET" --bin server && \
  80. mkdir -p /app/dist && \
  81. cp "/app/target/$MUSL_TARGET/release/server" /app/dist/server'
  82. FROM ${RUNTIME_BASE} AS runtime
  83. WORKDIR /app
  84. # Static BFF binary plus the release WASM assets it serves from dist_dir.
  85. COPY --from=builder /app/dist/server /app/server
  86. COPY --from=web /app/crates/web/dist /app/crates/web/dist
  87. # Single-origin demo bind; the SPA is served from the committed dist path.
  88. # Secret-bearing config/client.json, config/dpop-key.json and config.json are
  89. # mounted at runtime, never baked in.
  90. ENV SERVER_BIND_ADDR=0.0.0.0:3000 \
  91. SERVER_DIST_DIR=/app/crates/web/dist
  92. # Non-root, exec-form entrypoint (no shell in the image to interpret one).
  93. USER 65532:65532
  94. EXPOSE 3000
  95. # Shell-less self-probe: the `healthcheck` subcommand GETs /healthz over
  96. # loopback and exits 0/1 without starting the HTTP server.
  97. HEALTHCHECK --interval=30s --timeout=3s --start-period=5s CMD ["/app/server","healthcheck"]
  98. ENTRYPOINT ["/app/server"]
  99. LABEL org.opencontainers.image.title="d-OAuth-Client" \
  100. org.opencontainers.image.description="OAuth 2.1 BFF + Leptos WASM demo client (static musl, non-root, no shell)" \
  101. org.opencontainers.image.licenses="MIT OR Apache-2.0" \
  102. org.opencontainers.image.revision="unknown" \
  103. org.opencontainers.image.vendor="d-oauth"