Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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
67 changes: 67 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`, `-p <path>` | 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
Expand Down
45 changes: 45 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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