Review draft — Docker support PR #
Scope/limitations: static review only. The pinned /workspace/source snapshot already contains these Docker files, so line numbers below are from that snapshot. I did not run docker build/docker compose because no Docker/network is available in the review VM; runtime behavior is inferred from the files and application source.
1. High: compose uses a root-owned bind mount with a non-root container user #
compose.yml:9-10 uses ./radio-data:/app/data, while Dockerfile:45-47 creates /app/data and then switches to USER radio (UID 10001).
On a normal Linux Docker host, when ./radio-data does not exist, the daemon creates it as root. The container then runs as UID 10001, so it cannot create /app/data/radio.db or /app/data/audio:
- With Dockerfile’s
DATABASE_URL=sqlite:///app/data/radio.db, startup fails opening the DB.
- With the documented quickstart (
.env overriding DATABASE_URL), startup may succeed but audio uploads fail when the app calls tokio::fs::create_dir_all(self.audio_dir) at src/radio/service.rs:1262.
The declared named volume radio-data (compose.yml:12-13) is never referenced. Use - radio-data:/app/data, or if the bind mount is intentional, pre-create/chown ./radio-data and/or set a matching user: in compose.
2. High: env_file overrides the Dockerfile’s absolute DB/audio paths, putting the database outside the volume #
compose.yml:8 loads .env, and the README quickstart says cp .env.example .env (README.md:43-44). That file contains:
DATABASE_URL=sqlite://radio.db (.env.example:16)
AUDIO_DIR=data/audio (.env.example:18)
Runtime env_file values take precedence over image ENV, so they override the Dockerfile defaults at Dockerfile:51-53. With WORKDIR /app, DATABASE_URL=sqlite://radio.db resolves to /app/radio.db, outside /app/data and outside the declared volume. The result is a database that is not persisted across container recreation even though the intent is clearly for /app/data to hold state.
Fix by adding environment: overrides in compose for DATABASE_URL and AUDIO_DIR (which take precedence over env_file), or by changing the Docker README/.env guidance to use absolute paths.
3. Medium: .dockerignore misses .env.local, radio-data, and root node_modules #
.dockerignore:6 excludes only .env. The backend explicitly loads .env.local (src/main.rs:108), so a developer’s .env.local would be sent to the Docker daemon and copied into the backend-builder layer by COPY . . (Dockerfile:22), potentially exposing secrets in build cache/intermediate layers.
Also, compose.yml:10 creates ./radio-data on the host, but radio-data is not in .dockerignore. On subsequent builds that directory (possibly containing the station DB and uploaded audio) is sent as build context and copied into the builder. Root node_modules is likewise not ignored if someone runs npm install at the repo root.
Recommend adding at least:
.env*
radio-data
**/node_modules
*.db-shm
*.db-wal
4. Low: runtime image is missing ffmpeg and yt-dlp #
The runtime installs only ca-certificates and libssl3 (Dockerfile:33-37), but the application shells out to:
ffmpeg in src/loudness.rs:15 and src/tempo.rs:37
yt-dlp in src/routes/upload.rs:59 / :96 and src/metadata.rs:67
Loudness/tempo backfills will only log background warnings, but URL imports and yt-dlp metadata/cover lookup will fail in the container. If Docker is expected to match the non-Docker feature set, install ffmpeg and yt-dlp, or explicitly document these limitations.
5. Low: use npm ci instead of npm install #
Dockerfile:7 runs npm install, but frontend/package-lock.json exists. npm ci would give reproducible installs and fail clearly if the lockfile is out of sync.
6. Low/optional: ports binds on all host interfaces #
compose.yml:6-7 exposes 3000:3000 on 0.0.0.0. If the reverse proxy runs on the same host, binding to 127.0.0.1:3000:3000 is safer and still sufficient.
reviewed by weaver · model: deepseek-v4-pro
automated review; not independently verified.
repository context: original source fork or commit is unavailable; pinned target repository snapshot 2f6042bfa314d0ae4de8c92ce258901410c64fd9 is context, not an exact PR checkout; the immutable submitted patch remains unapplied. target snapshot: 2f6042bfa314d0ae4de8c92ce258901410c64fd9. this is not the original pr checkout; checks against this snapshot are not exact pr verification.