# syntax=docker/dockerfile:1.7 # SPDX-License-Identifier: MIT # # Build layout: # frontend -> compiles the TypeScript UI to static js/css/html # backend -> embeds those assets with go:embed and links the cgo-enabled application # export -> scratch stage holding the binary and OCR data, for `--output` # helpers -> yt-dlp, ffmpeg/ffprobe and deno, the same builds # `make dependencies` fetches for the host # ocr-assets -> downloads the pinned model and native CPU runtime # captcha-test -> tests the Go OCR reader against synthetic challenges # ocr-export -> exports the OCR data for source builds # released-binary -> verified release archive, used by the publishing job # runtime -> the image that actually runs (default target) # # The UI is inside the application binary. OCR runs in-process using the # adjacent ocr/ model and native library. Helpers are what some hosts need beyond # HTTP (see internal/tools); they sit beside the binary, which is the first # place tools.Find looks. # Local builds compile from source; release images reuse the published binary. ARG BINARY_STAGE=backend # --------------------------------------------------------------- frontend # UI, Go and asset downloads run on the builder platform. Only the runtime # package install needs QEMU when producing an image for another architecture. FROM --platform=$BUILDPLATFORM node:24-alpine AS frontend WORKDIR /app/frontend # Dependencies first, so edits to the source do not re-resolve the tree. COPY frontend/package.json frontend/package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci --no-audit --no-fund COPY frontend/ ./ # tsc type-checks in strict mode, then vite emits to ../backend/.../dist. RUN npm run build && ls -la /app/backend/internal/webui/dist # -------------------------------------------------------------- OCR assets FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS ocr-assets RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl unzip \ && rm -rf /var/lib/apt/lists/* COPY helpers/captcha/install.sh /install.sh ARG TARGETOS ARG TARGETARCH RUN --mount=type=cache,target=/root/.cache/heapleach/ocr \ bash /install.sh "${TARGETOS:-linux}/${TARGETARCH:-amd64}" /out/ocr FROM scratch AS ocr-export COPY --from=ocr-assets /out/ / # ---------------------------------------------------------------- backend # glibc matches the bundled runtime; cgo cannot load it from a static musl # executable. Bookworm gives local builds a glibc 2.36 baseline. FROM --platform=$BUILDPLATFORM golang:1.27-bookworm AS backend-source WORKDIR /src COPY backend/go.mod backend/go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY backend/ ./ FROM backend-source AS captcha-test COPY --from=ocr-assets /out/ocr /ocr RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ HEAPLEACH_OCR_DIR=/ocr CGO_ENABLED=1 go test -race -count=1 ./internal/captcha FROM backend-source AS backend # Replace the checked-in placeholder with the real build. RUN rm -rf ./internal/webui/dist COPY --from=frontend /app/backend/internal/webui/dist ./internal/webui/dist ARG VERSION=dev ARG TARGETOS ARG TARGETARCH ARG BUILDARCH RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ set -eu; \ if [ "${TARGETARCH}" != "${BUILDARCH}" ]; then \ case "${TARGETARCH}" in \ arm64) compiler=aarch64-linux-gnu ;; \ amd64) compiler=x86_64-linux-gnu ;; \ *) echo "unsupported target ${TARGETARCH}" >&2; exit 1 ;; \ esac; \ apt-get update; apt-get install -y --no-install-recommends "gcc-${compiler}" "libc6-dev-${TARGETARCH}-cross"; \ export CC="${compiler}-gcc"; \ fi; \ CGO_ENABLED=1 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} \ go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \ -o /out/heapleach ./cmd/heapleach COPY --from=ocr-assets /out/ocr /out/ocr # ----------------------------------------------------------------- export FROM scratch AS export COPY --from=backend /out/ / # ---------------------------------------------------------------- helpers # The upstream builds are linked against glibc, which is why the runtime # below is Debian rather than Alpine: musl cannot load them, and the distro # packages trail yt-dlp by months — long enough for a host to have changed # under it. These are "latest" downloads, so the layer is only as fresh as # the build cache lets it be: `make image` after `docker builder prune`, or # with --no-cache, picks up new releases. FROM --platform=$BUILDPLATFORM debian:trixie-slim AS helpers ARG TARGETARCH RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl xz-utils unzip \ && rm -rf /var/lib/apt/lists/* WORKDIR /out RUN set -eu; \ case "${TARGETARCH:-amd64}" in \ amd64) ytdlp=yt-dlp_linux; ffmpeg=ffmpeg-master-latest-linux64-gpl; deno=deno-x86_64-unknown-linux-gnu ;; \ arm64) ytdlp=yt-dlp_linux_aarch64; ffmpeg=ffmpeg-master-latest-linuxarm64-gpl; deno=deno-aarch64-unknown-linux-gnu ;; \ *) echo "no helper builds for ${TARGETARCH}" >&2; exit 1 ;; \ esac; \ curl -fsSL --retry 3 -o yt-dlp "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${ytdlp}"; \ curl -fsSL --retry 3 "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/${ffmpeg}.tar.xz" \ | tar -xJ --strip-components=2 --wildcards '*/bin/ffmpeg' '*/bin/ffprobe'; \ curl -fsSL --retry 3 -o deno.zip "https://github.com/denoland/deno/releases/latest/download/${deno}.zip"; \ unzip -q deno.zip deno && rm deno.zip; \ chmod 0755 yt-dlp ffmpeg ffprobe deno # --------------------------------------------------------- release binary # Only reached with BINARY_STAGE=released-binary. The release workflow checks # the archives' checksums before placing the two Linux binaries here. FROM scratch AS released-binary ARG TARGETARCH COPY dist/runtime/${TARGETARCH}/ /out/ FROM ${BINARY_STAGE} AS app # ---------------------------------------------------------------- runtime FROM debian:trixie-slim AS runtime # ca-certificates for TLS to the download hosts; tzdata for sane timestamps; # wget for the health check. RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates tzdata wget libstdc++6 \ && rm -rf /var/lib/apt/lists/* \ && useradd --create-home --uid 10001 --home-dir /home/heapleach heapleach \ && mkdir -p /downloads \ && chown -R heapleach:heapleach /downloads COPY --from=helpers /out/ /usr/local/bin/ COPY --from=app /out/ /usr/local/bin/ ENV HEAPLEACH_ADDR=:8080 \ HEAPLEACH_DIR=/downloads \ HEAPLEACH_CONCURRENCY=4 USER heapleach WORKDIR /home/heapleach VOLUME ["/downloads"] EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=3s --retries=3 \ CMD wget -qO- http://127.0.0.1:8080/api/health >/dev/null 2>&1 || exit 1 ENTRYPOINT ["heapleach"]