Implements the core of SPEC.md — the manifest exchange, less audio-tier matching (§3) and federation (§9a), both of which the spec sequences as later work. - §2 Jmanifest format and series bundles - §3 cut matching: exact / runtime / loose tiers - §4 API, less POST /manifests/search - §5 rate limiting; §5a trust model, anonymous bearer tokens - §6 upload validation, all four stages - §7 relational storage, no JSON blob on the write path - §8 Rust + Axum + SQLite, single serialized writer, in-process job queue - §9a content addressing, computed on upload Reconciled against the system spec: - anneal_sec removed, withdrawn upstream by AR-012/AR-013. Presence follows track extent, so a track survives its own gaps and there is nothing to anneal. Its successor extinction_sec and the new gallery_scope are accepted and stored; scope enters the §7 ranking. A manifest still carrying anneal_sec is a hard 400, not silently ignored — it came from a pipeline whose window semantics differ from what this server assumes. - Audio signature: media under 120 s now emits no signature at all, matching scene-actor-extraction IR-007. The earlier §3 draft allowed a shortened window under 150 s, which was the weaker rule — a caller-varying length is the property SR-004 forbids. - UR IDs regularised to UR-nnn; docs/requirements.md registers 32 requirements, each tracing to an SR-nnn or PR-nnn. 189 tests: unit, end-to-end through the real router, and an injection suite covering SQL, JSON, header and Unicode payloads. Writing that suite found two real gaps, both fixed here: compatibility homoglyphs passed the §5a character class, and a one-frame audio signature was accepted on a feature-length item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
3.8 KiB
Docker
90 lines
3.8 KiB
Docker
# 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"]
|