Files
harmony/fleet/harmony-fleet-operator/Dockerfile
Jean-Gabriel Gill-Couture 9be4f63636
All checks were successful
Run Check Script / check (pull_request) Successful in 2m17s
fix(fleet-operator): build + embed Tailwind CSS in the container image
The image shipped empty CSS: build.rs shells out to the tailwindcss v4
CLI and silently falls back to an empty bundle when it's absent — which
it was in the rust:slim builder, so /static/tailwind.css served nothing.

- Dockerfile: install the pinned Tailwind v4 standalone CLI (curl) in the
  builder and set TAILWIND_REQUIRED=1.
- build.rs: when TAILWIND_REQUIRED is set (container/prod), a missing or
  failing CLI is now a hard build error instead of empty CSS; dev builds
  keep the soft fallback for the `serve-web --css-from` workflow. The env
  is a rerun trigger, so the first required build regenerates rather than
  reusing a cache-mounted empty bundle.

Verified: with the CLI on PATH the embedded bundle is ~26 KB; with
TAILWIND_REQUIRED=1 and no CLI the build fails as intended.
2026-06-01 23:26:38 -04:00

85 lines
3.8 KiB
Docker

# syntax=docker/dockerfile:1.7
# Multi-stage container build for harmony-fleet-operator.
#
# Build context is the workspace root (the operator's Cargo.toml has
# `path = "../../harmony"` deps, which only resolve when the whole
# workspace is in scope). Invoke from the repo root:
#
# docker build -f fleet/harmony-fleet-operator/Dockerfile \
# -t hub.nationtech.io/harmony/harmony-fleet-operator:<tag> .
#
# This replaces an earlier single-stage image that copied a host-built
# binary into archlinux:base — the archlinux choice was a glibc-ABI
# workaround for that host-build path. Building inside a pinned Rust
# image lets us use a matched debian-slim runtime instead.
#
# The builder and runtime stages must pin the same Debian release —
# unsuffixed `rust:slim` follows Debian's latest stable (trixie =
# glibc 2.40), so a binary built there fails to start on
# `debian:bookworm-slim` (glibc 2.36) with "GLIBC_2.39 not found".
# Both stages are pinned to bookworm here for a stable, matched ABI.
FROM docker.io/rust:1.94-slim-bookworm AS builder
# pkg-config + libssl-dev cover transitive native-tls/openssl-sys deps
# that surface during link; ca-certificates lets cargo fetch over TLS;
# curl downloads the Tailwind CLI below.
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config \
ca-certificates \
libssl-dev \
curl \
&& rm -rf /var/lib/apt/lists/*
# Tailwind v4 standalone CLI: build.rs shells out to it to generate the
# embedded CSS bundle. Pinned to match the dev host; override with
# --build-arg TAILWIND_VERSION=... TAILWIND_REQUIRED makes build.rs hard
# fail (not silently ship empty CSS) if the CLI is missing — see build.rs.
ARG TAILWIND_VERSION=v4.3.0
RUN curl -fsSL -o /usr/local/bin/tailwindcss \
"https://github.com/tailwindlabs/tailwindcss/releases/download/${TAILWIND_VERSION}/tailwindcss-linux-x64" \
&& chmod +x /usr/local/bin/tailwindcss \
&& tailwindcss --help >/dev/null
ENV TAILWIND_REQUIRED=1
WORKDIR /app
COPY . .
# `web-frontend` bundles the dashboard the operator serves in-process
# alongside the reconcile loop.
#
# BuildKit cache mounts persist the cargo registry + `target/` across
# builds, so an iterate-build-deploy loop recompiles only changed crates
# (seconds) instead of the whole workspace (minutes). The mounts are
# build-time only — the resulting image is identical, and a cold CI
# runner just rebuilds from scratch. The binary is copied out of the
# cache-mounted target within the same RUN, since cache mounts aren't
# part of the produced layer.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/app/target \
cargo build --release --locked -p harmony-fleet-operator --features web-frontend \
&& cp /app/target/release/harmony-fleet-operator /harmony-fleet-operator
FROM docker.io/library/debian:bookworm-slim
# ca-certificates: outbound TLS to the K8s API and (optionally) NATS.
# kube + async-nats use rustls, so no libssl3 needed at runtime.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /harmony-fleet-operator /usr/local/bin/harmony-fleet-operator
# Non-root runtime. Pairs with the Pod's `securityContext.
# runAsNonRoot: true` in the helm chart — k8s admission rejects pods
# with that flag unless either the image declares a non-root USER or
# the Pod pins runAsUser. We deliberately don't pin runAsUser
# (OpenShift's restricted-v2 SCC assigns a namespace-specific UID and
# rejects fixed UIDs); the image's USER is the portable mechanism.
# 65532 is the `nonroot` UID convention used by distroless + many
# security-hardened base images.
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/harmony-fleet-operator"]