# Deploy image for jray-server. # # §8 is explicit that neither Docker nor Compose should be *required* — the # primary artifact is a single static binary plus one database file, and that is # deliberately the lowest-friction thing a hobbyist operator can deploy. This # image is the optional convenience, not the intended path. # # It builds against musl so the runtime stage can be `scratch`: no libc, no shell, # no package manager, nothing to keep patched. rusqlite is built with `bundled` # (SQLite compiled in) and reqwest with rustls rather than OpenSSL, so there is # genuinely nothing left to link against. # # docker build -t jray-server . # docker run --rm -p 8080:8080 -v jray-data:/data \ # -e JRAY_TMDB_API_KEY=... jray-server FROM rust:1.92-alpine AS builder # `musl-dev` for the C toolchain rusqlite's bundled SQLite needs; `file` for the # static-linkage assertion below. RUN apk add --no-cache musl-dev file WORKDIR /build # Dependencies first, in their own layer, so editing source does not re-download # and rebuild the entire tree. COPY Cargo.toml Cargo.lock ./ RUN mkdir -p src \ && echo 'fn main() {}' > src/main.rs \ && echo '' > src/lib.rs \ && cargo build --release --target x86_64-unknown-linux-musl \ && rm -rf src COPY src ./src # `touch` defeats the cargo staleness check that the dummy-source trick above # would otherwise leave in place. RUN touch src/main.rs src/lib.rs \ && cargo build --release --target x86_64-unknown-linux-musl \ && strip target/x86_64-unknown-linux-musl/release/jray-server # Verify the result is genuinely static. A dynamically-linked binary would fail at # runtime on `scratch`, and failing here is far easier to diagnose. # # Asserted with `file`, not `ldd`: the musl target produces a **static-PIE**, and # `ldd` prints the musl loader path for one, so an `ldd`-based check reports a # static binary as dynamic. `file` reports "static-pie linked" and is unambiguous. RUN file target/x86_64-unknown-linux-musl/release/jray-server | tee /tmp/linkage \ && grep -qE 'static-pie linked|statically linked' /tmp/linkage \ || (echo "binary is not statically linked; it will not run on scratch" && exit 1) # Stage the data directory with the runtime uid's ownership. `scratch` has no # shell, so this cannot be done in the final stage — and a bare `VOLUME` there # would be created root-owned, leaving the non-root process unable to create the # database at all. RUN mkdir -p /staged-data && chown 65534:65534 /staged-data # --------------------------------------------------------------------------- FROM scratch COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/jray-server /jray-server # The database lives on a volume; §8 warns that a plain file copy of a live WAL # database is not a valid backup, so back it up with `VACUUM INTO` from the host # rather than by archiving this directory. # # Copied from the builder so it arrives owned by the runtime uid. Docker seeds a # named volume from the image's directory, ownership included, so the server can # create the database on first run. A bind mount is *not* seeded this way — the # host directory keeps its own ownership, so it must be made writable by uid # 65534 (`chown 65534:65534 /path/on/host`). COPY --from=builder --chown=65534:65534 /staged-data /data VOLUME ["/data"] # Non-root. `scratch` has no /etc/passwd, so this is a bare uid — which is all the # kernel needs, and the binary touches nothing outside /data. USER 65534:65534 ENV JRAY_BIND=0.0.0.0:8080 \ JRAY_DB=/data/jray.db EXPOSE 8080 # No HEALTHCHECK: it would need a shell or curl, and `scratch` has neither. # §4 provides `GET /health` (liveness) and `/ready` (database and migrations) for # an orchestrator to probe externally, which is the right place for it. ENTRYPOINT ["/jray-server"]