# Full-stack web UI image for ps5upload-engine. # Serves the complete React frontend at http://: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.: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 ../ ARG PS5UPLOAD_BASE_URL=/ ENV VITE_BASE_URL=$PS5UPLOAD_BASE_URL 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 ARG PS5UPLOAD_BASE_URL=/ 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 PS5UPLOAD_BASE_URL=$PS5UPLOAD_BASE_URL # 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"]