From 20bda7d7f78acb53daedd66143115bd9fa6731a6 Mon Sep 17 00:00:00 2001 From: ChoosenMEME <24540327+ChoosenMEME@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:17:56 +0200 Subject: [PATCH 1/3] Support custom PUID/PGID for flexible user mapping The image hardcoded the suwayomi user (UID/GID 1000:1000) with ownership baked in at build time, so data directories owned by a different host user only worked through the undocumented and fragile "--user" override. Following the pattern established by the LinuxServer.io images, the container now starts as root: a new entrypoint remaps the suwayomi user to the requested PUID/PGID (default 1000:1000, so existing setups are unaffected), hands files with a mismatched owner in /home/suwayomi over to it and then drops privileges before starting the server. Privileges are dropped via setpriv, util-linux' equivalent of gosu/su-exec, which avoids adding a dependency. Only files with a wrong owner are touched so that startups with big libraries stay fast, and with the default PUID/PGID nothing is modified at all, which keeps read-only containers working. Overriding the container user (eg docker run --user) behaves like before: the entrypoint detects that it is not root, skips the remapping and runs the server directly as that user. The entrypoint lives in /usr/local/bin instead of the world-writable /home/suwayomi since it is executed as root. --- Dockerfile | 11 +++++-- scripts/docker_entrypoint.sh | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100755 scripts/docker_entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 442dc92..3a6f701 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,6 +60,8 @@ RUN userdel -r ubuntu && \ COPY scripts/create_server_conf.sh /home/suwayomi/create_server_conf.sh COPY scripts/startup_script.sh /home/suwayomi/startup_script.sh +# the entrypoint runs as root, so keep it outside of the world-writable /home/suwayomi +COPY scripts/docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh ARG TACHIDESK_RELEASE_DOWNLOAD_URL # Copy the app into the container @@ -67,6 +69,7 @@ ARG TACHIDESK_RELEASE_DOWNLOAD_URL # we grant o+rwx because we need to allow non default UIDs (eg via docker run ... --user) # to write to the directory to generate the server.conf RUN curl -s --create-dirs -L $TACHIDESK_RELEASE_DOWNLOAD_URL -o /home/suwayomi/startup/tachidesk_latest.jar && \ + chmod 755 /usr/local/bin/docker_entrypoint.sh && \ chmod 777 -R /home/suwayomi && \ chown -R suwayomi:suwayomi /home/suwayomi @@ -91,10 +94,14 @@ LABEL maintainer="suwayomi" \ ENV HOME=/home/suwayomi WORKDIR /home/suwayomi -USER suwayomi +# No USER here: the container starts as root and docker_entrypoint.sh remaps +# the suwayomi user to PUID/PGID (default 1000:1000), fixes the ownership of +# the data directory and then drops privileges before starting the server. +# Starting the container with an explicit user (eg docker run --user) skips +# the remapping and runs the server directly as that user, like it used to. EXPOSE 4567 -ENTRYPOINT ["tini", "--"] +ENTRYPOINT ["tini", "--", "/usr/local/bin/docker_entrypoint.sh"] CMD ["/home/suwayomi/startup_script.sh"] # vim: set ft=dockerfile: diff --git a/scripts/docker_entrypoint.sh b/scripts/docker_entrypoint.sh new file mode 100755 index 0000000..b26c9bd --- /dev/null +++ b/scripts/docker_entrypoint.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +# The container starts as root so that the suwayomi user can be remapped to the +# UID/GID requested via the PUID/PGID environment variables (following the +# pattern established by the LinuxServer.io images). After remapping and fixing +# the ownership of the data directory, privileges are dropped and the actual +# command runs as the suwayomi user. + +set -e + +# When the container is started with an explicit user (eg "docker run --user" +# or "user:" in a compose file) there are no root privileges to remap with, +# so behave exactly like before PUID/PGID support existed and just run the +# command as that user. +if [ "$(id -u)" -ne 0 ]; then + if [ -n "$PUID" ] || [ -n "$PGID" ]; then + echo "WARNING: ignoring PUID/PGID because the container user was overridden (--user), which prevents remapping" >&2 + fi + exec "$@" +fi + +PUID="${PUID:-1000}" +PGID="${PGID:-1000}" + +case "${PUID}${PGID}" in + *[!0-9]*) + echo "ERROR: PUID and PGID must be non-negative integers, got PUID='${PUID}' PGID='${PGID}'" >&2 + exit 1 + ;; +esac + +# groupmod also updates the primary group of the suwayomi user in /etc/passwd, +# usermod also updates the owner of files inside /home/suwayomi that are owned +# by the old UID. When PUID/PGID match the current IDs (the default), nothing +# is modified, which keeps eg read-only containers working. +if [ "$PGID" -ne "$(id -g suwayomi)" ]; then + groupmod -o -g "$PGID" suwayomi +fi +if [ "$PUID" -ne "$(id -u suwayomi)" ]; then + usermod -o -u "$PUID" suwayomi +fi + +echo "Starting Suwayomi as UID:GID $(id -u suwayomi):$(id -g suwayomi)" + +# Hand anything left that is not owned by the (possibly remapped) user over to +# it, most importantly the mounted data directory. Only files with a wrong +# owner are touched so that startups with big libraries stay fast. A failed +# chown (eg rootless podman with the ID not mapped) is only a warning, since +# the server may still have access through the file mode bits. +find /home/suwayomi \( ! -user suwayomi -o ! -group suwayomi \) -print0 \ + | xargs -0 -r chown -h suwayomi:suwayomi \ + || echo "WARNING: could not fix the ownership of some files in /home/suwayomi" >&2 + +# setpriv is the util-linux equivalent of gosu/su-exec; --init-groups keeps +# the supplementary audio/video group memberships of the suwayomi user. +exec setpriv --reuid=suwayomi --regid=suwayomi --init-groups "$@" From 2b3172db9dbe9d487cb35e30bbb2b80620a30955 Mon Sep 17 00:00:00 2001 From: ChoosenMEME <24540327+ChoosenMEME@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:18:15 +0200 Subject: [PATCH 2/3] Document PUID/PGID in README, compose examples and quadlet Add the new variables to the environment variable table, explain the root-start/privilege-drop mechanics and how forcing the container user from the outside (--user, user:, runAsUser) disables them, and show commented examples in the compose files and the Podman quadlet template, including how PUID/PGID interact with UserNS=keep-id for host users whose UID is not 1000. --- README.md | 18 +++++++++++++++++- docker-compose-postgresql.yml | 5 +++++ docker-compose.yml | 5 +++++ suwayomi-server.container | 7 +++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ef61f69..4f7c49e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:| | [![Build Docker Images](https://github.com/Suwayomi/Suwayomi-Server-docker/actions/workflows/build_container_images.yml/badge.svg)](https://github.com/Suwayomi/Suwayomi-Server-docker/actions/workflows/build_container_images.yml) | [![Latest](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Suwayomi-Server-docker/raw/main/scripts/tachidesk_version.json&label=version&query=$.stable&color=blue)](https://github.com/orgs/suwayomi/packages/container/package/suwayomi-server/) | [![Preview](https://ghcr-badge.egpl.dev/suwayomi/suwayomi-server/latest_tag?color=%231183c3&ignore=preview&label=version&trim=)](https://github.com/orgs/suwayomi/packages/container/package/suwayomi-server) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) | -Run [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) inside docker container as non-root user. The server will be running on http://localhost:4567 open this url in your browser. +Run [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) inside docker container as non-root user (by default UID/GID `1000:1000`, configurable via [PUID/PGID](#running-as-a-different-user-puid--pgid)). The server will be running on http://localhost:4567 open this url in your browser. Docker Releases - https://github.com/Suwayomi/Suwayomi-Server-docker/pkgs/container/suwayomi-server @@ -45,6 +45,8 @@ There are a number of environment variables available to configure Suwayomi: | Variable | Server Default | Description | |:---------------------------------------:|:--------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| | **TZ** | `Etc/UTC` | What time zone the container thinks it is. | +| **PUID** | `1000` | UID to run the server as, see [Running as a different user](#running-as-a-different-user-puid--pgid). Ignored when the container user is overridden (eg `docker run --user`). | +| **PGID** | `1000` | GID to run the server as, see [Running as a different user](#running-as-a-different-user-puid--pgid). Ignored when the container user is overridden (eg `docker run --user`). | | **BIND_IP** | `0.0.0.0` | The interface to listen on, inside the container. You almost never want to change this. | | **BIND_PORT** | `4567` | Which port Suwayomi will listen on | | **SOCKS_PROXY_ENABLED** | `false` | Whether Suwayomi will connect through a SOCKS5 proxy | @@ -127,6 +129,20 @@ There are a number of environment variables available to configure Suwayomi: | **SYNC_INTERVAL** | `0s` | Interval between automatic sync operations in ISO-8601 Duration format. Use `0s` to disable. | +### Running as a different user (PUID / PGID) + +By default the server runs as the built-in `suwayomi` user with UID/GID `1000:1000`. Set the `PUID` and `PGID` environment variables to run it as a different UID/GID, e.g. to match the owner of a bind-mounted data directory: + +```yaml + environment: + - PUID=1050 + - PGID=1050 +``` + +To make this possible the container starts as root: it remaps the `suwayomi` user to the requested IDs, updates the ownership of the data directory to match (only files with a wrong owner are touched, so startups with large libraries stay fast) and then drops privileges - the server itself never runs as root. + +Alternatively, the container user can still be forced from the outside (`docker run --user`, `user:` in a compose file or `runAsUser` in Kubernetes). In that case nothing runs as root at any point, `PUID`/`PGID` are ignored and no ownership fixing takes place - the mounted data directory must then already be writable by that user. + > [!CAUTION] > This docker image is known to occasionally fail to work. This seems to be caused by problems in the download. If the logs simply end with `LD_PRELOAD=/opt/catch_abort.so /home/suwayomi/.local/share/Tachidesk/bin/kcef/libcef.so`, please remove the downloaded image and pull again. If this does not help, open a [new issue](https://github.com/Suwayomi/Suwayomi-Server-docker/issues/new). diff --git a/docker-compose-postgresql.yml b/docker-compose-postgresql.yml index ac9516f..4b8d5d0 100644 --- a/docker-compose-postgresql.yml +++ b/docker-compose-postgresql.yml @@ -2,9 +2,14 @@ services: suwayomi: image: ghcr.io/suwayomi/suwayomi-server:preview + # Forcing the container user disables the PUID/PGID remapping below; the data + # directory must then already be writable by that user. Prefer PUID/PGID. # user: 1000:1000 environment: - TZ=${TZ:-Etc/UTC} # Add a TZ variable to .env to change it + # UID/GID to run the server as; the data directory ownership is updated to match (default 1000:1000) + # - PUID=1000 + # - PGID=1000 - DATABASE_TYPE=POSTGRESQL - DATABASE_URL=postgresql://postgresql:5432/${POSTGRES_DB} - DATABASE_USERNAME=${POSTGRES_USER} diff --git a/docker-compose.yml b/docker-compose.yml index b1ab4c3..eef1966 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,14 @@ services: suwayomi: image: ghcr.io/suwayomi/suwayomi-server:preview + # Forcing the container user disables the PUID/PGID remapping below; the data + # directory must then already be writable by that user. Prefer PUID/PGID. # user: 1000:1000 environment: - TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + # UID/GID to run the server as; the data directory ownership is updated to match (default 1000:1000) + # - PUID=1000 + # - PGID=1000 # Comment these out if you do not use the flaresolverr container at the bottom of this file - FLARESOLVERR_ENABLED=true - FLARESOLVERR_URL=http://flaresolverr:8191 diff --git a/suwayomi-server.container b/suwayomi-server.container index 6274479..87ce4b1 100644 --- a/suwayomi-server.container +++ b/suwayomi-server.container @@ -5,6 +5,13 @@ Environment=TZ=Etc/UTC # Use TZ database name from https://en.wikipedia.org/wiki Image=ghcr.io/suwayomi/suwayomi-server:preview PublishPort=4567:4567 +# Environment=PUID=1000 +# Environment=PGID=1000 +# The container starts as (namespaced) root and drops to PUID/PGID, default 1000:1000. +# With UserNS=keep-id below and a host UID of 1000 the defaults are already correct; +# if your host user has a different UID/GID, set PUID/PGID to it so that the server +# runs as the container UID that keep-id maps to your host user. + UserNS=keep-id # Without UserNS=keep-id, the container's UID 1000 would map to a diffferent host UID which probably doesn't own the bind-mounted directory below From 262422186b33573cb90e68cab1eef13503b20aac Mon Sep 17 00:00:00 2001 From: ChoosenMEME <24540327+ChoosenMEME@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:18:15 +0200 Subject: [PATCH 3/3] Test PUID/PGID remapping in the container workflow Pass PUID/PGID=1050 in the environment variable test and verify that server.conf ends up owned by the remapped UID/GID. Since server.conf is created by the server after the privilege drop, its owner proves the server actually ran as the requested user. --- .github/workflows/container.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 80a938b..9428fc3 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -121,6 +121,8 @@ jobs: mkdir -p ${{ runner.temp }}/tachidesk_env_vars chmod -R 777 ${{ runner.temp }}/tachidesk_env_vars docker run -d -p 127.0.0.1:4568:4567 -v ${{ runner.temp }}/tachidesk_env_vars:/home/suwayomi/.local/share/Tachidesk \ + -e PUID=1050 \ + -e PGID=1050 \ -e BIND_IP=0.0.0.0 \ -e BIND_PORT=4567 \ -e SOCKS_PROXY_ENABLED=false \ @@ -205,6 +207,8 @@ jobs: ${{ env.test_image_tag }} sleep 15 curl -s http://manga:hello123@127.0.0.1:4568/api/v1/settings/about/ && val_env_vars=$(curl -s http://manga:hello123@127.0.0.1:4568/api/v1/settings/about/ | grep -o "Suwayomi-Server" | sort --unique) + # server.conf is created by the server side of the PUID/PGID privilege drop, so its owner shows the UID/GID the server ran as + data_owner=$(docker exec suwayomi_test stat -c '%u:%g' /home/suwayomi/.local/share/Tachidesk/server.conf || echo "docker exec failed") docker stop suwayomi_test docker logs suwayomi_test > ${{ runner.temp }}/tachidesk_env_vars.log docker rm suwayomi_test @@ -222,6 +226,20 @@ jobs: fi exit 1 fi + if [[ $data_owner != "1050:1050" ]]; then + echo "Container logs:" + echo "==============================" + cat ${{ runner.temp }}/tachidesk_env_vars.log + echo "==============================" + echo "PUID/PGID remapping failed: server.conf is owned by '${data_owner}', expected '1050:1050'" + if [[ $DO_UPLOAD == "true" ]]; then + curl \ + -F 'payload_json={"username": "Github", "content": "<@199705443789045771>\nDocker ${{ inputs.tachidesk_release_type }} image dry run failed! 😢 Version - ${{ steps.get_latest_release_metadata.outputs.release_tag }}. [See the full run log](${{ env.this_actions_run_url }})"}' \ + -F "file1=@${{ runner.temp }}/tachidesk_env_vars.log" \ + "https://discord.com/api/webhooks/${{ secrets.DISCORD_TACHIDESK_WEBHOOK_ID }}/${{ secrets.DISCORD_TACHIDESK_TOKEN }}" + fi + exit 1 + fi # Now we build for all the platforms we support here. NB: the amd64 # won't be rebuilt since the local docker daemon has that still cached