# syntax=docker/dockerfile:1.4
# One dev-mesh node: a zc broker and a stand-in worker in one container.
#
# docker compose -f mesh/compose.yaml up -d --build
#
# Build context is the REPO ROOT, not mesh/ -- the build needs Cargo.toml and
# src/. compose.yaml sets `context: ..` with `dockerfile: mesh/Dockerfile`.
#
# Broker and worker share a container on purpose. The broker classifies a worker
# on 127.0.0.1 as local, and local means free; a worker on another node is
# billed. Splitting them into two containers would put every worker on a
# different address from its broker and make all execution remote, which erases
# the free/paid distinction this mesh exists to demonstrate.
# Pinned to bookworm so the builder's glibc matches the runtime stage below.
# `rust:latest` floats to trixie (glibc 2.39) and produces a binary that dies in
# bookworm-slim with `GLIBC_2.39 not found`.
FROM rust:1-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Cargo needs credentials even though this image does NOT use the private dep.
#
# `hooks` is off by default (Cargo.toml: `default = []`), so zakuro-client is
# never compiled or linked here. But cargo resolves the dependency GRAPH before
# it resolves features, and resolving a git dep means fetching it -- measured:
# `cargo build --release --locked` in a clean container still runs
# `git fetch https://github.com/zakuro-ai/zakuro-drive.git` and dies with
# exit 128 when it cannot authenticate. `--locked` does not avoid it.
#
# So: one fetch layer holding the credential, and nothing else. A GitHub token
# arrives as a BuildKit secret and is exported through GIT_CONFIG_* env rather
# than `git config --global`, which would write it into this layer's
# /root/.gitconfig. Secrets are mounted, never committed to a layer, and the
# builder stage is discarded anyway -- only the binary is copied out.
#
# docker build --secret id=gh_token,env=GH_TOKEN ...
COPY Cargo.toml Cargo.lock build.rs ./
COPY .cargo .cargo
RUN --mount=type=secret,id=gh_token \
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.https://x-access-token:$(cat /run/secrets/gh_token)@github.com/zakuro-ai/.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/zakuro-ai/" \
cargo fetch --locked
# Everything from here is --offline: the caches above hold every source cargo
# needs, so no later layer can reach the network and none of them needs the
# token. A missing crate now fails as a build error instead of silently
# resolving something newer than Cargo.lock pins.
RUN mkdir src && echo "fn main() {}" > src/main.rs
# `|| true`: nothing here needs the stub to link. The layer exists to cache the
# dependency compile; build.rs just leaves the git hash out of `zc --version`
# when the context has no .git.
RUN cargo build --release --offline --locked || true
# Real build. Only the zc crate recompiles; the dependency graph above is a
# cached layer. `touch` is required -- the COPY may preserve a source mtime
# older than the stub build, and cargo would consider the binary up to date.
COPY src ./src
RUN touch src/main.rs && cargo build --release --offline --locked
# -------------------------------------------------------------------
FROM debian:bookworm-slim
# python3, NOT python3-minimal. Measured: python3-minimal has no `http` module
# at all (`import http.server` -> ModuleNotFoundError), so the worker died at
# import and every node came up healthy serving zero workers. The stdlib lives
# in libpython3-stdlib, which the full python3 package pulls in.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates python3 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/release/zc /usr/local/bin/zc
COPY mesh/worker.py /usr/local/bin/worker.py
COPY mesh/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/worker.py
# The broker's write-ahead log lives here. It must be writable by the run user:
# the WAL is how a crash mid-job leaves a "reserved but never settled" note for
# recovery, so a read-only path would silently disable crash recovery.
RUN useradd -r -u 1001 -g root -s /sbin/nologin zc \
&& mkdir -p /var/zakuro && chown -R 1001:0 /var/zakuro
USER zc
EXPOSE 9000 3960
# Health is the BROKER's readiness, not the worker's. A node that answers here
# has bound its HTTP port and can be probed by peers; that is what "up" means
# for a mesh member.
HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=6 \
CMD curl -fsS http://localhost:9000/health || exit 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]