From 52276be711494905031bece575128484cb838c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20S=C3=A6ther?= Date: Mon, 11 May 2026 15:39:56 +0000 Subject: [PATCH 1/3] Add Dockerfile and docker-compose for headless deployment Multi-stage build: rust:1-bookworm compiles client (pnpm) and onetagger-cli only (no webkit/gtk deps), debian:bookworm-slim runtime ships libasound2, libssl3, ca-certificates. Container binds 0.0.0.0:36913 with --expose; compose maps 127.0.0.1:36913 for an external reverse proxy. Mounts mirror the host ReadWritePaths policy (/mnt/music/on-hold rw, /mnt/music/main ro). Hardened with read_only, cap_drop ALL, no-new-privileges. Co-Authored-By: Claude Opus 4.7 (1M context) --- .dockerignore | 26 ++++++++++++++++++ Dockerfile | 67 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 39 +++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ecbf79e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +# Build context = the whole repo. Filter junk that bloats it or shouldn't end up in the image. + +# Build outputs (huge, rebuilt inside the container anyway). +target/ +client/node_modules/ +client/dist/ + +# VCS and editor state. +.git/ +.github/ +.idea/ +.vscode/ + +# Local pnpm/npm caches if any leaked into the tree. +.pnpm-store/ + +# Compose-local bind-mount target — never want this baked into the image. +data/ + +# Self-references — the build doesn't need to see them. +Dockerfile +docker-compose.yml +.dockerignore + +# Misc. +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..91fde0b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +# syntax=docker/dockerfile:1.6 + +# ---- Build stage ---------------------------------------------------------- +FROM rust:1-bookworm AS builder + +ARG DEBIAN_FRONTEND=noninteractive + +# Build deps: +# - libasound2-dev: onetagger-player links alsa even when the binary runs in server mode +# - lld: matches .cargo/config.toml's `link-arg=-fuse-ld=lld` for x86_64-unknown-linux-gnu +# - nodejs/npm + pnpm: build the Vue/Quasar client (client/dist) which onetagger-ui embeds via include_dir! +# We skip libwebkit2gtk-4.1-dev: only the `onetagger` GUI crate needs it, and we build onetagger-cli only. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libssl-dev \ + libasound2-dev \ + ca-certificates \ + git \ + lld \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g pnpm@8 + +WORKDIR /src + +# Source comes from this repo (Dockerfile lives at the repo root, alongside Cargo.toml, crates/, client/). +# .dockerignore filters target/, node_modules/, dist/, .git/ so local edits are picked up without bloating context. +COPY . . + +# Client must be built before cargo: onetagger-ui's include_dir!("../../client/dist") runs at compile time. +# client/.gitignore excludes pnpm-lock.yaml, so checkouts never have one — `pnpm install` (not `--frozen-lockfile`). +RUN cd client && pnpm install && pnpm run build + +# -p onetagger-cli skips the GUI binary (webkit/gtk transitive deps). +RUN cargo build --release -p onetagger-cli + + +# ---- Runtime stage -------------------------------------------------------- +FROM debian:bookworm-slim AS runtime + +ARG DEBIAN_FRONTEND=noninteractive + +# Runtime libs: +# - libasound2: dynamic alsa link from onetagger-player +# - libssl3: openssl runtime for HTTPS calls to MusicBrainz/Discogs/Spotify/Beatport/... +# - ca-certificates: trust store for those outbound calls +RUN apt-get update && apt-get install -y --no-install-recommends \ + libasound2 \ + libssl3 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && useradd --create-home --uid 1000 --user-group --shell /usr/sbin/nologin onetagger + +COPY --from=builder /src/target/release/onetagger-cli /usr/local/bin/onetagger-cli + +USER onetagger +WORKDIR /home/onetagger +ENV HOME=/home/onetagger + +# Server binds 0.0.0.0:36913 with --expose; without it the bind is 127.0.0.1 which is +# unreachable from outside the container. +EXPOSE 36913 + +ENTRYPOINT ["onetagger-cli"] +CMD ["server", "--expose"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..efbc0dc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + onetagger: + build: + # Context is this repo. The Dockerfile builds from local source (COPY .), so uncommitted + # edits to the wss/http handling are picked up on the next `docker compose build`. + context: . + dockerfile: Dockerfile + image: onetagger-local:latest + container_name: onetagger + restart: unless-stopped + + # Bind to localhost only. The external Nginx (TLS + Authelia) proxies HTTPS to this, + # same shape as the systemd setup it replaces. OneTagger speaks plain HTTP; the patched + # client picks ws:// vs wss:// from window.location.protocol, so it works behind both. + ports: + - "127.0.0.1:36913:36913" + + volumes: + # OneTagger config (Spotify/Discogs OAuth tokens, custom platform settings, etc). + # Pre-create: `mkdir -p ./data/config && sudo chown -R 1000:1000 ./data/config` + # otherwise compose creates it as root and the container user can't write to it. + - ./data/config:/home/onetagger/.config/onetagger + # Library mounts mirror the host's ReadWritePaths= policy: + # on-hold is where tagging happens (rw), main is browsable but never written (ro). + - /mnt/music/on-hold:/mnt/music/on-hold + - /mnt/music/main:/mnt/music/main:ro + + # Run as host UID:GID so tag writes keep correct ownership on the mounted music tree. + # Change if your host user isn't 1000:1000. + user: "1000:1000" + + # Hardening the user-mode systemd unit couldn't apply (the AGENT.md "move to Docker" rationale). + read_only: true + tmpfs: + - /tmp + cap_drop: + - ALL + security_opt: + - no-new-privileges:true From fdab5160e6f4a6bc28416d0ddf159be4a0fd0910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20S=C3=A6ther?= Date: Mon, 11 May 2026 16:16:12 +0000 Subject: [PATCH 2/3] Document Docker build and run options in README Adds a Docker section after Installing covering build (compose and docker build), run (compose and docker run), and an options reference for port, volumes, user, CLI flags, hardening, and reverse-proxy setup. Describes the ws/wss auto-detection introduced in 4f7d0ca so users know the same image works behind both HTTP and HTTPS proxies. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/README.md b/README.md index 009490b..3f0778f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,126 @@ https://user-images.githubusercontent.com/15169286/193469224-cbf3af71-f6d7-4ecd- You can download latest binaries from [releases](https://github.com/Marekkon5/onetagger/releases) +## Docker + +OneTagger can also run as a headless web service in a container. The image builds `onetagger-cli` only (no GUI / webkit / gtk dependencies) and serves the embedded web UI over plain HTTP on port `36913`. The Vue client picks `ws://` or `wss://` automatically from the page's protocol, so the same image works behind both HTTP and HTTPS reverse proxies. + +### Build the image + +From a checkout of this repository: + +``` +docker compose build +``` + +Or without compose: + +``` +docker build -t onetagger-local . +``` + +Multi-stage build: `rust:1-bookworm` compiles the Vue client (pnpm) and `onetagger-cli`, then `debian:bookworm-slim` is the runtime with `libasound2`, `libssl3`, and `ca-certificates`. Final image is around 210 MB. + +### Run with docker compose + +A `docker-compose.yml` is provided at the repo root. Adjust the volume paths to your music library, then: + +``` +mkdir -p data/config && sudo chown -R 1000:1000 data/config +docker compose up -d +docker compose logs -f +``` + +### Run with docker run + +Equivalent invocation without compose: + +``` +docker run -d \ + --name onetagger \ + --restart unless-stopped \ + -p 127.0.0.1:36913:36913 \ + -v "$(pwd)/data/config:/home/onetagger/.config/onetagger" \ + -v /path/to/your/music:/music \ + --user 1000:1000 \ + --read-only \ + --tmpfs /tmp \ + --cap-drop ALL \ + --security-opt no-new-privileges:true \ + onetagger-local:latest +``` + +### Options reference + +#### Port + +The server listens on TCP `36913` inside the container. The port is a compile-time constant (`onetagger-shared::PORT`) — to expose it on a different host port just remap, e.g. `-p 127.0.0.1:8080:36913`. + +#### Volumes + +| Container path | Purpose | +|---|---| +| `/home/onetagger/.config/onetagger` | OneTagger config: Spotify/Discogs OAuth tokens, custom platform settings, autotagger profiles. Persist this across rebuilds. | +| Any path you mount your music to | Music library. Mount read-write where tag writes are allowed; mount read-only (append `:ro`) for browse-only paths. A common pattern is a writable "staging" path and a read-only "main" path. | + +#### User + +The image creates a `onetagger` user at UID 1000, GID 1000. Run as a different host UID with `--user $(id -u):$(id -g)` so tag writes keep correct ownership on the mounted library. The numeric UID doesn't need to exist in `/etc/passwd` inside the container. + +#### CLI arguments + +The default command is `server --expose`. Override via compose `command:` or by appending args to `docker run`: + +| Flag | Effect | +|---|---| +| `--expose`, `-e` | **Required in the container** — binds the server to `0.0.0.0:36913`. Without it the bind is `127.0.0.1` inside the container, which the host can't reach. | +| `--path `, `-p ` | Initial path shown in the UI's folder picker. e.g. `--path /music`. | +| `--browser`, `-b` | Not useful headless — would try to open a browser inside the container. | + +Example: set the initial UI path via compose + +```yaml +services: + onetagger: + image: onetagger-local:latest + command: ["server", "--expose", "--path", "/music"] +``` + +The other `onetagger-cli` subcommands (`autotagger`, `audiofeatures`, `renamer`, `authorize-spotify`) can be invoked directly against the same image: + +``` +docker run --rm -v /path/to/music:/music onetagger-local:latest autotagger --path /music +``` + +#### Recommended hardening + +The provided `docker-compose.yml` enables these by default: + +| Flag | Effect | +|---|---| +| `--read-only` | Root filesystem mounted read-only. All persistent state lives in the explicit volumes. | +| `--tmpfs /tmp` | Writable scratch space; needed because the root FS is read-only. | +| `--cap-drop ALL` | Drops all Linux capabilities — OneTagger needs none. | +| `--security-opt no-new-privileges:true` | Standard hardening. | + +#### Behind a reverse proxy + +The container speaks plain HTTP. To put it behind HTTPS, terminate TLS at a reverse proxy (Caddy, Nginx, Traefik, etc.) and forward to `127.0.0.1:36913`. WebSocket upgrades on `/ws` must be passed through; the UI's WebSocket scheme is selected automatically from the page's protocol. + +Example Nginx fragment: + +``` +location / { + proxy_pass http://127.0.0.1:36913; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; +} +``` + + ## Credits Bas Curtiz - UI, Idea, Help SongRec (Shazam support) - https://github.com/marin-m/SongRec From 923af4ddd8cc8568ea33dd858a448afff90cd5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20S=C3=A6ther?= Date: Mon, 11 May 2026 16:16:42 +0000 Subject: [PATCH 3/3] Genericize docker-compose.yml defaults Replaces deployment-specific volume paths with ./music as a starter mount and adds an inline example of the rw-staging / ro-main pattern as a comment. Replaces references to a specific reverse proxy stack with neutral guidance. Image tag drops the -local suffix. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index efbc0dc..bace8dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,35 +1,41 @@ services: onetagger: build: - # Context is this repo. The Dockerfile builds from local source (COPY .), so uncommitted - # edits to the wss/http handling are picked up on the next `docker compose build`. + # Builds from local source (COPY . in the Dockerfile), so local edits to the source + # tree are picked up on the next `docker compose build`. context: . dockerfile: Dockerfile - image: onetagger-local:latest + image: onetagger:latest container_name: onetagger restart: unless-stopped - # Bind to localhost only. The external Nginx (TLS + Authelia) proxies HTTPS to this, - # same shape as the systemd setup it replaces. OneTagger speaks plain HTTP; the patched - # client picks ws:// vs wss:// from window.location.protocol, so it works behind both. + # Bind to localhost by default — OneTagger has no built-in authentication. Put a + # reverse proxy in front (Nginx/Caddy/Traefik) for TLS and any auth, or change to + # "36913:36913" to expose directly on the LAN. The web client picks ws:// vs wss:// + # automatically from window.location.protocol, so HTTP and HTTPS proxies both work. ports: - "127.0.0.1:36913:36913" volumes: # OneTagger config (Spotify/Discogs OAuth tokens, custom platform settings, etc). - # Pre-create: `mkdir -p ./data/config && sudo chown -R 1000:1000 ./data/config` - # otherwise compose creates it as root and the container user can't write to it. + # Pre-create with the right ownership so the container user (UID 1000) can write: + # mkdir -p ./data/config && sudo chown -R 1000:1000 ./data/config - ./data/config:/home/onetagger/.config/onetagger - # Library mounts mirror the host's ReadWritePaths= policy: - # on-hold is where tagging happens (rw), main is browsable but never written (ro). - - /mnt/music/on-hold:/mnt/music/on-hold - - /mnt/music/main:/mnt/music/main:ro - # Run as host UID:GID so tag writes keep correct ownership on the mounted music tree. + # Music library. Adjust to point at your collection. OneTagger writes tags in + # place, so anything mounted read-write here will be modified. A common pattern + # is to mount a small writable "staging" path for new files plus the rest of the + # collection read-only, e.g.: + # - ./music/staging:/music/staging + # - /path/to/main/library:/music/main:ro + - ./music:/music + + # Run as host UID:GID so tag writes keep correct ownership on the mounted library. # Change if your host user isn't 1000:1000. user: "1000:1000" - # Hardening the user-mode systemd unit couldn't apply (the AGENT.md "move to Docker" rationale). + # Hardening: root filesystem read-only with /tmp on tmpfs, no Linux capabilities, + # no new privileges. OneTagger doesn't need any of these. read_only: true tmpfs: - /tmp