# This file must stay buildable by the LEGACY docker builder — no `# syntax=`
# directive, no `COPY --chmod`, no `RUN --mount`. That is not conservatism: on
# the IHLC runners BuildKit resolves base images over HTTPS and the registry
# answers `tls: unrecognized name`, because it is reached as an insecure
# registry that only the daemon's own pull path falls back to HTTP for. Every
# image that builds green there uses the legacy builder. A `# syntax=` pin makes
# it worse again by fetching the frontend from Docker Hub, which those runners
# cannot reach at all — the same wall BASE_REGISTRY exists to get around.
#
# ── Provenance ────────────────────────────────────────────────────────────────
# Adapted copy of the application repository's root `Dockerfile`, living here
# under cicd/ with the rest of the image recipes. Build from the repo root:
#   docker build -f cicd/Dockerfile .
# Context is THIS tree, which is one level flatter than the parent workspace:
# the client directories the parent nests under its working-root directory
# (`meridian-web/`, `meridian-admin-web/`, `meridian-desktop/`) are top-level
# here, so every COPY below is relative to this repo root. It is a copy, like
# the crates under `crates/` — see `MISSING.md § Copies, not links`. Port
# changes in both directions.
#
# meridian-relay/Dockerfile.* still re-tags the upstream image. This file is
# what lets THIS tree build the image itself and push it through the IHLC
# pipeline to $JF_DOCKER_PROJECT_REGISTRY/meridian-relay:$TAG.
# ──────────────────────────────────────────────────────────────────────────────
#
# Relay image for IHLC — built from this tree, tagged :local / :dev / :staging.
#
# Builds the `meridian-relay` binary (Rust 1.95) and the `meridian-web` static bundle
# (pnpm + vite), then assembles them into a small debian-slim runtime with
# `git` available (the relay shells out to git for repo hydrate / receive-pack
# / upload-pack — see crates/meridian-relay/src/api/git).
#
# Multi-arch is handled by running this same Dockerfile on native amd64 and
# native arm64 runners (see cicd/pipeline-*.yml). The Dockerfile itself is
# platform-agnostic; do not add --platform pins.

ARG RUST_VERSION=1.95
ARG NODE_VERSION=24
ARG DEBIAN_VERSION=bookworm

# Optional registry prefix for the three public base images below. Empty default
# = Docker Hub, so a local `docker build -f cicd/Dockerfile .` is unchanged. The
# IHLC runners cannot reach Docker Hub — every other Dockerfile in this tree
# hard-codes `${DOCKER_REPOSITORY}/docker/…` for that reason — so the pipeline
# passes the proxy repo here rather than pinning it into the file:
#   docker build --build-arg BASE_REGISTRY=ironbank.ilab.private:8082/docker/ ...
# Must end with a slash when set.
ARG BASE_REGISTRY=

# Optional extra CA bundle for builds behind a TLS-intercepting corporate proxy
# (e.g. a Cloudflare/Zscaler gateway that re-signs TLS). Empty by default, so
# public CI builds are unaffected. Point it at a PEM file in the build context:
#   docker build --build-arg EXTRA_CA_CERTS=path/to/proxy-ca.pem ...
# Consumed by the network-touching stages below (cargo + pnpm).
ARG EXTRA_CA_CERTS=

# Optional npm registry for builds where the public registry is unreachable or
# policy-blocked (e.g. a corporate mirror / Artifactory). Empty default = public
# npmjs, so public CI builds are unaffected. Consumed by the web-builder stage.
ARG NPM_REGISTRY=

# ─── Stage 1: cargo-chef base ───────────────────────────────────────────────
FROM ${BASE_REGISTRY}rust:${RUST_VERSION}-${DEBIAN_VERSION} AS chef
# Trust an optional corporate-proxy CA before any network fetch (no-op if unset).
ARG EXTRA_CA_CERTS
COPY ${EXTRA_CA_CERTS:-rust-toolchain.toml} /tmp/extra-ca/src
RUN if [ -n "${EXTRA_CA_CERTS}" ]; then \
        cp /tmp/extra-ca/src /usr/local/share/ca-certificates/extra-proxy-ca.crt \
        && chmod 0644 /usr/local/share/ca-certificates/extra-proxy-ca.crt \
        && update-ca-certificates \
        && echo "CARGO_HTTP_CAINFO=/etc/ssl/certs/ca-certificates.crt" >> /etc/environment; \
    fi
ENV CARGO_HTTP_CAINFO=/etc/ssl/certs/ca-certificates.crt
RUN cargo install cargo-chef --locked --version 0.1.71
WORKDIR /build

# ─── Stage 2: plan dependency graph ─────────────────────────────────────────
# Only the manifests are needed to compute the recipe; this layer rebuilds
# only when Cargo.{toml,lock} or crate manifests change, not on every source
# edit.
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# ─── Stage 3: cook dependencies, then build the binary ──────────────────────
FROM chef AS builder
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        build-essential \
        pkg-config \
        libssl-dev \
        ca-certificates \
        git \
    && rm -rf /var/lib/apt/lists/*
# Keep enough DWARF for native profilers to resolve optimized code to source
# locations. The normal runtime strips it below; runtime-debug retains it.
ENV CARGO_PROFILE_RELEASE_DEBUG=line-tables-only
COPY --from=planner /build/recipe.json recipe.json
# Cook the full workspace recipe — relay deps include workspace siblings, so
# scoping to -p meridian-relay misses transitive deps and re-builds them later.
#
# The feature list here MUST match the one on `cargo build` below. Measured
# with `cargo tree -e normal,build` on this lockfile: the feature takes the
# workspace graph this layer cooks from 565 to 642 packages (+77), and the
# three-package selection built below from 505 to 596 (+91). Cook without it
# and every one of those compiles in the source layer instead, where any edit
# under crates/ invalidates them. The failure is a build that got slow, not a
# build that broke, so it is the kind that survives review — hence both lines,
# and this note between them.
RUN cargo chef cook --release --recipe-path recipe.json --features meridian-relay/zenoh
COPY . .
# `--features meridian-relay/zenoh` is load-bearing, not an optimisation.
# `MERIDIAN_BUS=zenoh|shadow` is a HARD startup error in a relay built without
# it — see `bus_backend_from_str` in crates/meridian-relay/src/config.rs, whose
# `#[cfg(not(feature = "zenoh"))]` arm answers ConfigError::InvalidValue rather
# than falling back to Redis. Shipping a deploy artifact that names a variable
# this binary cannot read is a guaranteed crash loop, so the image carries the
# adapter even while `MERIDIAN_BUS` defaults to `redis`.
#
# Package-qualified because three packages are selected here; a bare
# `--features zenoh` does not name which one it belongs to.
RUN cargo build --release --locked -p meridian-relay --bin meridian-relay \
                                   -p meridian-admin --bin meridian-admin \
                                   -p meridian-pair-relay --bin meridian-pair-relay \
                                   --features meridian-relay/zenoh

# Derive the normal release binaries from the same optimized ELF files as the
# debug image so the two variants cannot drift at code-generation time.
FROM builder AS stripped-binaries
RUN strip target/release/meridian-relay \
    && strip target/release/meridian-admin \
    && strip target/release/meridian-pair-relay

# ─── Stage 4: web bundle (pnpm + vite) ──────────────────────────────────────
# Independent of the Rust layers so a CSS change doesn't bust Rust cache and
# vice versa.
FROM ${BASE_REGISTRY}node:${NODE_VERSION}-${DEBIAN_VERSION}-slim AS web-builder
WORKDIR /build
# Trust an optional corporate-proxy CA so corepack + pnpm can fetch over an
# intercepting TLS gateway (no-op if EXTRA_CA_CERTS is unset).
ARG EXTRA_CA_CERTS
COPY ${EXTRA_CA_CERTS:-rust-toolchain.toml} /tmp/extra-ca/src
RUN if [ -n "${EXTRA_CA_CERTS}" ]; then \
        apt-get update && apt-get install -y --no-install-recommends ca-certificates \
        && cp /tmp/extra-ca/src /usr/local/share/ca-certificates/extra-proxy-ca.crt \
        && chmod 0644 /usr/local/share/ca-certificates/extra-proxy-ca.crt \
        && update-ca-certificates \
        && rm -rf /var/lib/apt/lists/*; \
    fi
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# Point npm + corepack at an optional mirror (no-op when NPM_REGISTRY is unset).
# corepack reads COREPACK_NPM_REGISTRY to fetch the pinned pnpm; pnpm/npm read
# the .npmrc registry for dependency installs.
ARG NPM_REGISTRY
ENV COREPACK_NPM_REGISTRY=${NPM_REGISTRY}
# When using a mirror, disable corepack's npmjs signature check: the mirror
# republishes tarballs without the public registry's provenance signatures, so
# strict verification fails ("No compatible signature found"). Only relaxed on
# the mirror path — public builds (NPM_REGISTRY unset) keep strict verification.
RUN if [ -n "${NPM_REGISTRY}" ]; then \
        echo "registry=${NPM_REGISTRY}" > /build/.npmrc \
        && echo "COREPACK_INTEGRITY_KEYS=0" >> /etc/environment; \
    fi
ENV COREPACK_INTEGRITY_KEYS=${NPM_REGISTRY:+0}
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY patches/ patches/
COPY meridian-web/package.json meridian-web/
COPY meridian-admin-web/package.json meridian-admin-web/
RUN pnpm install --frozen-lockfile --filter meridian-web --filter meridian-admin-web
COPY meridian-web/ meridian-web/
COPY meridian-admin-web/ meridian-admin-web/
# meridian-web/ and meridian-admin-web/ map `@brand/*` onto `../meridian-desktop/src/shared/ui/*` (see
# meridian-web/vite.config.ts and meridian-web/tsconfig.json), so this one shared module is a
# build input for the relay image even though the desktop app is not. Without
# it `tsc` fails with TS2307 on `@brand/meridian-globe/mountGlobe`. The module
# imports only itself and react, so this is its whole dependency closure.
# `.dockerignore` re-includes the same path; both are required.
COPY meridian-desktop/src/shared/ui/meridian-globe/ meridian-desktop/src/shared/ui/meridian-globe/
RUN pnpm -C meridian-web build && pnpm -C meridian-admin-web build

# ─── Stage 5: shared runtime ────────────────────────────────────────────────
FROM ${BASE_REGISTRY}debian:${DEBIAN_VERSION}-slim AS runtime-base

# OCI annotations: required for GHCR to auto-link the image to this repo and
# inherit its visibility. org.opencontainers.image.source is the load-bearing
# one — without it GHCR keeps the image private even when the repo is public.
LABEL org.opencontainers.image.title="Meridian" \
      org.opencontainers.image.description="WebSocket relay server for the Meridian communications platform" \
      org.opencontainers.image.source="https://gitlab.ilab.zone/ihlc/teams/mvp-teams/ih-mvp-teams/team-alpha/meridian/meridian" \
      org.opencontainers.image.url="https://gitlab.ilab.zone/ihlc/teams/mvp-teams/ih-mvp-teams/team-alpha/meridian/meridian" \
      org.opencontainers.image.documentation="https://gitlab.ilab.zone/ihlc/teams/mvp-teams/ih-mvp-teams/team-alpha/meridian/meridian" \
      org.opencontainers.image.licenses="Apache-2.0"

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        git \
        openssl \
    && rm -rf /var/lib/apt/lists/* \
    && groupadd --system --gid 1000 meridian \
    && useradd  --system --uid 1000 --gid 1000 --home-dir /var/lib/meridian \
                --create-home --shell /usr/sbin/nologin meridian

COPY --from=web-builder /build/meridian-web/dist                 /srv/meridian/web
COPY --from=web-builder /build/meridian-admin-web/dist           /srv/meridian/admin-web

# The invite landing page is always served from the bundled web UI. Repository
# browser routes require the separate MERIDIAN_SERVE_GIT_WEB_GUI=true opt-in. The
# admin bundle is inert until MERIDIAN_ADMIN_HOST is configured.
ENV MERIDIAN_WEB_DIR=/srv/meridian/web \
    MERIDIAN_ADMIN_WEB_DIR=/srv/meridian/admin-web

# Runtime shape, baked so it cannot drift per environment — the same five values
# meridian-relay/Dockerfile.* baked when this image was a re-tag of an upstream
# build. cicd/docker-compose.yml sets DATABASE_URL, REDIS_URL and the S3 block
# and nothing else, on the stated assumption that these live in the image. Once
# the pipeline started building the relay here instead, that assumption was
# quietly false: the container would come up on the default bind address with
# migrations OFF, and MERIDIAN_AUTO_MIGRATE=false does not fail — it logs
# "Skipping database migrations" and then serves errors against a schema that
# was never applied.
ENV MERIDIAN_BIND_ADDR=0.0.0.0:3000 \
    MERIDIAN_HEALTH_PORT=8080 \
    MERIDIAN_METRICS_PORT=9102 \
    MERIDIAN_GIT_REPO_PATH=/data/git \
    MERIDIAN_AUTO_MIGRATE=true

# 3000: app (WS + REST)  ·  8080: /_liveness, /_readiness  ·  9102: /metrics
EXPOSE 3000 8080 9102

# deploy/compose mounts a volume here; pre-created so it inherits meridian:meridian.
RUN mkdir -p /data/git && chown meridian:meridian /data/git

USER meridian:meridian
WORKDIR /var/lib/meridian

ENTRYPOINT ["/usr/local/bin/meridian-relay"]

# Optimized binaries with line-table debug information for native profiling.
# Published under debug-* tags; runtime behavior otherwise matches the normal
# image exactly.
FROM runtime-base AS runtime-debug
COPY --from=builder /build/target/release/meridian-relay /usr/local/bin/meridian-relay
COPY --from=builder /build/target/release/meridian-admin /usr/local/bin/meridian-admin
COPY --from=builder /build/target/release/meridian-pair-relay /usr/local/bin/meridian-pair-relay

# Keep the stripped runtime as the final/default Dockerfile target so existing
# `docker build .` callers and release tags retain their current behavior.
FROM runtime-base AS runtime
COPY --from=stripped-binaries /build/target/release/meridian-relay /usr/local/bin/meridian-relay
COPY --from=stripped-binaries /build/target/release/meridian-admin /usr/local/bin/meridian-admin
COPY --from=stripped-binaries /build/target/release/meridian-pair-relay /usr/local/bin/meridian-pair-relay
