# Multi-stage build for the FileServer pipeline. # # The runtime image carries only what serving needs: the Julia runtime, a depot # that is already instantiated *and* precompiled (so startup is load-only, no # compilation), exiftool, and optionally github-linguist. Nothing from the build # — no compilers, no package registry, no gem toolchain — survives into it. # # docker build -t file-server . # docker build --build-arg WITH_LINGUIST=false -t file-server . # ~200MB smaller # # github-linguist (stage-4 programming-language detection) is the one heavy # optional dependency: it needs a Ruby toolchain to build rugged. It degrades # gracefully — without it, text files still get natural-language enrichment and # the server logs a warning at startup — so it can be built out. ARG JULIA_VERSION=1.12.6 ARG DEBIAN_RELEASE=bookworm ARG WITH_LINGUIST=true # --------------------------------------------------------------------------- # Stage 1: instantiate and precompile the Julia depot. # --------------------------------------------------------------------------- FROM julia:${JULIA_VERSION}-${DEBIAN_RELEASE} AS julia-depot ENV JULIA_DEPOT_PATH=/opt/julia-depot \ JULIA_PKG_PRECOMPILE_AUTO=0 WORKDIR /app # Manifest-first so a source-only edit doesn't re-resolve or rebuild the whole # dependency tree. FileServer is its own project's root package, so Pkg insists # on a module file being there — a stub satisfies it while the (slow, rarely # invalidated) dependency layer is built; the real source lands below. COPY Project.toml Manifest.toml ./ RUN mkdir -p src \ && printf 'module FileServer\nend\n' > src/FileServer.jl \ && julia --project=/app -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()' # Precompiling FileServer itself means the runtime image loads cached code # instead of compiling Lux/Zygote on first request. COPY src/ ./src/ RUN julia --project=/app -e 'using Pkg; Pkg.precompile()' \ && julia --project=/app -e 'using FileServer' # Everything the depot keeps for *resolving* packages is dead weight once the # Manifest is pinned: the registry, git clones, scratch and log dirs. RUN rm -rf /opt/julia-depot/registries \ /opt/julia-depot/clones \ /opt/julia-depot/scratchspaces \ /opt/julia-depot/logs \ && find /opt/julia-depot/packages -type d \ \( -name test -o -name docs -o -name .git \) -prune -exec rm -rf {} + \ && find /opt/julia-depot/artifacts -type d -name doc -prune -exec rm -rf {} + # --------------------------------------------------------------------------- # Stage 2: build github-linguist into a relocatable GEM_HOME (optional). # # Built against Debian's system Ruby — the same package the runtime installs — # so rugged's native extension is ABI-compatible with the interpreter there. # --------------------------------------------------------------------------- FROM debian:${DEBIAN_RELEASE}-slim AS linguist ARG WITH_LINGUIST ENV GEM_HOME=/opt/gems RUN set -eux; \ mkdir -p "$GEM_HOME"; \ if [ "$WITH_LINGUIST" = "true" ]; then \ apt-get update; \ apt-get install -y --no-install-recommends \ ruby ruby-dev build-essential cmake pkg-config \ libicu-dev zlib1g-dev libssl-dev; \ gem install --no-document github-linguist; \ # Drop sources, docs and test fixtures from the installed gems; only the # built .so and the .rb load path matter at runtime. rm -rf "$GEM_HOME"/cache "$GEM_HOME"/doc; \ find "$GEM_HOME"/gems -type d \( -name test -o -name spec -o -name samples \) \ -prune -exec rm -rf {} +; \ find "$GEM_HOME"/extensions -type d -name tmp -prune -exec rm -rf {} +; \ rm -rf /var/lib/apt/lists/*; \ fi # --------------------------------------------------------------------------- # Stage 3: runtime. # --------------------------------------------------------------------------- FROM debian:${DEBIAN_RELEASE}-slim AS runtime ARG WITH_LINGUIST # exiftool is a hard startup prerequisite (stage-2 enrichment fails fast without # it). Ruby + libicu are only needed to *run* the linguist gem built above. RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ libimage-exiftool-perl ca-certificates curl; \ if [ "$WITH_LINGUIST" = "true" ]; then \ apt-get install -y --no-install-recommends ruby libicu72; \ fi; \ rm -rf /var/lib/apt/lists/* RUN useradd --system --uid 10001 --create-home --home-dir /app app # Depot ownership matters: Julia rewrites a precompile cache it considers stale, # and a read-only depot would turn that into a startup crash instead of a # recompile. --chown does it in the copy, so it costs no extra layer. COPY --from=julia-depot /usr/local/julia /usr/local/julia COPY --from=julia-depot --chown=app:app /opt/julia-depot /opt/julia-depot COPY --from=linguist /opt/gems /opt/gems ENV PATH=/usr/local/julia/bin:/opt/gems/bin:$PATH \ GEM_HOME=/opt/gems \ GEM_PATH=/opt/gems \ JULIA_DEPOT_PATH=/opt/julia-depot \ JULIA_NUM_THREADS=auto \ JULIA_PKG_OFFLINE=true WORKDIR /app COPY Project.toml Manifest.toml ./ COPY src/ ./src/ COPY bin/server.jl bin/cluster_sweep.jl ./bin/ COPY model/classifier.jld2 ./model/ # Every stage directory hangs off /data so one volume covers the whole pipeline. # 0.0.0.0 rather than the 127.0.0.1 default: the port is only reachable from # outside if it is published. ENV FS_HOST=0.0.0.0 \ FS_PORT=8080 \ FS_SPOOL_DIR=/data/spool \ FS_BINARY_DIR=/data/binary \ FS_DONE_DIR=/data/done \ FS_TEXT_DONE_DIR=/data/text_done \ FS_FAILED_DIR=/data/failed \ FS_CLUSTER_DIR=/data/binary \ FS_CLUSTER_CATALOG=/data/catalog.json \ FS_NOMINATED_DIR=/data/nominated \ FS_MODEL_PATH=/app/model/classifier.jld2 RUN mkdir -p /data && chown app:app /data USER app VOLUME ["/data"] EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD curl -fsS "http://127.0.0.1:${FS_PORT}/health" || exit 1 ENTRYPOINT ["julia", "--project=/app", "-t", "auto"] CMD ["bin/server.jl"]