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/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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bace8dd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + onetagger: + 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:latest + container_name: onetagger + restart: unless-stopped + + # 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 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 + + # 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: 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 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true