From b3d329e5641c7bf6cc52f2c2b8a84cd8645f1ba7 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Thu, 12 Mar 2026 16:21:26 +0100 Subject: [PATCH 01/16] Experiment with starting container as user --- docs/advanced/nixos.md | 4 -- docs/advanced/podman-support.md | 4 -- docs/configuration/options.md | 48 ------------------ flake.nix | 14 ------ src/Dockerfile | 10 ++-- src/build-image.sh | 22 +++++--- src/entrypoint.sh | 89 +++++++++------------------------ src/zwift.sh | 58 ++++++++++----------- 8 files changed, 73 insertions(+), 176 deletions(-) diff --git a/docs/advanced/nixos.md b/docs/advanced/nixos.md index cebc00a6..183e5564 100644 --- a/docs/advanced/nixos.md +++ b/docs/advanced/nixos.md @@ -72,10 +72,6 @@ environment variables in camelCase: wineExperimentalWayland = false; # Networking mode for the container ("bridge" is default) networking = "bridge"; - # User ID for running the container (usually your own UID) - zwiftUid = "1000"; - # Group ID for running the container (usually your own GID) - zwiftGid = "1000"; # GPU/device flags override (Docker: "--gpus=all", Podman/CDI: "--device=nvidia.com/gpu=all") vgaDeviceFlag = "--device=nvidia.com/gpu=all"; # Enable debug output and verbose logging if true diff --git a/docs/advanced/podman-support.md b/docs/advanced/podman-support.md index 5273836f..fce2e61c 100644 --- a/docs/advanced/podman-support.md +++ b/docs/advanced/podman-support.md @@ -15,7 +15,3 @@ From Podman 4.3 this became automatic by providing the Container UID/GID and pod For example if the host uid/gid is 1001/1001 then we need to map the host resources from `/run/user/1001` to the container resource `/run/user/1000` and map the user and group id's the same. This had to be done manually on the host podman start using `--uidmap` and `--gidmap` (not covered here). - -{: .warning } -Using ZWIFT_UID/GID will only work if the user starting podman has access to the `/run/user/$ZWIFT_UID` resources and does -not work the same way as in Docker so is not supported. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index cc0a4bfe..74ee072a 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -79,8 +79,6 @@ These environment variables can be used to alter the execution of the zwift bash | [`ZWIFT_NO_GAMEMODE`](#zwift_no_gamemode) | `0` | If set to `1`, don't run game mode | | [`WINE_EXPERIMENTAL_WAYLAND`](#wine_experimental_wayland) | `0` | If set to `1`, use native Wayland | | [`NETWORKING`](#networking) | `bridge` | Sets the type of container networking to use | -| [`ZWIFT_UID`](#zwift_uid) | `$(id -u)` | Sets the UID that Zwift will run as | -| [`ZWIFT_GID`](#zwift_gid) | `$(id -g)` | Sets the GID that Zwift will run as | | [`VGA_DEVICE_FLAG`](#vga_device_flag) | | Override container GPU/device flags | | [`PRIVILEGED_CONTAINER`](#privileged_container) | `0` | If set to `1`, run the container in privileged mode | @@ -657,52 +655,6 @@ Configure how the container connects to the Internet. --- -### `ZWIFT_UID` - -See also [`ZWIFT_GID`](#zwift_gid). - -Use this option to launch Zwift from a different user id. - -| Item | Description | -|:------------------|:-------------------------| -| Allowed values | number | -| Default value | `$(id -u)` | -| Commandline usage | `ZWIFT_UID="1001" zwift` | -| Config file usage | `ZWIFT_UID="1001"` | - -{: .warning } -> It is strongly discouraged to use a `ZWIFT_UID` that is different from your user uid. If you decide to do so anyway, know -> that: -> -> - It does not work with podman -> - It does not work with Wayland -> - Cats and dogs may start living together - ---- - -### `ZWIFT_GID` - -See also [`ZWIFT_UID`](#zwift_uid). - -Use this option to launch Zwift from a different group id. - -| Item | Description | -|:------------------|:-------------------------| -| Allowed values | number | -| Default value | `$(id -g)` | -| Commandline usage | `ZWIFT_GID="1001" zwift` | -| Config file usage | `ZWIFT_GID="1001"` | - -{: .warning } -> It is strongly discouraged to use a `ZWIFT_GID` that is different from your user gid. If you decide to do so anyway, know -> that: -> -> - It does not work with podman -> - It does not work with Wayland -> - Cats and dogs may start living together - ---- - ### `VGA_DEVICE_FLAG` See also [Prerequisites for NVIDIA graphics cards][nvidia-prerequisites-href]. diff --git a/flake.nix b/flake.nix index 5cec3394..2186e41f 100644 --- a/flake.nix +++ b/flake.nix @@ -30,8 +30,6 @@ zwiftNoGameMode, wineExperimentalWayland, networking, - zwiftUid, - zwiftGid, vgaDeviceFlag, debug, verbosity, @@ -77,8 +75,6 @@ wineExperimentalWayland != "" ) "export WINE_EXPERIMENTAL_WAYLAND=${wineExperimentalWayland}"} ${pkgs.lib.optionalString (networking != "") "export NETWORKING='${networking}'"} - ${pkgs.lib.optionalString (zwiftUid != "") "export ZWIFT_UID='${zwiftUid}'"} - ${pkgs.lib.optionalString (zwiftGid != "") "export ZWIFT_GID='${zwiftGid}'"} ${pkgs.lib.optionalString (debug != "") "export DEBUG=${debug}"} ${pkgs.lib.optionalString (verbosity != "") "export VERBOSITY='${verbosity}'"} ${pkgs.lib.optionalString (vgaDeviceFlag != "") "export VGA_DEVICE_FLAG='${vgaDeviceFlag}'"} @@ -201,14 +197,6 @@ type = str; default = ""; }; - zwiftUid = mkOption { - type = str; - default = ""; - }; - zwiftGid = mkOption { - type = str; - default = ""; - }; vgaDeviceFlag = mkOption { type = str; default = ""; @@ -250,8 +238,6 @@ zwiftScreenshotsDir zwiftOverrideResolution networking - zwiftUid - zwiftGid vgaDeviceFlag verbosity ; diff --git a/src/Dockerfile b/src/Dockerfile index b0ea886d..82c468f1 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -35,7 +35,6 @@ ARG WINETRICKS_VERSION=20240105 # - ca-certificates for wget and curl # - curl used in zwift authentication script # - gamemode for freedesktop screensaver inhibit -# - gosu for invoking scripts in entrypoint # - libegl1 and libgl1 for GL library # - libvulkan1 for vulkan loader library # - procps for pgrep @@ -49,7 +48,6 @@ RUN dpkg --add-architecture i386 \ ca-certificates \ curl \ gamemode \ - gosu \ libegl1 \ libgl1 \ libvulkan1 \ @@ -74,10 +72,10 @@ RUN wget -qO /etc/apt/trusted.gpg.d/winehq.asc https://dl.winehq.org/wine-builds && wget -qO /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/${WINETRICKS_VERSION}/src/winetricks \ && chmod +x /usr/local/bin/winetricks -# Create passwordless user and make nvidia libraries discoverable +# Add user with root privileges and make nvidia libraries discoverable RUN adduser --disabled-password --gecos '' user \ - && adduser user sudo \ - && echo '%SUDO ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ + && usermod -aG sudo user \ + && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ && echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ && echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf @@ -104,4 +102,6 @@ COPY --chmod=755 run_zwift.sh /bin/run_zwift.sh COPY --chmod=755 zwift-auth.sh /bin/zwift-auth COPY --chmod=755 --from=build-runfromprocess /usr/src/target/x86_64-pc-windows-gnu/release/runfromprocess-rs.exe /bin/runfromprocess-rs.exe +USER user + ENTRYPOINT ["entrypoint"] diff --git a/src/build-image.sh b/src/build-image.sh index 03e764f9..4998180f 100755 --- a/src/build-image.sh +++ b/src/build-image.sh @@ -73,9 +73,7 @@ readonly XAUTHORITY="${XAUTHORITY:-}" # Initialize script constants SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" -ZWIFT_UID="${UID}" -ZWIFT_GID="$(id -g)" -readonly SCRIPT_DIR ZWIFT_UID ZWIFT_GID +readonly SCRIPT_DIR # Initialize CONTAINER_TOOL: Use podman if available msgbox info "Looking for container tool" @@ -123,14 +121,22 @@ container_args=( -e VERBOSITY="${VERBOSITY}" -e COLORED_OUTPUT="${COLORED_OUTPUT_SUPPORTED}" -e CONTAINER_TOOL="${CONTAINER_TOOL}" - -e ZWIFT_UID="${ZWIFT_UID}" - -e ZWIFT_GID="${ZWIFT_GID}" ) +# Initialize user ids +host_uid="${UID}" +host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then # Podman maps the local user into the container as uid/gid 1000 (the container's user), # consistent with zwift.sh. Using the host uid/gid here causes a uid mismatch at runtime. - container_args+=(--userns "keep-id:uid=1000,gid=1000") + container_uid=1000 + container_args+=(--userns="keep-id:uid=1000,gid=1000") +else + container_uid="${host_uid}" + container_args+=( + -e HOST_UID="${host_uid}" + -e HOST_GID="${host_gid}" + ) fi # Configure window manager @@ -145,8 +151,8 @@ container_args+=( ) if [[ -n ${XAUTHORITY} ]]; then container_args+=( - -e XAUTHORITY="${XAUTHORITY}" - -v "${XAUTHORITY}:${XAUTHORITY}" + -e XAUTHORITY="${XAUTHORITY//${host_uid}/${container_uid}}" + -v "${XAUTHORITY}:${XAUTHORITY//${host_uid}/${container_uid}}" ) elif command_exists xhost && xhost +local: > /dev/null; then msgbox ok "Container X11 access provided through xhost" diff --git a/src/entrypoint.sh b/src/entrypoint.sh index f278b5ab..b3bc9fa1 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -22,14 +22,12 @@ else fi readonly VERBOSITY="${VERBOSITY:-1}" -readonly ZWIFT_UID="${ZWIFT_UID:-$(id -u user)}" -readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g user)}" +readonly HOST_UID="${HOST_UID:-$(id -u user)}" +readonly HOST_GID="${HOST_GID:-$(id -g user)}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" -readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" -readonly ZWIFT_DOCS="${WINE_USER_HOME}/AppData/Local/Zwift" msgbox() { local type="${1:?}" # Type: info, ok, warning, error, debug @@ -86,43 +84,37 @@ fi declare -a startup_cmd startup_cmd=(/bin/run_zwift.sh) -update_required=0 if is_empty_directory "${ZWIFT_HOME}"; then startup_cmd=(/bin/update_zwift.sh --install) - update_required=1 elif [[ ${1:-} == "--update" ]]; then startup_cmd=(/bin/update_zwift.sh) - update_required=1 fi -###################################### -##### Change ownership if needed ##### +################################################# +##### Add container user to host user group ##### if [[ ${CONTAINER_TOOL} == "docker" ]]; then - # with docker the container is launched as root - # here we update ids and ownership so zwift can be launched as user instead + # docker does not support remapping container user to host user out of the box - user_uid="$(id -u user)" - user_gid="$(id -g user)" + container_uid="$(id -u user)" + container_gid="$(id -g user)" should_change_user_ids() { - # ids should be updated if ZWIFT_UID:ZWIFT_GID is different from from user uid:gid + # ids should be updated if HOST_UID:HOST_GID is different from from user uid:gid # returns 0 if ids should be changed, 1 if not, so it can be used in an if local result=1 - if [[ ! ${ZWIFT_UID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring ZWIFT_UID '${ZWIFT_UID}' because it is not a number" - elif [[ ${user_uid} -ne ${ZWIFT_UID} ]]; then - user_uid="${ZWIFT_UID}" + if [[ ! ${HOST_UID} =~ ^[0-9]+$ ]]; then + msgbox warning "Ignoring HOST_UID '${HOST_UID}' because it is not a number" + elif [[ ${container_uid} -ne ${HOST_UID} ]]; then result=0 fi - if [[ ! ${ZWIFT_GID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring ZWIFT_GID '${ZWIFT_GID}' because it is not a number" - elif [[ ${user_gid} -ne ${ZWIFT_GID} ]]; then - user_gid="${ZWIFT_GID}" + if [[ ! ${HOST_GID} =~ ^[0-9]+$ ]]; then + msgbox warning "Ignoring HOST_GID '${HOST_GID}' because it is not a number" + elif [[ ${container_gid} -ne ${HOST_GID} ]]; then result=0 fi @@ -130,41 +122,15 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then } change_user_ids() { - usermod -ou "${user_uid}" user || return 1 - groupmod -og "${user_gid}" user || return 1 - mkdir -p "/run/user/${user_uid}" || return 1 - chown -R user:user "/run/user/${user_uid}" || return 1 - sed -i "s|/run/user/1000|/run/user/${user_uid}|g" /etc/pulse/client.conf || return 1 - } - - ownership_needs_update() { - # Quick check: if the top-level directory is already owned by user:user, assume everything is fine - # This avoids a costly recursive find on every normal startup - local target="${1:?}" - local result - [[ -d ${target} ]] && result="$(find "${target}" -maxdepth 0 \( ! -user user -o ! -group user \) -print 2> /dev/null)" && [[ -n ${result} ]] - } - - update_ownership() { - local target - if [[ ${update_required} -eq 1 ]]; then - target="/home/user" - else - target="${ZWIFT_DOCS}" - fi - - if ! ownership_needs_update "${target}"; then - msgbox ok "Ownership already correct, skipping" - return 0 - fi - - # Only chown files that actually need it, rather than blindly recursing everything - msgbox info "Updating ownership of files in ${target} (this may take a while on first run)..." - find "${target}" \( ! -user user -o ! -group user \) -exec chown user:user {} + || return 1 + sudo usermod -ou "${HOST_UID}" user || return 1 + sudo groupmod -og "${HOST_GID}" user || return 1 + sudo mkdir -p "/run/user/${HOST_UID}" || return 1 + sudo chown -R user:user "/run/user/${HOST_UID}" || return 1 + sudo sed -i "s|/run/user/1000|/run/user/${user_uid}|g" /etc/pulse/client.conf || return 1 } if should_change_user_ids; then - msgbox info "Changing user ids to ${user_uid}:${user_gid}" + msgbox info "Changing user ids to ${HOST_UID}:${HOST_GID}" if change_user_ids; then msgbox ok "Changed user ids" else @@ -172,19 +138,14 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then exit 1 fi fi - - msgbox info "Checking file ownership" - if update_ownership; then - msgbox ok "File ownership is correct" - else - msgbox error "Failed to update file ownership" - exit 1 - fi - - startup_cmd=(gosu user:user "${startup_cmd[@]}") fi ######################################### ##### Launch update or start script ##### +actual_user="$(whoami)" +actual_uid="$(id -u "${actual_user}")" +actual_gid="$(id -g "${actual_user}")" +msgbox debug "Running as ${actual_user} (uid=${actual_uid}, gid=${actual_gid})" + "${startup_cmd[@]}" diff --git a/src/zwift.sh b/src/zwift.sh index 8a4ad6ab..499f14a9 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -175,11 +175,18 @@ readonly ZWIFT_FG="${ZWIFT_FG:-0}" readonly ZWIFT_NO_GAMEMODE="${ZWIFT_NO_GAMEMODE:-0}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly NETWORKING="${NETWORKING:-bridge}" -readonly ZWIFT_UID="${ZWIFT_UID:-${UID}}" -readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g)}" readonly VGA_DEVICE_FLAG="${VGA_DEVICE_FLAG:-}" readonly PRIVILEGED_CONTAINER="${PRIVILEGED_CONTAINER:-0}" +# No longer supported configuration environment variables +readonly ZWIFT_UID="${ZWIFT_UID:-}" +readonly ZWIFT_GID="${ZWIFT_GID:-}" +if [[ -n ${ZWIFT_UID} ]] || [[ -n ${ZWIFT_GID} ]]; then + msgbox error "ZWIFT_UID and ZWIFT_GID options are no longer supported" + msgbox error " To start Zwift as a different user, use: sudo -i username zwift" + exit 1 +fi + # Initialize CONTAINER_TOOL: Use podman if available msgbox info "Looking for container tool" CONTAINER_TOOL="${CONTAINER_TOOL:-}" @@ -206,8 +213,8 @@ declare -a parameters_to_print parameters_to_print=( DEBUG VERBOSITY CONTAINER_TOOL IMAGE VERSION SCRIPT_VERSION DONT_CHECK DONT_PULL DONT_CLEAN DRYRUN INTERACTIVE CONTAINER_EXTRA_ARGS ZWIFT_USERNAME ZWIFT_PASSWORD ZWIFT_WORKOUT_DIR ZWIFT_ACTIVITY_DIR ZWIFT_LOG_DIR ZWIFT_SCREENSHOTS_DIR - ZWIFT_OVERRIDE_GRAPHICS ZWIFT_OVERRIDE_RESOLUTION ZWIFT_FG ZWIFT_NO_GAMEMODE WINE_EXPERIMENTAL_WAYLAND NETWORKING ZWIFT_UID - ZWIFT_GID VGA_DEVICE_FLAG PRIVILEGED_CONTAINER DBUS_SESSION_BUS_ADDRESS DISPLAY WAYLAND_DISPLAY XAUTHORITY XDG_RUNTIME_DIR + ZWIFT_OVERRIDE_GRAPHICS ZWIFT_OVERRIDE_RESOLUTION ZWIFT_FG ZWIFT_NO_GAMEMODE WINE_EXPERIMENTAL_WAYLAND NETWORKING + VGA_DEVICE_FLAG PRIVILEGED_CONTAINER DBUS_SESSION_BUS_ADDRESS DISPLAY WAYLAND_DISPLAY XAUTHORITY XDG_RUNTIME_DIR ) for parameter_to_print in "${parameters_to_print[@]}"; do parameter_print_value="$(declare -p "${parameter_to_print}")" @@ -342,26 +349,24 @@ container_args=() declare -a entrypoint_args entrypoint_args=() +# Initialize user ids +host_uid="${UID}" +host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then - # Podman has to use container id 1000 - # Local user is mapped to the container id - local_uid="${ZWIFT_UID}" container_uid=1000 - container_gid=1000 - container_args+=(--userns "keep-id:uid=${container_uid},gid=${container_gid}") + container_args+=(--userns="keep-id:uid=1000,gid=1000") else - # Docker will run as the id's provided. - local_uid="${UID}" - container_uid="${ZWIFT_UID}" - container_gid="${ZWIFT_GID}" + container_uid="${host_uid}" + container_env_vars+=( + HOST_UID="${host_uid}" + HOST_GID="${host_gid}" + ) fi # Define base container environment variables container_env_vars+=( DEBUG="${DEBUG}" VERBOSITY="${VERBOSITY}" - ZWIFT_UID="${container_uid}" - ZWIFT_GID="${container_gid}" CONTAINER_TOOL="${CONTAINER_TOOL}" ) @@ -587,18 +592,13 @@ fi if [[ ${window_manager} == "Wayland" ]]; then msgbox info "Using Wayland window manager" - if [[ ${ZWIFT_UID} -ne ${UID} ]]; then - msgbox error "Wayland does not support ZWIFT_UID different to your id of ${UID}" - exit 1 - fi - if [[ -n ${XDG_RUNTIME_DIR} ]] && [[ -n ${WAYLAND_DISPLAY} ]]; then container_env_vars+=( - XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR//${local_uid}/${container_uid}}" + XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR//${host_uid}/${container_uid}}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}" WINE_EXPERIMENTAL_WAYLAND="1" ) - container_args+=(-v "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}:${XDG_RUNTIME_DIR//${local_uid}/${container_uid}}/${WAYLAND_DISPLAY}") + container_args+=(-v "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}:${XDG_RUNTIME_DIR//${host_uid}/${container_uid}}/${WAYLAND_DISPLAY}") else msgbox error "Required environment variables XDG_RUNTIME_DIR and/or WAYLAND_DISPLAY are not set" msgbox error "Falling back to XWayland" 5 @@ -625,8 +625,8 @@ if [[ ${window_manager} == "XWayland" ]] || [[ ${window_manager} == "XOrg" ]]; t fi if [[ -n ${XAUTHORITY} ]]; then - container_env_vars+=(XAUTHORITY="${XAUTHORITY//${local_uid}/${container_uid}}") - container_args+=(-v "${XAUTHORITY}:${XAUTHORITY//${local_uid}/${container_uid}}") + container_env_vars+=(XAUTHORITY="${XAUTHORITY//${host_uid}/${container_uid}}") + container_args+=(-v "${XAUTHORITY}:${XAUTHORITY//${host_uid}/${container_uid}}") else msgbox info "XAUTHORITY environment variable not set, container access to X11 needs to be granted with xhost" xhost_access_required=1 @@ -641,17 +641,17 @@ if [[ -n ${DBUS_SESSION_BUS_ADDRESS} ]]; then [[ ${DBUS_SESSION_BUS_ADDRESS} =~ ^unix:path=([^,]+) ]] dbus_unix_socket=${BASH_REMATCH[1]} if [[ -n ${dbus_unix_socket} ]]; then - container_env_vars+=(DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS//${local_uid}/${container_uid}}") - container_args+=(-v "${dbus_unix_socket}:${dbus_unix_socket//${local_uid}/${container_uid}}") + container_env_vars+=(DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS//${host_uid}/${container_uid}}") + container_args+=(-v "${dbus_unix_socket}:${dbus_unix_socket//${host_uid}/${container_uid}}") fi fi # Configure sound driver container_env_vars+=(PULSE_SERVER="/run/user/${container_uid}/pulse/native") -if [[ -d "/run/user/${local_uid}/pulse" ]]; then - container_args+=(-v "/run/user/${local_uid}/pulse:/run/user/${container_uid}/pulse") +if [[ -d "/run/user/${host_uid}/pulse" ]]; then + container_args+=(-v "/run/user/${host_uid}/pulse:/run/user/${container_uid}/pulse") else - msgbox warning "PulseAudio socket /run/user/${local_uid}/pulse not found — audio may not work (PipeWire-only system?)" + msgbox warning "PulseAudio socket /run/user/${host_uid}/pulse not found — audio may not work (PipeWire-only system?)" fi # Check for proprietary nvidia driver and set correct device to use (respects existing VGA_DEVICE_FLAG) From c80f3c7c0eda376754d29cd7aee6f4f6348b066a Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sat, 14 Mar 2026 21:18:08 +0100 Subject: [PATCH 02/16] Remap user id and tag image --- src/Dockerfile | 15 +++---- src/build-image.sh | 5 --- src/entrypoint.sh | 100 ++++++++++++++++++++++++++++----------------- src/zwift.sh | 51 +++++++++++++++++++++-- 4 files changed, 116 insertions(+), 55 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index 82c468f1..0862e6cc 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -38,7 +38,6 @@ ARG WINETRICKS_VERSION=20240105 # - libegl1 and libgl1 for GL library # - libvulkan1 for vulkan loader library # - procps for pgrep -# - sudo for normal user installation # - wget for downloading winehq key # - winbind for ntml_auth required by zwift/wine # - xdg-utils seems to be a dependency of wayland @@ -52,7 +51,6 @@ RUN dpkg --add-architecture i386 \ libgl1 \ libvulkan1 \ procps \ - sudo \ wget \ winbind \ xdg-utils \ @@ -72,11 +70,8 @@ RUN wget -qO /etc/apt/trusted.gpg.d/winehq.asc https://dl.winehq.org/wine-builds && wget -qO /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/${WINETRICKS_VERSION}/src/winetricks \ && chmod +x /usr/local/bin/winetricks -# Add user with root privileges and make nvidia libraries discoverable -RUN adduser --disabled-password --gecos '' user \ - && usermod -aG sudo user \ - && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ - && echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ +# Make nvidia libraries discoverable +RUN echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ && echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf # Required for non-glvnd setups @@ -85,6 +80,10 @@ ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu:/usr/local # Configure audio driver COPY pulse-client.conf /etc/pulse/client.conf +# Run as a regular user instead of root +RUN adduser --disabled-password --gecos '' user +USER user + FROM wine-base ENV NVIDIA_VISIBLE_DEVICES=all @@ -102,6 +101,4 @@ COPY --chmod=755 run_zwift.sh /bin/run_zwift.sh COPY --chmod=755 zwift-auth.sh /bin/zwift-auth COPY --chmod=755 --from=build-runfromprocess /usr/src/target/x86_64-pc-windows-gnu/release/runfromprocess-rs.exe /bin/runfromprocess-rs.exe -USER user - ENTRYPOINT ["entrypoint"] diff --git a/src/build-image.sh b/src/build-image.sh index 4998180f..7d7acff3 100755 --- a/src/build-image.sh +++ b/src/build-image.sh @@ -125,7 +125,6 @@ container_args=( # Initialize user ids host_uid="${UID}" -host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then # Podman maps the local user into the container as uid/gid 1000 (the container's user), # consistent with zwift.sh. Using the host uid/gid here causes a uid mismatch at runtime. @@ -133,10 +132,6 @@ if [[ ${CONTAINER_TOOL} == "podman" ]]; then container_args+=(--userns="keep-id:uid=1000,gid=1000") else container_uid="${host_uid}" - container_args+=( - -e HOST_UID="${host_uid}" - -e HOST_GID="${host_gid}" - ) fi # Configure window manager diff --git a/src/entrypoint.sh b/src/entrypoint.sh index b3bc9fa1..4d03102e 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -54,6 +54,10 @@ msgbox() { esac } +is_user_root() { + [[ ${EUID} -eq 0 ]] +} + is_empty_directory() { local directory="${1:?}" if [[ ! -d ${directory} ]]; then @@ -64,38 +68,18 @@ is_empty_directory() { ! contents="$(ls -A "${directory}" 2> /dev/null)" || [[ -z ${contents} ]] } -########################### -##### Configure Zwift ##### - -if ! mkdir -p "${ZWIFT_HOME}" || ! cd "${ZWIFT_HOME}"; then - msgbox error "Zwift home directory '${ZWIFT_HOME}' does not exist or is not accessible!" - exit 1 -fi - -# If Wayland Experimental need to blank DISPLAY here to enable Wayland. -# NOTE: DISPLAY must be unset here before run_zwift to work -# Registry entries are set in the container install or won't work. -if [[ ${WINE_EXPERIMENTAL_WAYLAND} -eq 1 ]]; then - unset DISPLAY -fi - -############################################ -##### Clean install, update or launch? ##### - -declare -a startup_cmd -startup_cmd=(/bin/run_zwift.sh) - -if is_empty_directory "${ZWIFT_HOME}"; then - startup_cmd=(/bin/update_zwift.sh --install) -elif [[ ${1:-} == "--update" ]]; then - startup_cmd=(/bin/update_zwift.sh) -fi +remap_user() { + # docker does not support remapping container user to host user out of the box -################################################# -##### Add container user to host user group ##### + if ! is_user_root; then + msgbox error "Must be root to remap user" + return 1 + fi -if [[ ${CONTAINER_TOOL} == "docker" ]]; then - # docker does not support remapping container user to host user out of the box + if [[ ${CONTAINER_TOOL} != "docker" ]]; then + msgbox error "User should only be remapped when using docker" + return 1 + fi container_uid="$(id -u user)" container_gid="$(id -g user)" @@ -122,11 +106,11 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then } change_user_ids() { - sudo usermod -ou "${HOST_UID}" user || return 1 - sudo groupmod -og "${HOST_GID}" user || return 1 - sudo mkdir -p "/run/user/${HOST_UID}" || return 1 - sudo chown -R user:user "/run/user/${HOST_UID}" || return 1 - sudo sed -i "s|/run/user/1000|/run/user/${user_uid}|g" /etc/pulse/client.conf || return 1 + usermod -ou "${HOST_UID}" user || return 1 + groupmod -og "${HOST_GID}" user || return 1 + mkdir -p "/run/user/${HOST_UID}" || return 1 + chown -R user:user "/run/user/${HOST_UID}" || return 1 + sed -i "s|/run/user/1000|/run/user/${HOST_UID}|g" /etc/pulse/client.conf || return 1 } if should_change_user_ids; then @@ -135,10 +119,12 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then msgbox ok "Changed user ids" else msgbox error "Failed to change user ids" - exit 1 + return 1 fi + else + msgbox info "Nothing to do, user ids are already ${HOST_UID}:${HOST_GID}" fi -fi +} ######################################### ##### Launch update or start script ##### @@ -148,4 +134,44 @@ actual_uid="$(id -u "${actual_user}")" actual_gid="$(id -g "${actual_user}")" msgbox debug "Running as ${actual_user} (uid=${actual_uid}, gid=${actual_gid})" +if [[ ${1:-} == "--remap-user" ]]; then + msgbox info "Remapping container user to host user" + if remap_user; then + msgbox ok "Remapped container user to host user" + exit 0 + else + msgbox error "Failed to remap container user to host user" + exit 1 + fi +fi + +msgbox info "Starting or installing Zwift" + +if is_user_root; then + msgbox error "Cannot run or install Zwift as root!" + exit 1 +fi + +if ! mkdir -p "${ZWIFT_HOME}" || ! cd "${ZWIFT_HOME}"; then + msgbox error "Zwift home directory '${ZWIFT_HOME}' does not exist or is not accessible!" + exit 1 +fi + +# If Wayland Experimental need to blank DISPLAY here to enable Wayland. +# NOTE: DISPLAY must be unset here before run_zwift to work +# Registry entries are set in the container install or won't work. +if [[ ${WINE_EXPERIMENTAL_WAYLAND} -eq 1 ]]; then + msgbox info "Using Wayland, unsetting DISPLAY environment variable" + unset DISPLAY +fi + +declare -a startup_cmd +startup_cmd=(/bin/run_zwift.sh) + +if is_empty_directory "${ZWIFT_HOME}"; then + startup_cmd=(/bin/update_zwift.sh --install) +elif [[ ${1:-} == "--update" ]]; then + startup_cmd=(/bin/update_zwift.sh) +fi + "${startup_cmd[@]}" diff --git a/src/zwift.sh b/src/zwift.sh index 499f14a9..0e8cbdd3 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -357,10 +357,53 @@ if [[ ${CONTAINER_TOOL} == "podman" ]]; then container_args+=(--userns="keep-id:uid=1000,gid=1000") else container_uid="${host_uid}" - container_env_vars+=( - HOST_UID="${host_uid}" - HOST_GID="${host_gid}" - ) + tmp_dockerfile="" + + cleanup_remap_container() { + msgbox info "Removing temporary container zwift-remap-user" + ${CONTAINER_TOOL} rm zwift-remap-user || true + + msgbox info "Removing temporary image zwift-remap-user-image" + ${CONTAINER_TOOL} image rm zwift-remap-user-image || true + + msgbox info "Removing temporary dockerfile" + [[ -n ${tmp_dockerfile} ]] && [[ -f ${tmp_dockerfile} ]] && rm -f -- "${tmp_dockerfile}" || true + } + + if ! tmp_dockerfile="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)" || ! echo -e "FROM ${IMAGE}:${VERSION}\nUSER root\n ENTRYPOINT [\"entrypoint\"]" > "${tmp_dockerfile}"; then + msgbox error "Failed to create temporary dockerfile" + cleanup_remap_container + exit 1 + fi + + msgbox info "Creating image to remap user" + if ${CONTAINER_TOOL} build -t zwift-remap-user-image -f "${tmp_dockerfile}" .; then + msgbox ok "Created image to remap user" + else + msgbox error "Failed to create image to remap user" + cleanup_remap_container + exit 1 + fi + + msgbox info "Remapping container user to host user" + if ${CONTAINER_TOOL} run --name zwift-remap-user -e CONTAINER_TOOL="${CONTAINER_TOOL}" -e DEBUG="${DEBUG}" -e VERBOSITY="${VERBOSITY}" -e HOST_UID="${host_uid}" -e HOST_GID="${host_gid}" zwift-remap-user-image --remap-user; then + msgbox ok "Remapped container user to host user" + else + msgbox error "Failed to remap container user to host user" + cleanup_remap_container + exit 1 + fi + + msgbox info "Persisting container with remapped user" + if ${CONTAINER_TOOL} commit --change="USER user" --change='CMD [""]' -m "Remapped user to ${host_uid}:${host_gid}" zwift-remap-user "${IMAGE}:${VERSION}"; then + msgbox ok "Persisted container with remapped user" + else + msgbox error "Failed to persist container with remapped user" + cleanup_remap_container + exit 1 + fi + + cleanup_remap_container fi # Define base container environment variables From 24869b2e38b3e0a1ba90e6a482caeac16919b051 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sat, 14 Mar 2026 21:37:44 +0100 Subject: [PATCH 03/16] Use current user uid and gid during build phase --- src/Dockerfile | 7 ++++++- src/build-image.sh | 11 ++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index 0862e6cc..947e9d6f 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,4 +1,6 @@ ARG DEBIAN_VERSION=trixie +ARG USER_UID=1000 +ARG USER_GID=1000 FROM rust:1.90-slim AS build-runfromprocess @@ -27,6 +29,8 @@ FROM debian:${DEBIAN_VERSION}-slim AS wine-base # make sure to add "=" to the start, comment out for latest # WINE_VERSION="=9.9~bookworm-1" ARG DEBIAN_VERSION +ARG USER_UID +ARG USER_GID ARG WINE_BRANCH="devel" ARG WINE_VERSION="=9.9~${DEBIAN_VERSION}-1" ARG WINETRICKS_VERSION=20240105 @@ -81,7 +85,8 @@ ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu:/usr/local COPY pulse-client.conf /etc/pulse/client.conf # Run as a regular user instead of root -RUN adduser --disabled-password --gecos '' user +RUN addgroup --gid ${USER_GID} user \ + && adduser --uid ${USER_UID} --gid ${USER_GID} --disabled-password --comment '' user USER user FROM wine-base diff --git a/src/build-image.sh b/src/build-image.sh index 7d7acff3..2cac69c4 100755 --- a/src/build-image.sh +++ b/src/build-image.sh @@ -108,6 +108,10 @@ msgbox info "Image will be called ${IMAGE}" ############################### ##### Basic configuration ##### +# Create array for build arguments +declare -a build_args +build_args=() + # Create array for container arguments declare -a container_args container_args=( @@ -125,6 +129,7 @@ container_args=( # Initialize user ids host_uid="${UID}" +host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then # Podman maps the local user into the container as uid/gid 1000 (the container's user), # consistent with zwift.sh. Using the host uid/gid here causes a uid mismatch at runtime. @@ -132,6 +137,10 @@ if [[ ${CONTAINER_TOOL} == "podman" ]]; then container_args+=(--userns="keep-id:uid=1000,gid=1000") else container_uid="${host_uid}" + build_args+=( + --build-arg USER_UID="${host_uid}" + --build-arg USER_GID="${host_gid}" + ) fi # Configure window manager @@ -186,7 +195,7 @@ cleanup() { trap cleanup EXIT msgbox info "Building image ${IMAGE}" -if ${CONTAINER_TOOL} build --force-rm -t "${BUILD_NAME}" "${SCRIPT_DIR}"; then +if ${CONTAINER_TOOL} build --force-rm "${build_args[@]}" -t "${BUILD_NAME}" "${SCRIPT_DIR}"; then msgbox ok "Successfully built image ${IMAGE}" else msgbox error "Failed to build image" From 461ba9506fb2f5e19cbdc9eef0f0ed29716f4118 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sat, 14 Mar 2026 22:43:34 +0100 Subject: [PATCH 04/16] Remap using dockerfile instead of in entrypoint --- src/entrypoint.sh | 75 +------------------------------------ src/zwift.sh | 94 +++++++++++++++++++++++++---------------------- 2 files changed, 52 insertions(+), 117 deletions(-) diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 4d03102e..f1085f4c 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -22,8 +22,6 @@ else fi readonly VERBOSITY="${VERBOSITY:-1}" -readonly HOST_UID="${HOST_UID:-$(id -u user)}" -readonly HOST_GID="${HOST_GID:-$(id -g user)}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" @@ -68,85 +66,16 @@ is_empty_directory() { ! contents="$(ls -A "${directory}" 2> /dev/null)" || [[ -z ${contents} ]] } -remap_user() { - # docker does not support remapping container user to host user out of the box - - if ! is_user_root; then - msgbox error "Must be root to remap user" - return 1 - fi - - if [[ ${CONTAINER_TOOL} != "docker" ]]; then - msgbox error "User should only be remapped when using docker" - return 1 - fi - - container_uid="$(id -u user)" - container_gid="$(id -g user)" - - should_change_user_ids() { - # ids should be updated if HOST_UID:HOST_GID is different from from user uid:gid - # returns 0 if ids should be changed, 1 if not, so it can be used in an if - - local result=1 - - if [[ ! ${HOST_UID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring HOST_UID '${HOST_UID}' because it is not a number" - elif [[ ${container_uid} -ne ${HOST_UID} ]]; then - result=0 - fi - - if [[ ! ${HOST_GID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring HOST_GID '${HOST_GID}' because it is not a number" - elif [[ ${container_gid} -ne ${HOST_GID} ]]; then - result=0 - fi - - return "${result}" - } - - change_user_ids() { - usermod -ou "${HOST_UID}" user || return 1 - groupmod -og "${HOST_GID}" user || return 1 - mkdir -p "/run/user/${HOST_UID}" || return 1 - chown -R user:user "/run/user/${HOST_UID}" || return 1 - sed -i "s|/run/user/1000|/run/user/${HOST_UID}|g" /etc/pulse/client.conf || return 1 - } - - if should_change_user_ids; then - msgbox info "Changing user ids to ${HOST_UID}:${HOST_GID}" - if change_user_ids; then - msgbox ok "Changed user ids" - else - msgbox error "Failed to change user ids" - return 1 - fi - else - msgbox info "Nothing to do, user ids are already ${HOST_UID}:${HOST_GID}" - fi -} - ######################################### ##### Launch update or start script ##### +msgbox info "Starting or installing Zwift" + actual_user="$(whoami)" actual_uid="$(id -u "${actual_user}")" actual_gid="$(id -g "${actual_user}")" msgbox debug "Running as ${actual_user} (uid=${actual_uid}, gid=${actual_gid})" -if [[ ${1:-} == "--remap-user" ]]; then - msgbox info "Remapping container user to host user" - if remap_user; then - msgbox ok "Remapped container user to host user" - exit 0 - else - msgbox error "Failed to remap container user to host user" - exit 1 - fi -fi - -msgbox info "Starting or installing Zwift" - if is_user_root; then msgbox error "Cannot run or install Zwift as root!" exit 1 diff --git a/src/zwift.sh b/src/zwift.sh index 0e8cbdd3..bd54486d 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -323,6 +323,9 @@ if [[ ${DONT_CLEAN} -ne 1 ]] && [[ ${DONT_PULL} -ne 1 ]]; then fi fi +container_image="${IMAGE}" +container_image_version="${VERSION}" + ############################### ##### Basic configuration ##### @@ -354,56 +357,59 @@ host_uid="${UID}" host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then container_uid=1000 - container_args+=(--userns="keep-id:uid=1000,gid=1000") + container_gid=1000 + container_args+=(--userns="keep-id:uid=${container_uid},gid=${container_gid}") else - container_uid="${host_uid}" - tmp_dockerfile="" - - cleanup_remap_container() { - msgbox info "Removing temporary container zwift-remap-user" - ${CONTAINER_TOOL} rm zwift-remap-user || true - - msgbox info "Removing temporary image zwift-remap-user-image" - ${CONTAINER_TOOL} image rm zwift-remap-user-image || true + create_remap_dockerfile() { + local user_uid="${1:?}" + local user_gid="${2:?}" + local tmp_file="" + + if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then + msgbox error "Failed to create dockerfile for remapping user" + return 1 + fi - msgbox info "Removing temporary dockerfile" - [[ -n ${tmp_dockerfile} ]] && [[ -f ${tmp_dockerfile} ]] && rm -f -- "${tmp_dockerfile}" || true + { + echo "FROM ${IMAGE}:${VERSION}" + echo "USER root" + echo "RUN usermod -ou ${user_uid} user \\" + echo " && groupmod -og ${user_gid} user \\" + echo " && mkdir -p /run/user/${user_uid} \\" + echo " && chown -R user:user /run/user/${user_uid} \\" + echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" + echo "USER user" + echo 'ENTRYPOINT ["entrypoint"]' + } > "${tmp_file}" + + echo "${tmp_file}" } - if ! tmp_dockerfile="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)" || ! echo -e "FROM ${IMAGE}:${VERSION}\nUSER root\n ENTRYPOINT [\"entrypoint\"]" > "${tmp_dockerfile}"; then - msgbox error "Failed to create temporary dockerfile" - cleanup_remap_container - exit 1 - fi - - msgbox info "Creating image to remap user" - if ${CONTAINER_TOOL} build -t zwift-remap-user-image -f "${tmp_dockerfile}" .; then - msgbox ok "Created image to remap user" - else - msgbox error "Failed to create image to remap user" - cleanup_remap_container - exit 1 - fi + build_remap_dockerfile() { + local tag_name="${1:?}" + local dockerfile="${2:?}" - msgbox info "Remapping container user to host user" - if ${CONTAINER_TOOL} run --name zwift-remap-user -e CONTAINER_TOOL="${CONTAINER_TOOL}" -e DEBUG="${DEBUG}" -e VERBOSITY="${VERBOSITY}" -e HOST_UID="${host_uid}" -e HOST_GID="${host_gid}" zwift-remap-user-image --remap-user; then - msgbox ok "Remapped container user to host user" - else - msgbox error "Failed to remap container user to host user" - cleanup_remap_container - exit 1 - fi + if ${CONTAINER_TOOL} build -t "${tag_name}" -f "${dockerfile}" .; then + msgbox info "Created ${CONTAINER_TOOL} image ${tag_name}" + else + msgbox error "Failed to create ${CONTAINER_TOOL} image ${tag_name}" + return 1 + fi + } - msgbox info "Persisting container with remapped user" - if ${CONTAINER_TOOL} commit --change="USER user" --change='CMD [""]' -m "Remapped user to ${host_uid}:${host_gid}" zwift-remap-user "${IMAGE}:${VERSION}"; then - msgbox ok "Persisted container with remapped user" - else - msgbox error "Failed to persist container with remapped user" - cleanup_remap_container - exit 1 + container_uid="${host_uid}" + container_gid="${host_gid}" + if [[ ${host_uid} -ne 1000 ]] || [[ ${host_gid} -ne 1000 ]]; then + msgbox info "Remapping container user to host user" + container_image="netbrain/zwift" + container_image_version="remapped_user_${container_uid}_${container_gid}" + if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then + msgbox ok "Remapped container user to host user" + else + msgbox error "Failed to remap container user to host user" + exit 1 + fi fi - - cleanup_remap_container fi # Define base container environment variables @@ -717,7 +723,7 @@ fi ##### Start container ##### declare -a container_command -container_command=("${CONTAINER_TOOL}" run "${container_args[@]}" "${IMAGE}:${VERSION}" "${entrypoint_args[@]}") +container_command=("${CONTAINER_TOOL}" run "${container_args[@]}" "${container_image}:${container_image_version}" "${entrypoint_args[@]}") # Print the exact command that would be executed From 8d60929a799dcb57c2e2a5179778033f10cf87bc Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sun, 15 Mar 2026 17:02:06 +0100 Subject: [PATCH 05/16] Only build remapped user image if needed --- src/zwift.sh | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index bd54486d..e1e7bad3 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -360,16 +360,40 @@ if [[ ${CONTAINER_TOOL} == "podman" ]]; then container_gid=1000 container_args+=(--userns="keep-id:uid=${container_uid},gid=${container_gid}") else + remap_build_required() { + local tag_name="${1:?}" + + local latest_image_digest="" + if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{.Digest}}')"; then + msgbox error "Failed to get ${IMAGE}:${VERSION} image digest" + exit 1 + fi + + local current_image_digest="" + if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}')"; then + msgbox info "Failed to get ${tag_name} base image digest, may not exist yet" + return 0 + fi + + [[ ${current_image_digest} != "${latest_image_digest}" ]] + } + create_remap_dockerfile() { local user_uid="${1:?}" local user_gid="${2:?}" - local tmp_file="" + local tmp_file="" if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then msgbox error "Failed to create dockerfile for remapping user" return 1 fi + local image_digest="" + if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{.Digest}}')"; then + msgbox error "Failed to get ${IMAGE}:${VERSION} image digest" + return 1 + fi + { echo "FROM ${IMAGE}:${VERSION}" echo "USER root" @@ -380,6 +404,7 @@ else echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" echo "USER user" echo 'ENTRYPOINT ["entrypoint"]' + echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" } > "${tmp_file}" echo "${tmp_file}" @@ -403,11 +428,15 @@ else msgbox info "Remapping container user to host user" container_image="netbrain/zwift" container_image_version="remapped_user_${container_uid}_${container_gid}" - if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then - msgbox ok "Remapped container user to host user" + if remap_build_required "${container_image}:${container_image_version}"; then + if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then + msgbox ok "Remapped container user to host user" + else + msgbox error "Failed to remap container user to host user" + exit 1 + fi else - msgbox error "Failed to remap container user to host user" - exit 1 + msgbox ok "${container_image}:${container_image_version} is up to date" fi fi fi From a2cd468423b896f08782edf33f7552831a10db97 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sun, 15 Mar 2026 17:30:50 +0100 Subject: [PATCH 06/16] Don't assume container user has id 1000:1000 --- src/zwift.sh | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index e1e7bad3..080ec8a0 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -422,22 +422,20 @@ else fi } + msgbox info "Remapping container user to host user" container_uid="${host_uid}" container_gid="${host_gid}" - if [[ ${host_uid} -ne 1000 ]] || [[ ${host_gid} -ne 1000 ]]; then - msgbox info "Remapping container user to host user" - container_image="netbrain/zwift" - container_image_version="remapped_user_${container_uid}_${container_gid}" - if remap_build_required "${container_image}:${container_image_version}"; then - if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then - msgbox ok "Remapped container user to host user" - else - msgbox error "Failed to remap container user to host user" - exit 1 - fi + container_image="netbrain/zwift" + container_image_version="remapped_user_${container_uid}_${container_gid}" + if remap_build_required "${container_image}:${container_image_version}"; then + if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then + msgbox ok "Remapped container user to host user" else - msgbox ok "${container_image}:${container_image_version} is up to date" + msgbox error "Failed to remap container user to host user" + exit 1 fi + else + msgbox ok "${container_image}:${container_image_version} is up to date" fi fi From 921514835b2b1b47daa3946fd358d171aeeac374 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sun, 15 Mar 2026 17:47:29 +0100 Subject: [PATCH 07/16] Use RepoDigests instead of Digest Digest only exists in podman. RepoDigests exists in both podman and docker. --- src/zwift.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 080ec8a0..2d935590 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -364,13 +364,13 @@ else local tag_name="${1:?}" local latest_image_digest="" - if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{.Digest}}')"; then - msgbox error "Failed to get ${IMAGE}:${VERSION} image digest" - exit 1 + if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then + msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" + return 0 fi local current_image_digest="" - if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}')"; then + if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))"; then msgbox info "Failed to get ${tag_name} base image digest, may not exist yet" return 0 fi @@ -389,9 +389,9 @@ else fi local image_digest="" - if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{.Digest}}')"; then - msgbox error "Failed to get ${IMAGE}:${VERSION} image digest" - return 1 + if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then + msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" + image_digest="${IMAGE}:${VERSION}" fi { From 9444f7d7222bbe87885debfee575edfa8f6da77f Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sun, 15 Mar 2026 22:31:51 +0100 Subject: [PATCH 08/16] Move user remapping to separate section --- src/zwift.sh | 194 +++++++++++++++++++++++++-------------------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 2d935590..1a1afecb 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -342,91 +342,125 @@ fi # Create array for container environment variables declare -a container_env_vars -container_env_vars=() +container_env_vars=( + DEBUG="${DEBUG}" + VERBOSITY="${VERBOSITY}" + CONTAINER_TOOL="${CONTAINER_TOOL}" +) # Create array for container arguments declare -a container_args -container_args=() +container_args=( + --rm + --network "${NETWORKING}" + --name "zwift-${USER}" + --hostname "${HOSTNAME}" + --env-file "${container_env_file}" + -v "zwift-${USER}:${ZWIFT_DOCS}" +) # Create array for entrypoint arguments declare -a entrypoint_args entrypoint_args=() -# Initialize user ids -host_uid="${UID}" -host_gid="$(id -g)" -if [[ ${CONTAINER_TOOL} == "podman" ]]; then - container_uid=1000 - container_gid=1000 - container_args+=(--userns="keep-id:uid=${container_uid},gid=${container_gid}") -else - remap_build_required() { - local tag_name="${1:?}" +################################################### +##### Forward arguments passed to this script ##### - local latest_image_digest="" - if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then - msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" - return 0 - fi +# Arguments before -- are forwarded to the container tool +# Arguments after -- are forwarded to the container entrypoint - local current_image_digest="" - if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))"; then - msgbox info "Failed to get ${tag_name} base image digest, may not exist yet" - return 0 - fi +dashes_found=0 +for arg; do + if [[ ${dashes_found} -eq 1 ]]; then + entrypoint_args+=("${arg}") + elif [[ ${arg} == "--" ]]; then + dashes_found=1 + else + container_args+=("${arg}") + fi +done - [[ ${current_image_digest} != "${latest_image_digest}" ]] - } +############################################# +##### Remap container user to host user ##### - create_remap_dockerfile() { - local user_uid="${1:?}" - local user_gid="${2:?}" +remap_build_required() { + local tag_name="${1:?}" - local tmp_file="" - if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then - msgbox error "Failed to create dockerfile for remapping user" - return 1 - fi + local latest_image_digest="" + if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then + msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" + return 0 + fi - local image_digest="" - if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then - msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" - image_digest="${IMAGE}:${VERSION}" - fi + local current_image_digest="" + if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))"; then + msgbox info "Failed to get ${tag_name} base image digest, may not exist yet" + return 0 + fi - { - echo "FROM ${IMAGE}:${VERSION}" - echo "USER root" - echo "RUN usermod -ou ${user_uid} user \\" - echo " && groupmod -og ${user_gid} user \\" - echo " && mkdir -p /run/user/${user_uid} \\" - echo " && chown -R user:user /run/user/${user_uid} \\" - echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" - echo "USER user" - echo 'ENTRYPOINT ["entrypoint"]' - echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" - } > "${tmp_file}" - - echo "${tmp_file}" - } + [[ ${current_image_digest} != "${latest_image_digest}" ]] +} - build_remap_dockerfile() { - local tag_name="${1:?}" - local dockerfile="${2:?}" +create_remap_dockerfile() { + local user_uid="${1:?}" + local user_gid="${2:?}" - if ${CONTAINER_TOOL} build -t "${tag_name}" -f "${dockerfile}" .; then - msgbox info "Created ${CONTAINER_TOOL} image ${tag_name}" - else - msgbox error "Failed to create ${CONTAINER_TOOL} image ${tag_name}" - return 1 - fi - } + local tmp_file="" + if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then + msgbox error "Failed to create dockerfile for remapping user" + return 1 + fi - msgbox info "Remapping container user to host user" + local image_digest="" + if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then + msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" + image_digest="${IMAGE}:${VERSION}" + fi + + { + echo "FROM ${IMAGE}:${VERSION}" + echo "USER root" + echo "RUN usermod -ou ${user_uid} user \\" + echo " && groupmod -og ${user_gid} user \\" + echo " && mkdir -p /run/user/${user_uid} \\" + echo " && chown -R user:user /run/user/${user_uid} \\" + echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" + echo "USER user" + echo 'ENTRYPOINT ["entrypoint"]' + echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" + } > "${tmp_file}" + + echo "${tmp_file}" +} + +build_remap_dockerfile() { + local tag_name="${1:?}" + local dockerfile="${2:?}" + + if ${CONTAINER_TOOL} build -t "${tag_name}" -f "${dockerfile}" .; then + msgbox info "Created ${CONTAINER_TOOL} image ${tag_name}" + else + msgbox error "Failed to create ${CONTAINER_TOOL} image ${tag_name}" + return 1 + fi +} + +host_uid="${UID}" +host_gid="$(id -g)" + +if [[ ${CONTAINER_TOOL} == "podman" ]]; then + container_uid=1000 + container_gid=1000 + + container_args+=(--userns="keep-id:uid=${container_uid},gid=${container_gid}") +else container_uid="${host_uid}" container_gid="${host_gid}" + container_image="netbrain/zwift" container_image_version="remapped_user_${container_uid}_${container_gid}" + + msgbox info "Remapping container user to host user" if remap_build_required "${container_image}:${container_image_version}"; then if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then msgbox ok "Remapped container user to host user" @@ -439,40 +473,6 @@ else fi fi -# Define base container environment variables -container_env_vars+=( - DEBUG="${DEBUG}" - VERBOSITY="${VERBOSITY}" - CONTAINER_TOOL="${CONTAINER_TOOL}" -) - -# Define base container parameters -container_args+=( - --rm - --network "${NETWORKING}" - --name "zwift-${USER}" - --hostname "${HOSTNAME}" - --env-file "${container_env_file}" - -v "zwift-${USER}:${ZWIFT_DOCS}" -) - -################################################### -##### Forward arguments passed to this script ##### - -# Arguments before -- are forwarded to the container tool -# Arguments after -- are forwarded to the container entrypoint - -dashes_found=0 -for arg; do - if [[ ${dashes_found} -eq 1 ]]; then - entrypoint_args+=("${arg}") - elif [[ ${arg} == "--" ]]; then - dashes_found=1 - else - container_args+=("${arg}") - fi -done - ############################################## ##### User defined environment variables ##### From 66147cd03970f3b24567d336aebd6adbe7bce27d Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Sun, 15 Mar 2026 23:04:43 +0100 Subject: [PATCH 09/16] Create functions for duplicate code --- src/zwift.sh | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 1a1afecb..8b7a0247 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -383,18 +383,36 @@ done ############################################# ##### Remap container user to host user ##### +image_repo_digest() { + # Local images do not have a remote repository, will return 1 + + local tag_name="${1:?}" + + local repo_digest + repo_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .RepoDigests 0}}' 2> /dev/null)" || return 1 + echo "${repo_digest}" +} + +image_digest_label() { + local tag_name="${1:?}" + + local digest_label + digest_label="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))" || return 1 + echo "${digest_label}" +} + remap_build_required() { local tag_name="${1:?}" - local latest_image_digest="" - if ! latest_image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then - msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" + local latest_image_digest + if ! latest_image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + msgbox info "Failed to get ${IMAGE}:${VERSION} image repository digest, assuming rebuild is required" return 0 fi - local current_image_digest="" - if ! current_image_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))"; then - msgbox info "Failed to get ${tag_name} base image digest, may not exist yet" + local current_image_digest + if ! current_image_digest="$(image_digest_label "${tag_name}")"; then + msgbox info "Failed to get ${tag_name} base image digest, may not exist yet, assuming rebuild is required" return 0 fi @@ -405,17 +423,14 @@ create_remap_dockerfile() { local user_uid="${1:?}" local user_gid="${2:?}" - local tmp_file="" + local tmp_file if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then msgbox error "Failed to create dockerfile for remapping user" return 1 fi - local image_digest="" - if ! image_digest="$(${CONTAINER_TOOL} inspect "${IMAGE}:${VERSION}" --format '{{index .RepoDigests 0}}' 2> /dev/null)"; then - msgbox warning "Failed to get ${IMAGE}:${VERSION} image digest" - image_digest="${IMAGE}:${VERSION}" - fi + local image_digest + image_digest="$(image_repo_digest "${IMAGE}:${VERSION}" || echo "Unknown")" { echo "FROM ${IMAGE}:${VERSION}" From 020add8657eec5fb11d816b4ab1e8406d0ad4918 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Mon, 16 Mar 2026 11:10:33 +0100 Subject: [PATCH 10/16] Don't use temporary Dockerfile --- src/zwift.sh | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 8b7a0247..1c6d9ff3 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -423,36 +423,35 @@ create_remap_dockerfile() { local user_uid="${1:?}" local user_gid="${2:?}" - local tmp_file - if ! tmp_file="$(mktemp -q /tmp/zwift-remap-user.dockerfile.XXXXXXXXXX)"; then - msgbox error "Failed to create dockerfile for remapping user" - return 1 - fi - local image_digest image_digest="$(image_repo_digest "${IMAGE}:${VERSION}" || echo "Unknown")" - { - echo "FROM ${IMAGE}:${VERSION}" - echo "USER root" - echo "RUN usermod -ou ${user_uid} user \\" - echo " && groupmod -og ${user_gid} user \\" - echo " && mkdir -p /run/user/${user_uid} \\" - echo " && chown -R user:user /run/user/${user_uid} \\" - echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" - echo "USER user" - echo 'ENTRYPOINT ["entrypoint"]' - echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" - } > "${tmp_file}" - - echo "${tmp_file}" + echo "FROM ${IMAGE}:${VERSION}" + echo "USER root" + echo "RUN usermod -ou ${user_uid} user \\" + echo " && groupmod -og ${user_gid} user \\" + echo " && mkdir -p /run/user/${user_uid} \\" + echo " && chown -R user:user /run/user/${user_uid} \\" + echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" + echo "USER user" + echo 'ENTRYPOINT ["entrypoint"]' + echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" } build_remap_dockerfile() { local tag_name="${1:?}" local dockerfile="${2:?}" - if ${CONTAINER_TOOL} build -t "${tag_name}" -f "${dockerfile}" .; then + msgbox info "Building container image with remapped user" + + msgbox debug "Using dockerfile:" + local dockerfile_lines line + readarray -t dockerfile_lines <<< "${dockerfile}" + for line in "${dockerfile_lines[@]}"; do + msgbox debug " ${line/\\/\\\\}" + done + + if ${CONTAINER_TOOL} build -t "${tag_name}" - <<< "${dockerfile}"; then msgbox info "Created ${CONTAINER_TOOL} image ${tag_name}" else msgbox error "Failed to create ${CONTAINER_TOOL} image ${tag_name}" @@ -477,7 +476,8 @@ else msgbox info "Remapping container user to host user" if remap_build_required "${container_image}:${container_image_version}"; then - if remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" && build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then + remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" + if build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then msgbox ok "Remapped container user to host user" else msgbox error "Failed to remap container user to host user" From 67d6ccf0d61aa7ad216d5e2aaee87ba04a1b52e2 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Mon, 16 Mar 2026 12:19:40 +0100 Subject: [PATCH 11/16] Check volume permissions --- src/run_zwift.sh | 10 ++++++++++ src/zwift.sh | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/run_zwift.sh b/src/run_zwift.sh index 2de77306..e45aa852 100755 --- a/src/run_zwift.sh +++ b/src/run_zwift.sh @@ -66,6 +66,16 @@ if [[ ! -d ${ZWIFT_HOME} ]] || ! cd "${ZWIFT_HOME}"; then exit 1 fi +if [[ ! -d ${ZWIFT_DOCS} ]] || [[ ! -O ${ZWIFT_DOCS} ]] || [[ ! -G ${ZWIFT_DOCS} ]]; then + # shellcheck disable=SC2016 # using a command as literal string on the next line + expected_owner='$(id -u $USER):$(id -g $USER)' + [[ ${CONTAINER_TOOL} == "podman" ]] && expected_owner="1000:1000" + msgbox error "Directory ${ZWIFT_DOCS} does not exist or is not accessible." + msgbox error "You can try to fix its permissions by running:" + msgbox error " ${CONTAINER_TOOL} run --rm --user root -it -v zwift-\$USER:/zwift-docs --entrypoint bash netbrain/zwift:latest -c \"chown -R ${expected_owner} /zwift-docs\"" + exit 1 +fi + if [[ -n ${ZWIFT_OVERRIDE_RESOLUTION} ]]; then if [[ -f ${ZWIFT_PREFS} ]]; then msgbox info "Setting zwift resolution to ${ZWIFT_OVERRIDE_RESOLUTION}." diff --git a/src/zwift.sh b/src/zwift.sh index 1c6d9ff3..e9e99d82 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -792,16 +792,24 @@ else print_container_command debug fi -# Create a volume if not already exists, this is done now as -# if left to the run command the directory can get the wrong permissions -if [[ ${CONTAINER_TOOL} == "podman" ]] && ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then +# Create the volume for the zwift documents directory if it does not already exist +if ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then msgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" - if ${CONTAINER_TOOL} volume create "zwift-${USER}"; then + if ${CONTAINER_TOOL} volume create "zwift-${USER}" > /dev/null 2>&1; then msgbox ok "Created volume zwift-${USER}" else msgbox error "Failed to create volume zwift-${USER}" exit 1 fi + if [[ ${CONTAINER_TOOL} != "podman" ]]; then + msgbox info "Updating owner of volume zwift-${USER}" + if ${CONTAINER_TOOL} run --rm --user root -it -v "zwift-${USER}:/zwift-docs" --entrypoint bash "${container_image}:${container_image_version}" -c "chown -R \"${container_uid}:${container_gid}\" /zwift-docs"; then + msgbox ok "Updated zwift-${USER} volume owner to ${container_uid}:${container_gid}" + else + msgbox error "Failed to update zwift-${USER} volume owner to ${container_uid}:${container_gid}" + exit 1 + fi + fi fi # Only write environment variables to file when needed From faba798e84d835ee78ba68a83af99760bc152e63 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Mon, 16 Mar 2026 14:31:33 +0100 Subject: [PATCH 12/16] Automatically remap zwift user volume if needed --- src/zwift.sh | 62 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index e9e99d82..1e53de7c 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -488,6 +488,48 @@ else fi fi +# Create the volume for the zwift documents directory if it does not already exist +if ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then + msgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" + if ${CONTAINER_TOOL} volume create "zwift-${USER}" > /dev/null 2>&1; then + msgbox ok "Created volume zwift-${USER}" + else + msgbox error "Failed to create volume zwift-${USER}" + exit 1 + fi +fi + +volume_remap_required() { + ${CONTAINER_TOOL} run --rm \ + -v "zwift-${USER}:/zwift-docs" \ + -it --entrypoint bash \ + "${container_image}:${container_image_version}" \ + -c "[[ ! -O /zwift-docs ]] || [[ ! -G /zwift-docs ]]" +} + +remap_volume() { + ${CONTAINER_TOOL} run --rm \ + --user root \ + -v "zwift-${USER}:/zwift-docs" \ + -it --entrypoint bash \ + "${container_image}:${container_image_version}" \ + -c "chown -R \"${container_uid}:${container_gid}\" /zwift-docs" +} + +# Docker: Remap volume to container user +# Necessary in two cases: +# - Volume was just created, owner will be root, remap required +# - End user changed user uid/gid, remap required +if [[ ${CONTAINER_TOOL} != "podman" ]] && volume_remap_required; then + msgbox info "Updating owner of volume zwift-${USER}" + if remap_volume; then + msgbox ok "Updated zwift-${USER} volume owner to ${container_uid}:${container_gid}" + else + msgbox error "Failed to update zwift-${USER} volume owner to ${container_uid}:${container_gid}" + exit 1 + fi +fi + ############################################## ##### User defined environment variables ##### @@ -792,26 +834,6 @@ else print_container_command debug fi -# Create the volume for the zwift documents directory if it does not already exist -if ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then - msgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" - if ${CONTAINER_TOOL} volume create "zwift-${USER}" > /dev/null 2>&1; then - msgbox ok "Created volume zwift-${USER}" - else - msgbox error "Failed to create volume zwift-${USER}" - exit 1 - fi - if [[ ${CONTAINER_TOOL} != "podman" ]]; then - msgbox info "Updating owner of volume zwift-${USER}" - if ${CONTAINER_TOOL} run --rm --user root -it -v "zwift-${USER}:/zwift-docs" --entrypoint bash "${container_image}:${container_image_version}" -c "chown -R \"${container_uid}:${container_gid}\" /zwift-docs"; then - msgbox ok "Updated zwift-${USER} volume owner to ${container_uid}:${container_gid}" - else - msgbox error "Failed to update zwift-${USER} volume owner to ${container_uid}:${container_gid}" - exit 1 - fi - fi -fi - # Only write environment variables to file when needed msgbox info "Writing environment variables to temporary file" if printf '%s\n' "${container_env_vars[@]}" > "${container_env_file}"; then From 420b5b3b8b923e2c41de7ad6261b625fbddc7030 Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Tue, 17 Mar 2026 11:06:42 +0100 Subject: [PATCH 13/16] Don't use temporary variables if not needed, increase verbosity a bit --- src/zwift.sh | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 1e53de7c..081c340f 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -384,34 +384,34 @@ done ##### Remap container user to host user ##### image_repo_digest() { - # Local images do not have a remote repository, will return 1 + # Local images do not have a remote repository, will return non-zero local tag_name="${1:?}" - local repo_digest - repo_digest="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .RepoDigests 0}}' 2> /dev/null)" || return 1 - echo "${repo_digest}" + ${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .RepoDigests 0}}' 2> /dev/null } image_digest_label() { local tag_name="${1:?}" - local digest_label - digest_label="$(${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null))" || return 1 - echo "${digest_label}" + ${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null } remap_build_required() { local tag_name="${1:?}" local latest_image_digest - if ! latest_image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + if latest_image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + msgbox debug "Latest image digest is ${latest_image_digest}" + else msgbox info "Failed to get ${IMAGE}:${VERSION} image repository digest, assuming rebuild is required" return 0 fi local current_image_digest - if ! current_image_digest="$(image_digest_label "${tag_name}")"; then + if current_image_digest="$(image_digest_label "${tag_name}")"; then + msgbox debug "Base image digest is ${current_image_digest}" + else msgbox info "Failed to get ${tag_name} base image digest, may not exist yet, assuming rebuild is required" return 0 fi @@ -424,7 +424,9 @@ create_remap_dockerfile() { local user_gid="${2:?}" local image_digest - image_digest="$(image_repo_digest "${IMAGE}:${VERSION}" || echo "Unknown")" + if ! image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + image_digest="Unknown" + fi echo "FROM ${IMAGE}:${VERSION}" echo "USER root" From ccc0cb180cc3c2fef803bf868f0c396ef53a142b Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Tue, 17 Mar 2026 11:25:25 +0100 Subject: [PATCH 14/16] Fix user id consistency in dockerfiles --- src/Dockerfile | 23 +++++++++++++++-------- src/zwift.sh | 6 +++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index 947e9d6f..e02eee50 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -74,19 +74,26 @@ RUN wget -qO /etc/apt/trusted.gpg.d/winehq.asc https://dl.winehq.org/wine-builds && wget -qO /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/${WINETRICKS_VERSION}/src/winetricks \ && chmod +x /usr/local/bin/winetricks -# Make nvidia libraries discoverable -RUN echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ +# Add audio driver config file +COPY pulse-client.conf /etc/pulse/client.conf + +# Various configuration +# - Create user +# - Create runtime directory +# - Update audio driver config file +# - Make nvidia libraries discoverable +RUN addgroup --gid ${USER_GID} user \ + && adduser --uid ${USER_UID} --gid ${USER_GID} --disabled-password --comment '' user \ + && sed -i "s|/run/user/1000|/run/user/${USER_UID}|g" /etc/pulse/client.conf \ + && mkdir -p /run/user/${USER_UID} \ + && chown -R user:user /run/user/${USER_UID} \ + && echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ && echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf -# Required for non-glvnd setups +# Make nvidia libraries discoverable (Required for non-glvnd setups) ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu:/usr/local/nvidia/lib:/usr/local/nvidia/lib64 -# Configure audio driver -COPY pulse-client.conf /etc/pulse/client.conf - # Run as a regular user instead of root -RUN addgroup --gid ${USER_GID} user \ - && adduser --uid ${USER_UID} --gid ${USER_GID} --disabled-password --comment '' user USER user FROM wine-base diff --git a/src/zwift.sh b/src/zwift.sh index 081c340f..6b836237 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -430,11 +430,11 @@ create_remap_dockerfile() { echo "FROM ${IMAGE}:${VERSION}" echo "USER root" - echo "RUN usermod -ou ${user_uid} user \\" + echo "RUN sed -i \"s|/run/user/\$(id -u user)|/run/user/${user_uid}|g\" /etc/pulse/client.conf \\" + echo " && usermod -ou ${user_uid} user \\" echo " && groupmod -og ${user_gid} user \\" echo " && mkdir -p /run/user/${user_uid} \\" - echo " && chown -R user:user /run/user/${user_uid} \\" - echo " && sed -i \"s|/run/user/1000|/run/user/${user_uid}|g\" /etc/pulse/client.conf" + echo " && chown -R user:user /run/user/${user_uid}" echo "USER user" echo 'ENTRYPOINT ["entrypoint"]' echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" From 11083852a8486c3222b3a13913bc45ee8a74798e Mon Sep 17 00:00:00 2001 From: Glenn Van Loon Date: Tue, 17 Mar 2026 11:35:45 +0100 Subject: [PATCH 15/16] Don't container with tty if not needed --- src/zwift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index 6b836237..cb7ebd28 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -504,7 +504,7 @@ fi volume_remap_required() { ${CONTAINER_TOOL} run --rm \ -v "zwift-${USER}:/zwift-docs" \ - -it --entrypoint bash \ + --entrypoint bash \ "${container_image}:${container_image_version}" \ -c "[[ ! -O /zwift-docs ]] || [[ ! -G /zwift-docs ]]" } @@ -513,7 +513,7 @@ remap_volume() { ${CONTAINER_TOOL} run --rm \ --user root \ -v "zwift-${USER}:/zwift-docs" \ - -it --entrypoint bash \ + --entrypoint bash \ "${container_image}:${container_image_version}" \ -c "chown -R \"${container_uid}:${container_gid}\" /zwift-docs" } From 21d65b6aa3749c6703d834a723bc475103d67cd1 Mon Sep 17 00:00:00 2001 From: Glenn Date: Wed, 18 Mar 2026 10:57:49 +0100 Subject: [PATCH 16/16] Update remapped image tag name Co-authored-by: Glenn --- src/zwift.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index cb7ebd28..efd2939b 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -473,8 +473,8 @@ else container_uid="${host_uid}" container_gid="${host_gid}" - container_image="netbrain/zwift" - container_image_version="remapped_user_${container_uid}_${container_gid}" + container_image="${IMAGE#docker.io/}" + container_image_version="uid_${container_uid}_gid_${container_gid}" msgbox info "Remapping container user to host user" if remap_build_required "${container_image}:${container_image_version}"; then