Containerize the server: multi-stage build, compose, data volume
The runtime image carries only what serving needs — the Julia runtime, an already-precompiled depot, exiftool, and optionally github-linguist. The package registry, git clones, and the Ruby/C toolchain that builds rugged all stay in earlier stages. Two things shape the build. Dependencies are instantiated and precompiled in a layer keyed only on Project.toml/Manifest.toml, so a src/ edit rebuilds in seconds rather than minutes; a stub src/FileServer.jl satisfies Pkg's root-package check there. And github-linguist, the one heavy optional dependency, is behind WITH_LINGUIST: it degrades gracefully (stage 4 keeps natural-language enrichment and warns), so building it out is a supported ~160MB saving. All stage directories are pointed under /data via the FS_*_DIR variables so one volume holds the whole in-flight working set, and data/ is excluded from the build context. Claude-Session: https://claude.ai/code/session_01KMNwyqLS3ns6uwWHGDGdAQ
This commit is contained in:
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# The pipeline working set (17G here) belongs in a volume, not the image.
|
||||||
|
data/
|
||||||
|
.git/
|
||||||
|
test/
|
||||||
|
# Benchmark / training / calibration harnesses are dev tools, not runtime.
|
||||||
|
bin/bench.jl
|
||||||
|
bin/bench_model.jl
|
||||||
|
bin/bench_stage1.jl
|
||||||
|
bin/bench_stage2.jl
|
||||||
|
bin/cluster_calibrate.jl
|
||||||
|
bin/train.jl
|
||||||
|
bin/send_dir.sh
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
*.md
|
||||||
146
Dockerfile
Normal file
146
Dockerfile
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# 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"]
|
||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
file-server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
# false drops the Ruby toolchain and github-linguist (~160MB smaller).
|
||||||
|
# Stage-4 text files then get natural-language enrichment but no
|
||||||
|
# programming language — the server warns at startup and carries on.
|
||||||
|
WITH_LINGUIST: "true"
|
||||||
|
image: file-server:latest
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
# Every pipeline stage (spool, done, text_done, binary, failed, nominated)
|
||||||
|
# lives under /data, so one volume holds the whole in-flight working set.
|
||||||
|
- fs-data:/data
|
||||||
|
environment:
|
||||||
|
# The four pools each default to nthreads(), which oversubscribes the
|
||||||
|
# threads Julia actually has. Size them for the host instead.
|
||||||
|
FS_WORKERS: "4"
|
||||||
|
FS_KNOWN_WORKERS: "4"
|
||||||
|
FS_UNKNOWN_WORKERS: "2"
|
||||||
|
FS_TEXT_WORKERS: "2"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fs-data:
|
||||||
Reference in New Issue
Block a user