Files
ps5upload/engine/Dockerfile.webui
T
PhantomPtr 5954c8adde 6.0.0: AVA1 replaces FTX2 — one encrypted connection for everything (#374)
See CHANGELOG.md (6.0.0). Full history of the work: tag ftx2-last and branch history archived in ps5upload-docs.
2026-10-05 00:25:05 -07:00

109 lines
5.4 KiB
Docker

# Full-stack web UI image for ps5upload-engine.
# Serves the complete React frontend at http://<host>:19113 so any browser
# on an allowlisted IP can manage a PS5 — no native client required.
#
# Build (from the repo root, NOT engine/):
# docker build -f engine/Dockerfile.webui -t ps5upload-engine-webui .
#
# Run (Linux server; engine/compose.yaml does the same):
# docker run -d --network host -v ps5upload-data:/data -v /srv/pkgs:/pkgs:ro \
# -e PS5_ADDR=192.168.1.50 \
# -e PS5UPLOAD_ALLOW_IP=192.168.1.0/24 \
# ps5upload-engine-webui
#
# Then open http://192.168.1.<host>:19113 in a browser on the machine whose
# IP you set in PS5UPLOAD_ALLOW_IP.
#
# DOCKER DESKTOP (macOS/Windows): the engine's loopback guard rejects
# connections from Docker's NAT bridge. You MUST set PS5UPLOAD_ALLOW_IP to
# the bridge gateway (usually 172.17.0.1) for port-forwarded connections to
# work. On Linux, prefer --network host instead. See engine/Dockerfile for
# full networking details. Note: on macOS/Windows the Docker VM is NAT'd
# and may not be able to reach the PS5 on your LAN — this is a Docker
# Desktop limitation, not an engine bug.
#
# The image also carries the two PS5 helper ELFs (ps5upload.elf and the DPI
# install daemon). The browser cannot open a socket to the console's loader
# or hold those bytes itself, so without them the web UI can install base
# games but not updates or DLC — the web UI half of #152. Override with
# PS5UPLOAD_PAYLOAD_DIR if you need to pin different ones.
#
# SECURITY: the web UI is UNAUTHENTICATED. Anyone whose IP is in
# PS5UPLOAD_ALLOW_IP gets full control over your PS5. Only add IPs from
# your own trusted LAN. Never expose port 19113 to the internet.
# ── Stage 1: build the React frontend ────────────────────────────────────────
FROM node:26-alpine AS frontend
# The client at its repo-relative place: the FAQ and Changelog screens bundle
# ../FAQ.md and ../CHANGELOG.md from the repo root.
WORKDIR /repo/client
# Copy only the client package files first so npm ci is cached
COPY client/package.json client/package-lock.json ./
RUN npm ci --silent
# Now copy the rest of the client source and build
COPY client/ .
COPY FAQ.md CHANGELOG.md ../
RUN npm run build:vite
# ── Stage 2: build the Rust engine with embedded web UI ──────────────────────
FROM rust:alpine AS builder
# g++ compiles unrar's bundled C++ against musl; musl target links static.
RUN apk add --no-cache musl-dev g++ make
# Keep engine/ and third_party/ at their real repo-relative positions
# rather than flattening engine/ into /build: the workspace patches
# `unrar` to ../third_party/unrar (see third_party/unrar/README.md), and
# flattening puts that path outside the image entirely.
WORKDIR /build
COPY engine/ ./engine/
COPY third_party/ ./third_party/
# See engine/Dockerfile: build.rs embeds these, and the web UI's update /
# DLC install is exactly what breaks without them (the web UI half of #152).
COPY payload/ ./payload/
WORKDIR /build/engine
# Populate the webui/ embed directory from the frontend build output.
# rust-embed will bake every file in this directory into the binary.
COPY --from=frontend /repo/client/dist/ ./crates/ps5upload-engine/webui/
RUN cargo build --release -p ps5upload-engine --features webui
# The runtime image is `scratch`, which has no /tmp and no home: the engine
# could not stage an uploaded package ("could not create upload dir:
# Permission denied"), cache artwork, or remember saved servers and install
# history. Make both here (scratch has no mkdir) and copy them across below.
# /data is world-writable too (sticky, like /tmp) so a `user:` override in
# Compose — common on NAS boxes, e.g. `user: "1000:1000"` (#346) — can still
# write its state there; its owner is the default runtime user.
RUN mkdir -p /rootfs/tmp /rootfs-data/data && chmod 1777 /rootfs/tmp /rootfs-data/data
# ── Stage 3: minimal scratch image ───────────────────────────────────────────
FROM scratch
COPY --from=builder /build/engine/target/release/ps5upload-engine /ps5upload-engine
# /tmp: staging for uploaded packages, archive extraction and the like. It
# is copied as a child of /rootfs because copying a directory applies only
# its contents: `COPY /rootfs/tmp /tmp` left a 0755 root-owned /tmp and the
# upload still failed. /data: the engine's home (HOME=/data puts its state in
# /data/.ps5upload), owned by the runtime user so it is writable.
COPY --from=builder /rootfs/ /
COPY --from=builder --chown=65532:65532 /rootfs-data/ /
ENV HOME=/data TMPDIR=/tmp
# Saved servers (with their encrypted credentials), install history, the
# artwork cache and the backport library live here. Mount a volume on it so
# they survive a container upgrade: `-v ps5upload-data:/data`.
VOLUME ["/data"]
EXPOSE 19113
# Run as an unprivileged user (65532 = nobody in distroless convention).
# scratch has no /etc/passwd, so we use a raw numeric UID — the kernel
# doesn't need a passwd entry to run a process. Port 19113 > 1024 so
# no CAP_NET_BIND_SERVICE needed.
USER 65532:65532
# scratch has no shell or HTTP client, so the engine probes itself:
# `--healthcheck` exits 0 when GET /api/jobs answers 200 (no PS5 needed).
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/ps5upload-engine", "--healthcheck"]
ENTRYPOINT ["/ps5upload-engine"]