Skip to content
Draft
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
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,23 @@ ENV HOME="/config" \
XDG_DATA_HOME="/config"

# Install runtime dependencies
RUN apk --no-cache add ca-certificates curl tzdata
RUN apk --no-cache add ca-certificates curl tzdata su-exec

WORKDIR /config

# Declare volume for persistent data
VOLUME /config

# Copy binary
# Copy binary and entrypoint
COPY --from=go-builder /app/qui /usr/local/bin/
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 7476

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:7476/health || exit 1

ENTRYPOINT ["/usr/local/bin/qui"]
ENTRYPOINT ["/entrypoint.sh"]
CMD ["serve"]
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ A fast, modern web interface for qBittorrent. Supports managing multiple qBittor
- [Features](#features)
- [Installation](#installation)
- [Docker](#docker)
- [Permissions (PUID/PGID/UMASK)](#permissions-puidpgidumask)
- [Updating](#updating)
- [Configuration](#configuration)
- [Base URL](#base-url-configuration)
Expand Down Expand Up @@ -162,6 +163,50 @@ If the app logs to stdout, check logs via Docker → qui → Logs; if it writes
- If you pinned a specific version tag, edit the repository field to the new tag when you're ready to upgrade
- Restart the container if needed after the image update so the new binary is loaded

### Permissions (PUID/PGID/UMASK)

By default the container runs as root. To run as a specific user and ensure files in `/config` have correct ownership, set both `PUID` and `PGID` environment variables (required together).

When both are set, the entrypoint will:
1. Create a user/group with the specified IDs
2. Recursively `chown -R` the `/config` directory
3. Run qui as that user

This ensures the database, logs, and any directories created by cross-seed hardlink mode inherit the expected ownership.

Optional: `UMASK` controls default permissions for files and directories qui creates (database, logs, backups, hardlink-mode directories). Common values:
- `022` - owner read/write, group/others read-only (typical default)
- `002` - owner and group read/write, others read-only (group-writable)
- `077` - owner only, no group/others access (private)

**Docker Compose:**
```yaml
services:
qui:
image: ghcr.io/autobrr/qui:latest
environment:
PUID: "1000"
PGID: "1000"
UMASK: "002" # optional
volumes:
- ./qui:/config
ports:
- "7476:7476"
```

**Docker Run:**
```bash
docker run -d \
-e PUID=1000 \
-e PGID=1000 \
-e UMASK=002 \
-p 7476:7476 \
-v $(pwd)/config:/config \
ghcr.io/autobrr/qui:latest
```

> **Note:** Using `user:` in compose or `--user` in docker run bypasses the entrypoint's chown and privilege-drop behavior. Use `PUID`/`PGID` instead for proper `/config` ownership handling.

## Updating

qui includes a built-in update command that automatically downloads and installs the latest release:
Expand Down
8 changes: 5 additions & 3 deletions ci.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,23 @@ ENV HOME="/config" \
XDG_DATA_HOME="/config"

# Install runtime dependencies
RUN apk --no-cache add ca-certificates curl tzdata
RUN apk --no-cache add ca-certificates curl tzdata su-exec

WORKDIR /config

# Declare volume for persistent data
VOLUME /config

# Copy binary from build stage
# Copy binary and entrypoint
COPY --from=go-builder /app/qui /usr/local/bin/
COPY --from=go-builder /app/docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 7476

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:7476/health || exit 1

ENTRYPOINT ["/usr/local/bin/qui"]
ENTRYPOINT ["/entrypoint.sh"]
CMD ["serve"]
16 changes: 16 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ services:
volumes:
- ./qui:/config
environment:
# User/Group ID for file ownership (optional, must set both)
# When set, the entrypoint will:
# 1. Create a user/group with the specified IDs
# 2. Recursively chown /config to PUID:PGID
# 3. Run qui as that user
# Leave unset to run as root (container default)
#PUID: "1000"
#PGID: "1000"

# File creation mask (optional)
# Example: 002 for rw-rw-r-- files and rwxrwxr-x dirs
#UMASK: "002"

# WARNING: Using 'user:' in compose bypasses the entrypoint's
# chown and privilege-drop behavior. Use PUID/PGID instead.

# Server configuration
# Default: 0.0.0.0 in containers
#QUI__HOST: "0.0.0.0"
Expand Down
50 changes: 50 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh
set -e

# Apply UMASK if set
if [ -n "$UMASK" ]; then
umask "$UMASK"
fi

# Fail fast if only one of PUID/PGID is set
if { [ -n "$PUID" ] && [ -z "$PGID" ]; } || { [ -z "$PUID" ] && [ -n "$PGID" ]; }; then
echo >&2 "ERROR: PUID and PGID must be set together"
exit 1
fi

# Validate PUID/PGID are numeric
if [ -n "$PUID" ]; then
case "$PUID" in *[!0-9]*|"") echo >&2 "ERROR: PUID must be a numeric uid"; exit 1;; esac
case "$PGID" in *[!0-9]*|"") echo >&2 "ERROR: PGID must be a numeric gid"; exit 1;; esac
fi

# If PUID/PGID are set, run as that user
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
# Create group if GID doesn't exist in /etc/group
if ! grep -q "^[^:]*:[^:]*:${PGID}:" /etc/group; then
addgroup -g "$PGID" qui
fi

# Get group name for this GID
GROUP_NAME=$(awk -F: -v gid="$PGID" '$3 == gid { print $1 }' /etc/group)

# Create user if UID doesn't exist in /etc/passwd
if ! grep -q "^[^:]*:[^:]*:${PUID}:" /etc/passwd; then
adduser -D -H -u "$PUID" -G "$GROUP_NAME" -s /sbin/nologin qui
fi

# Fix ownership of /config (skip if already correct)
mkdir -p /config
current_uid=$(stat -c %u /config 2>/dev/null || echo "")
current_gid=$(stat -c %g /config 2>/dev/null || echo "")
if [ -z "$current_uid" ] || [ -z "$current_gid" ] || \
[ "$current_uid" -ne "$PUID" ] || [ "$current_gid" -ne "$PGID" ]; then
chown -R "$PUID:$PGID" /config
fi

# Drop privileges and exec qui
exec su-exec "$PUID:$PGID" /usr/local/bin/qui "$@"
fi

# No PUID/PGID set, run as current user (root in container)
exec /usr/local/bin/qui "$@"
Loading