From cb0eb2be848b12a4dfa2309b1dd352a59a68b626 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 08:59:04 +0100 Subject: [PATCH 1/8] Add minimal variant for volume-based Zwift installation Introduces ZWIFT_VARIANT=minimal mode where /home/user is mounted as a persistent volume. Zwift installs on first run into the volume instead of being baked into the container image. Eliminates per-launch chown for Docker users with non-1000 uid/gid and reduces image size. --- .github/workflows/zwift_build_minimal.yaml | 34 +++++++++ docs/advanced/minimal-variant.md | 82 ++++++++++++++++++++++ docs/advanced/nixos.md | 2 + docs/configuration/options.md | 3 + docs/troubleshooting/slow-start.md | 8 ++- flake.nix | 40 +++++++++++ src/entrypoint.sh | 15 ++-- src/update_zwift.sh | 6 +- src/zwift.sh | 24 +++++-- 9 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/zwift_build_minimal.yaml create mode 100644 docs/advanced/minimal-variant.md diff --git a/.github/workflows/zwift_build_minimal.yaml b/.github/workflows/zwift_build_minimal.yaml new file mode 100644 index 00000000..d77ef086 --- /dev/null +++ b/.github/workflows/zwift_build_minimal.yaml @@ -0,0 +1,34 @@ +--- +name: Zwift build minimal +'on': + workflow_dispatch: + push: + branches: + - master + paths: + - src/Dockerfile +concurrency: + group: zwift-minimal-container-image +jobs: + zwift_build_minimal: + if: github.repository == 'netbrain/zwift' + runs-on: ubuntu-22.04 + timeout-minutes: 30 + steps: + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v6 + - name: Build and push minimal image + run: | + build_date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + + docker build \ + --label "org.opencontainers.image.created=${build_date}" \ + -t netbrain/zwift:minimal \ + src/ + + docker push netbrain/zwift:minimal diff --git a/docs/advanced/minimal-variant.md b/docs/advanced/minimal-variant.md new file mode 100644 index 00000000..2e93142f --- /dev/null +++ b/docs/advanced/minimal-variant.md @@ -0,0 +1,82 @@ +--- +title: Minimal Variant +parent: Advanced +nav_order: 4 +--- + +# Minimal Variant (Volume-Based) + +The default mode mounts only the Zwift documents directory as a volume. The **minimal variant** mounts the entire +`/home/user` directory as a persistent volume instead. On first run, the container runtime automatically populates the +volume from the image contents — no separate installation step needed. + +## Benefits + +- **No per-launch chown**: File ownership persists in the volume, eliminating the slow `chown` that Docker users with + non-1000 uid/gid experience on every launch. +- **Persistent wine prefix**: The entire Wine configuration, registry, and installed prerequisites are preserved between + runs. + +## How it works + +| | Default (`container`) | Minimal (`minimal`) | +| --- | --- | --- | +| **Image** | `netbrain/zwift:latest` | `netbrain/zwift:latest` (same image) | +| **Volume** | `zwift-$USER` mounted at Zwift docs directory | `zwift-home-$USER` mounted at `/home/user` | +| **First run** | Seconds | Seconds (volume auto-populated from image) | +| **Subsequent runs** | Seconds | Seconds | +| **chown on launch** | Every launch if uid/gid != 1000 | Skipped (ownership persists in volume) | + +When a new named volume is mounted to a container path that already has data, Docker (and Podman) automatically copy +the image contents into the volume. This means the first launch is just as fast as any other — no download or +installation wait. + +## Usage + +### Environment variable + +```bash +ZWIFT_VARIANT="minimal" zwift +``` + +Or add to your config file (`~/.config/zwift/config`): + +```bash +ZWIFT_VARIANT="minimal" +``` + +### NixOS module + +```nix +{ + programs.zwift = { + enable = true; + variant = "minimal"; + }; +} +``` + +### Standalone Nix package + +```bash +nix run github:netbrain/zwift#zwift-minimal +``` + +## Updating Zwift + +Zwift updates itself through its built-in launcher. You can also force an update by passing `--update` as an entrypoint +argument: + +```bash +ZWIFT_VARIANT="minimal" zwift -- --update +``` + +## Resetting the installation + +To start fresh, remove the persistent volume: + +```bash +docker volume rm "zwift-home-$USER" +``` + +The next launch will re-populate the volume from the current image. diff --git a/docs/advanced/nixos.md b/docs/advanced/nixos.md index cebc00a6..bcc3abf2 100644 --- a/docs/advanced/nixos.md +++ b/docs/advanced/nixos.md @@ -36,6 +36,8 @@ environment variables in camelCase: programs.zwift = { # Enable the zwift module and install required dependencies enable = true; + # Variant: "container" (pre-installed, default) or "minimal" (volume-based install) + variant = "container"; # The Docker image to use for zwift image = "docker.io/netbrain/zwift"; # The zwift game version to run diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 96ceaaf4..4d700978 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -40,6 +40,7 @@ These environment variables can be used to alter the execution of the zwift bash | `DEBUG` | `0` | If set to `1`, enable debug of zwift script `set -x` | | `VGA_DEVICE_FLAG` | | Override GPU/device flags for container (`--gpus=all`) | | `PRIVILEGED_CONTAINER` | `0` | If set, container will run in privileged mode, SELinux label separation will be disabled (`--privileged --security-opt label=disable`) | +| `ZWIFT_VARIANT` | `container` | Set to `minimal` to mount `/home/user` as a persistent volume (faster startup, no per-launch chown) | {: .important } `ZWIFT_UID` and `ZWIFT_GID` can only be used with X11. They do not work in wayland! @@ -59,6 +60,8 @@ These environment variables can be used to alter the execution of the zwift bash the user that started the container. You should not need to change this except in rare cases. - `WINE_EXPERIMENTAL_WAYLAND="1" zwift` This will start zwift using Wayland and not XWayland. It will start full screen windowed. +- `ZWIFT_VARIANT="minimal" zwift` will mount `/home/user` as a persistent volume, eliminating per-launch `chown` (see + [Minimal Variant]({% link advanced/minimal-variant.md %}) for details). {: .note } > To pass extra environment variables to the container, they can be added to `CONTAINER_EXTRA_ARGS` with the `-e` flag. diff --git a/docs/troubleshooting/slow-start.md b/docs/troubleshooting/slow-start.md index 2bf80023..7b38b70a 100644 --- a/docs/troubleshooting/slow-start.md +++ b/docs/troubleshooting/slow-start.md @@ -9,5 +9,9 @@ nav_order: 3 If your `$(id -u)` or `$(id -g)` is not equal to 1000 then this would cause the zwift container to re-map all files (`chown`, `chgrp`) within the container so there is no uid/gid conflicts. -So if speed is a concern of yours, consider changing your user to match the containers uid and gid using `usermod` or contribute -a better solution for handling uid/gid remapping in containers. :smiley: +If speed is a concern, you have two options: + +1. **Use the minimal variant** (`ZWIFT_VARIANT="minimal"`): This mounts the entire `/home/user` as a persistent volume, + so file ownership persists between runs and no `chown` is needed after the first launch. See + [Minimal Variant]({% link advanced/minimal-variant.md %}) for details. +2. **Change your user IDs** to match the container's uid and gid (1000) using `usermod`. diff --git a/flake.nix b/flake.nix index cb928017..9898a532 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ { image, tag, + variant, dontCheck, dontPull, dontClean, @@ -45,6 +46,7 @@ nixosRun = pkgs.writeShellScript "zwift-nixos.sh" '' ${pkgs.lib.optionalString (image != "") "export IMAGE=${image}"} ${pkgs.lib.optionalString (tag != "") "export VERSION=${tag}"} + ${pkgs.lib.optionalString (variant != "") "export ZWIFT_VARIANT=${variant}"} ${pkgs.lib.optionalString (dontCheck != "") "export DONT_CHECK=${dontCheck}"} ${pkgs.lib.optionalString (dontPull != "") "export DONT_PULL=${dontPull}"} ${pkgs.lib.optionalString (dontClean != "") "export DONT_CLEAN=${dontClean}"} @@ -110,6 +112,14 @@ options.programs.zwift = { enable = mkEnableOption "zwift on linux"; + variant = mkOption { + type = enum [ + "container" + "minimal" + ]; + default = "container"; + description = "Zwift variant: 'container' (pre-installed) or 'minimal' (volume-based install on first run)"; + }; image = lib.mkOption { type = str; default = ""; @@ -227,6 +237,7 @@ (wrapPackage { inherit image + variant containerTool containerExtraArgs zwiftUsername @@ -316,6 +327,35 @@ desktopItems = [ "bin/Zwift.desktop" ]; }; + zwift-minimal = wrapPackage { + image = ""; + tag = ""; + variant = "minimal"; + dontCheck = ""; + dontPull = ""; + dontClean = ""; + dryRun = ""; + interactive = ""; + containerTool = ""; + containerExtraArgs = ""; + zwiftUsername = ""; + zwiftPassword = ""; + zwiftWorkoutDir = ""; + zwiftActivityDir = ""; + zwiftLogDir = ""; + zwiftScreenshotsDir = ""; + zwiftOverrideGraphics = ""; + zwiftOverrideResolution = ""; + zwiftFg = ""; + zwiftNoGameMode = ""; + wineExperimentalWayland = ""; + networking = ""; + zwiftUid = ""; + zwiftGid = ""; + vgaDeviceFlag = ""; + debug = ""; + privilegedContainer = ""; + }; default = self.packages.x86_64-linux.zwift; }; }; diff --git a/src/entrypoint.sh b/src/entrypoint.sh index b92e3c0b..51d63136 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -25,6 +25,7 @@ readonly ZWIFT_UID="${ZWIFT_UID:-$(id -u user)}" readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g user)}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" +readonly ZWIFT_MINIMAL="${ZWIFT_MINIMAL:-0}" readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" @@ -142,12 +143,16 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then fi fi - msgbox info "Changing ownership from root to user" - if update_ownership; then - msgbox ok "Changed ownership to user" + if [[ ${ZWIFT_MINIMAL} -eq 1 ]]; then + msgbox ok "Minimal variant: skipping ownership update (persisted in volume)" else - msgbox error "Failed to change owership from root to user" - exit 1 + msgbox info "Changing ownership from root to user" + if update_ownership; then + msgbox ok "Changed ownership to user" + else + msgbox error "Failed to change ownership from root to user" + exit 1 + fi fi startup_cmd=(gosu user:user "${startup_cmd[@]}") diff --git a/src/update_zwift.sh b/src/update_zwift.sh index c6b07aaa..0d3c2ac3 100755 --- a/src/update_zwift.sh +++ b/src/update_zwift.sh @@ -22,6 +22,7 @@ else fi readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" +readonly ZWIFT_MINIMAL="${ZWIFT_MINIMAL:-0}" readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" @@ -155,7 +156,10 @@ cleanup() { rm -rf -- "${WINE_USER_HOME}/Downloads/Zwift" || true rm -rf -- "/home/user/.cache/wine*" || true # remove Zwift documents because it causes permission errors with podman - rm -rf -- "${ZWIFT_DOCS}" || true + # skip in minimal variant — ZWIFT_DOCS lives in the persistent volume + if [[ ${ZWIFT_MINIMAL} -ne 1 ]]; then + rm -rf -- "${ZWIFT_DOCS}" || true + fi } trap cleanup EXIT diff --git a/src/zwift.sh b/src/zwift.sh index bcdb2448..c43636d2 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -157,6 +157,7 @@ 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}" +readonly ZWIFT_VARIANT="${ZWIFT_VARIANT:-container}" # Initialize CONTAINER_TOOL: Use podman if available msgbox info "Looking for container tool" @@ -329,9 +330,15 @@ container_args+=( --name "zwift-${USER}" --hostname "${HOSTNAME}" --env-file "${container_env_file}" - -v "zwift-${USER}:${ZWIFT_DOCS}" ) +if [[ ${ZWIFT_VARIANT} == "minimal" ]]; then + container_args+=(-v "zwift-home-${USER}:/home/user") + container_env_vars+=(ZWIFT_MINIMAL="1") +else + container_args+=(-v "zwift-${USER}:${ZWIFT_DOCS}") +fi + ################################################### ##### Forward arguments passed to this script ##### @@ -623,12 +630,17 @@ 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 ls | grep -q "zwift-${USER}"; then - mshgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" - if ${CONTAINER_TOOL} volume create "zwift-${USER}"; then - msgbox ok "Created volume zwift-${USER}" +if [[ ${ZWIFT_VARIANT} == "minimal" ]]; then + volume_name="zwift-home-${USER}" +else + volume_name="zwift-${USER}" +fi +if [[ ${CONTAINER_TOOL} == "podman" ]] && ! ${CONTAINER_TOOL} volume ls | grep -q "${volume_name}"; then + msgbox info "Creating ${CONTAINER_TOOL} volume ${volume_name}" + if ${CONTAINER_TOOL} volume create "${volume_name}"; then + msgbox ok "Created volume ${volume_name}" else - msgbox error "Failed to create volume zwift-${USER}" + msgbox error "Failed to create volume ${volume_name}" exit 1 fi fi From e5d966c9b572ea85537f33a93dd5c1413daa0723 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 09:04:05 +0100 Subject: [PATCH 2/8] Add dcompiler to cspell word list --- .cspell.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.cspell.yaml b/.cspell.yaml index 0bec19c2..b6865b83 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -2,6 +2,7 @@ version: '0.2' language: en words: +- dcompiler - devcontainer - direnv - hadolint From a7f49c53aef0298984e6179f496d988e42a43200 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 09:09:59 +0100 Subject: [PATCH 3/8] Use image-populated volume instead of separate minimal image Instead of a separate lightweight image that installs Zwift at first run, use the same pre-installed image with a /home/user volume mount. Docker and Podman auto-populate new named volumes from image contents, so the first launch is instant with no installation step needed. --- .github/workflows/zwift_build_minimal.yaml | 34 ---------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/zwift_build_minimal.yaml diff --git a/.github/workflows/zwift_build_minimal.yaml b/.github/workflows/zwift_build_minimal.yaml deleted file mode 100644 index d77ef086..00000000 --- a/.github/workflows/zwift_build_minimal.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Zwift build minimal -'on': - workflow_dispatch: - push: - branches: - - master - paths: - - src/Dockerfile -concurrency: - group: zwift-minimal-container-image -jobs: - zwift_build_minimal: - if: github.repository == 'netbrain/zwift' - runs-on: ubuntu-22.04 - timeout-minutes: 30 - steps: - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Checkout - uses: actions/checkout@v6 - - name: Build and push minimal image - run: | - build_date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" - - docker build \ - --label "org.opencontainers.image.created=${build_date}" \ - -t netbrain/zwift:minimal \ - src/ - - docker push netbrain/zwift:minimal From f45f373a48788daef28dffaac3f03ac54848b603 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 09:19:10 +0100 Subject: [PATCH 4/8] Rename minimal variant to volume variant --- docs/advanced/nixos.md | 2 +- .../{minimal-variant.md => volume-variant.md} | 18 +++++++++--------- docs/configuration/options.md | 6 +++--- docs/troubleshooting/slow-start.md | 4 ++-- flake.nix | 8 ++++---- src/entrypoint.sh | 6 +++--- src/update_zwift.sh | 4 ++-- src/zwift.sh | 6 +++--- 8 files changed, 27 insertions(+), 27 deletions(-) rename docs/advanced/{minimal-variant.md => volume-variant.md} (85%) diff --git a/docs/advanced/nixos.md b/docs/advanced/nixos.md index bcc3abf2..ef4be444 100644 --- a/docs/advanced/nixos.md +++ b/docs/advanced/nixos.md @@ -36,7 +36,7 @@ environment variables in camelCase: programs.zwift = { # Enable the zwift module and install required dependencies enable = true; - # Variant: "container" (pre-installed, default) or "minimal" (volume-based install) + # Variant: "container" (default) or "volume" (persistent /home/user volume) variant = "container"; # The Docker image to use for zwift image = "docker.io/netbrain/zwift"; diff --git a/docs/advanced/minimal-variant.md b/docs/advanced/volume-variant.md similarity index 85% rename from docs/advanced/minimal-variant.md rename to docs/advanced/volume-variant.md index 2e93142f..aad7eac9 100644 --- a/docs/advanced/minimal-variant.md +++ b/docs/advanced/volume-variant.md @@ -1,12 +1,12 @@ --- -title: Minimal Variant +title: Volume Variant parent: Advanced nav_order: 4 --- -# Minimal Variant (Volume-Based) +# Volume Variant -The default mode mounts only the Zwift documents directory as a volume. The **minimal variant** mounts the entire +The default mode mounts only the Zwift documents directory as a volume. The **volume variant** mounts the entire `/home/user` directory as a persistent volume instead. On first run, the container runtime automatically populates the volume from the image contents — no separate installation step needed. @@ -19,7 +19,7 @@ volume from the image contents — no separate installation step needed. ## How it works -| | Default (`container`) | Minimal (`minimal`) | +| | Default (`container`) | Volume (`volume`) | | --- | --- | --- | | **Image** | `netbrain/zwift:latest` | `netbrain/zwift:latest` (same image) | | **Volume** | `zwift-$USER` mounted at Zwift docs directory | `zwift-home-$USER` mounted at `/home/user` | @@ -36,13 +36,13 @@ installation wait. ### Environment variable ```bash -ZWIFT_VARIANT="minimal" zwift +ZWIFT_VARIANT="volume" zwift ``` Or add to your config file (`~/.config/zwift/config`): ```bash -ZWIFT_VARIANT="minimal" +ZWIFT_VARIANT="volume" ``` ### NixOS module @@ -51,7 +51,7 @@ ZWIFT_VARIANT="minimal" { programs.zwift = { enable = true; - variant = "minimal"; + variant = "volume"; }; } ``` @@ -59,7 +59,7 @@ ZWIFT_VARIANT="minimal" ### Standalone Nix package ```bash -nix run github:netbrain/zwift#zwift-minimal +nix run github:netbrain/zwift#zwift-volume ``` ## Updating Zwift @@ -68,7 +68,7 @@ Zwift updates itself through its built-in launcher. You can also force an update argument: ```bash -ZWIFT_VARIANT="minimal" zwift -- --update +ZWIFT_VARIANT="volume" zwift -- --update ``` ## Resetting the installation diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 4d700978..eac996ab 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -40,7 +40,7 @@ These environment variables can be used to alter the execution of the zwift bash | `DEBUG` | `0` | If set to `1`, enable debug of zwift script `set -x` | | `VGA_DEVICE_FLAG` | | Override GPU/device flags for container (`--gpus=all`) | | `PRIVILEGED_CONTAINER` | `0` | If set, container will run in privileged mode, SELinux label separation will be disabled (`--privileged --security-opt label=disable`) | -| `ZWIFT_VARIANT` | `container` | Set to `minimal` to mount `/home/user` as a persistent volume (faster startup, no per-launch chown) | +| `ZWIFT_VARIANT` | `container` | Set to `volume` to mount `/home/user` as a persistent volume (faster startup, no per-launch chown) | {: .important } `ZWIFT_UID` and `ZWIFT_GID` can only be used with X11. They do not work in wayland! @@ -60,8 +60,8 @@ These environment variables can be used to alter the execution of the zwift bash the user that started the container. You should not need to change this except in rare cases. - `WINE_EXPERIMENTAL_WAYLAND="1" zwift` This will start zwift using Wayland and not XWayland. It will start full screen windowed. -- `ZWIFT_VARIANT="minimal" zwift` will mount `/home/user` as a persistent volume, eliminating per-launch `chown` (see - [Minimal Variant]({% link advanced/minimal-variant.md %}) for details). +- `ZWIFT_VARIANT="volume" zwift` will mount `/home/user` as a persistent volume, eliminating per-launch `chown` (see + [Volume Variant]({% link advanced/volume-variant.md %}) for details). {: .note } > To pass extra environment variables to the container, they can be added to `CONTAINER_EXTRA_ARGS` with the `-e` flag. diff --git a/docs/troubleshooting/slow-start.md b/docs/troubleshooting/slow-start.md index 7b38b70a..bd3515fe 100644 --- a/docs/troubleshooting/slow-start.md +++ b/docs/troubleshooting/slow-start.md @@ -11,7 +11,7 @@ If your `$(id -u)` or `$(id -g)` is not equal to 1000 then this would cause the If speed is a concern, you have two options: -1. **Use the minimal variant** (`ZWIFT_VARIANT="minimal"`): This mounts the entire `/home/user` as a persistent volume, +1. **Use the volume variant** (`ZWIFT_VARIANT="volume"`): This mounts the entire `/home/user` as a persistent volume, so file ownership persists between runs and no `chown` is needed after the first launch. See - [Minimal Variant]({% link advanced/minimal-variant.md %}) for details. + [Volume Variant]({% link advanced/volume-variant.md %}) for details. 2. **Change your user IDs** to match the container's uid and gid (1000) using `usermod`. diff --git a/flake.nix b/flake.nix index 9898a532..31fd9591 100644 --- a/flake.nix +++ b/flake.nix @@ -115,10 +115,10 @@ variant = mkOption { type = enum [ "container" - "minimal" + "volume" ]; default = "container"; - description = "Zwift variant: 'container' (pre-installed) or 'minimal' (volume-based install on first run)"; + description = "Zwift variant: 'container' (default) or 'volume' (persistent /home/user volume, no per-launch chown)"; }; image = lib.mkOption { type = str; @@ -327,10 +327,10 @@ desktopItems = [ "bin/Zwift.desktop" ]; }; - zwift-minimal = wrapPackage { + zwift-volume = wrapPackage { image = ""; tag = ""; - variant = "minimal"; + variant = "volume"; dontCheck = ""; dontPull = ""; dontClean = ""; diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 51d63136..0e50ce82 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -25,7 +25,7 @@ readonly ZWIFT_UID="${ZWIFT_UID:-$(id -u user)}" readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g user)}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" -readonly ZWIFT_MINIMAL="${ZWIFT_MINIMAL:-0}" +readonly ZWIFT_VOLUME="${ZWIFT_VOLUME:-0}" readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" @@ -143,8 +143,8 @@ if [[ ${CONTAINER_TOOL} == "docker" ]]; then fi fi - if [[ ${ZWIFT_MINIMAL} -eq 1 ]]; then - msgbox ok "Minimal variant: skipping ownership update (persisted in volume)" + if [[ ${ZWIFT_VOLUME} -eq 1 ]]; then + msgbox ok "Volume variant: skipping ownership update (persisted in volume)" else msgbox info "Changing ownership from root to user" if update_ownership; then diff --git a/src/update_zwift.sh b/src/update_zwift.sh index 0d3c2ac3..c09c3ab1 100755 --- a/src/update_zwift.sh +++ b/src/update_zwift.sh @@ -22,7 +22,7 @@ else fi readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" -readonly ZWIFT_MINIMAL="${ZWIFT_MINIMAL:-0}" +readonly ZWIFT_VOLUME="${ZWIFT_VOLUME:-0}" readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" @@ -157,7 +157,7 @@ cleanup() { rm -rf -- "/home/user/.cache/wine*" || true # remove Zwift documents because it causes permission errors with podman # skip in minimal variant — ZWIFT_DOCS lives in the persistent volume - if [[ ${ZWIFT_MINIMAL} -ne 1 ]]; then + if [[ ${ZWIFT_VOLUME} -ne 1 ]]; then rm -rf -- "${ZWIFT_DOCS}" || true fi } diff --git a/src/zwift.sh b/src/zwift.sh index c43636d2..e672160b 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -332,9 +332,9 @@ container_args+=( --env-file "${container_env_file}" ) -if [[ ${ZWIFT_VARIANT} == "minimal" ]]; then +if [[ ${ZWIFT_VARIANT} == "volume" ]]; then container_args+=(-v "zwift-home-${USER}:/home/user") - container_env_vars+=(ZWIFT_MINIMAL="1") + container_env_vars+=(ZWIFT_VOLUME="1") else container_args+=(-v "zwift-${USER}:${ZWIFT_DOCS}") fi @@ -630,7 +630,7 @@ 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 [[ ${ZWIFT_VARIANT} == "minimal" ]]; then +if [[ ${ZWIFT_VARIANT} == "volume" ]]; then volume_name="zwift-home-${USER}" else volume_name="zwift-${USER}" From 36a9775f2df4cac1e520b542b3a480be7aabac3f Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 09:25:28 +0100 Subject: [PATCH 5/8] Fix doc links to use relative paths instead of Liquid tags --- docs/configuration/options.md | 2 +- docs/troubleshooting/slow-start.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index eac996ab..8d0d9d6d 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -61,7 +61,7 @@ These environment variables can be used to alter the execution of the zwift bash - `WINE_EXPERIMENTAL_WAYLAND="1" zwift` This will start zwift using Wayland and not XWayland. It will start full screen windowed. - `ZWIFT_VARIANT="volume" zwift` will mount `/home/user` as a persistent volume, eliminating per-launch `chown` (see - [Volume Variant]({% link advanced/volume-variant.md %}) for details). + [Volume Variant](../advanced/volume-variant.md) for details). {: .note } > To pass extra environment variables to the container, they can be added to `CONTAINER_EXTRA_ARGS` with the `-e` flag. diff --git a/docs/troubleshooting/slow-start.md b/docs/troubleshooting/slow-start.md index bd3515fe..d5ff9d26 100644 --- a/docs/troubleshooting/slow-start.md +++ b/docs/troubleshooting/slow-start.md @@ -13,5 +13,5 @@ If speed is a concern, you have two options: 1. **Use the volume variant** (`ZWIFT_VARIANT="volume"`): This mounts the entire `/home/user` as a persistent volume, so file ownership persists between runs and no `chown` is needed after the first launch. See - [Volume Variant]({% link advanced/volume-variant.md %}) for details. + [Volume Variant](../advanced/volume-variant.md) for details. 2. **Change your user IDs** to match the container's uid and gid (1000) using `usermod`. From acbdaab9523e2e5cb861cadaa9eccb4b1d48fb79 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 09:52:54 +0100 Subject: [PATCH 6/8] Add pre-launch volume sync from image for volume variant Before launching Zwift, compare the image version (OCI label) against a marker in the volume. If they differ, run an init container that rsyncs the image /home/user into the volume, preserving user data (activities, workouts, logs, prefs, screenshots). This keeps the volume up to date with the image without network downloads or image size duplication. --- .../workflows/zwift_build_from_scratch.yaml | 1 + docs/advanced/volume-variant.md | 7 +++- src/Dockerfile | 1 + src/zwift.sh | 40 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zwift_build_from_scratch.yaml b/.github/workflows/zwift_build_from_scratch.yaml index ec309fe1..75e83341 100644 --- a/.github/workflows/zwift_build_from_scratch.yaml +++ b/.github/workflows/zwift_build_from_scratch.yaml @@ -54,6 +54,7 @@ jobs: docker commit \ --change="LABEL org.opencontainers.image.created=${build_date}" \ --change="LABEL org.opencontainers.image.version=${version}" \ + --change="ENV ZWIFT_IMAGE_VERSION=${version}" \ --change='CMD [""]' \ -m "built from scratch from version ${version}" \ zwift \ diff --git a/docs/advanced/volume-variant.md b/docs/advanced/volume-variant.md index aad7eac9..358dbfb7 100644 --- a/docs/advanced/volume-variant.md +++ b/docs/advanced/volume-variant.md @@ -64,8 +64,11 @@ nix run github:netbrain/zwift#zwift-volume ## Updating Zwift -Zwift updates itself through its built-in launcher. You can also force an update by passing `--update` as an entrypoint -argument: +When a new container image is pulled, the launch script automatically detects the version mismatch and syncs the updated +files from the image into the volume. User data (activities, workouts, logs, preferences, screenshots) is preserved +during the sync. + +You can also force an update manually by passing `--update` as an entrypoint argument: ```bash ZWIFT_VARIANT="volume" zwift -- --update diff --git a/src/Dockerfile b/src/Dockerfile index c797f079..07a23dcf 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -53,6 +53,7 @@ RUN dpkg --add-architecture i386 \ libgl1 \ libvulkan1 \ procps \ + rsync \ sudo \ wget \ winbind \ diff --git a/src/zwift.sh b/src/zwift.sh index e672160b..aa28a635 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -645,6 +645,46 @@ if [[ ${CONTAINER_TOOL} == "podman" ]] && ! ${CONTAINER_TOOL} volume ls | grep - fi fi +# Volume variant: sync image contents into volume if image version changed +if [[ ${ZWIFT_VARIANT} == "volume" ]]; then + image_version="" + if image_version="$(${CONTAINER_TOOL} inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}}' "${IMAGE}:${VERSION}" 2> /dev/null)"; then + msgbox info "Image version: ${image_version}" + fi + + if [[ -n ${image_version} ]]; then + volume_version="$(${CONTAINER_TOOL} run --rm -v "${volume_name}:/mnt/volume:ro" "${IMAGE}:${VERSION}" cat /mnt/volume/.zwift-image-version 2> /dev/null || true)" + + if [[ ${image_version} != "${volume_version}" ]]; then + msgbox info "Volume outdated (${volume_version:-empty}), syncing from image (${image_version})..." + if ${CONTAINER_TOOL} run --rm \ + -v "${volume_name}:/mnt/volume" \ + "${IMAGE}:${VERSION}" \ + rsync -a --delete \ + --exclude "AppData/Local/Zwift/Activities/" \ + --exclude "AppData/Local/Zwift/Workouts/" \ + --exclude "AppData/Local/Zwift/Logs/" \ + --exclude "AppData/Local/Zwift/prefs.xml" \ + --exclude "Pictures/Zwift/" \ + /home/user/ /mnt/volume/; then + + ${CONTAINER_TOOL} run --rm \ + -v "${volume_name}:/mnt/volume" \ + "${IMAGE}:${VERSION}" \ + sh -c "echo '${image_version}' > /mnt/volume/.zwift-image-version" + + msgbox ok "Volume synced to image version ${image_version}" + else + msgbox error "Failed to sync volume from image" + fi + else + msgbox ok "Volume is up to date (${image_version})" + fi + else + msgbox warning "Could not determine image version, skipping volume sync" + 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 69e13ac617dcc47b3f10193a6c6f5fdbe3cf20ab Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 10:13:14 +0100 Subject: [PATCH 7/8] Fix sync containers to bypass entrypoint --- src/zwift.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/zwift.sh b/src/zwift.sh index aa28a635..7dbbaa3f 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -653,14 +653,14 @@ if [[ ${ZWIFT_VARIANT} == "volume" ]]; then fi if [[ -n ${image_version} ]]; then - volume_version="$(${CONTAINER_TOOL} run --rm -v "${volume_name}:/mnt/volume:ro" "${IMAGE}:${VERSION}" cat /mnt/volume/.zwift-image-version 2> /dev/null || true)" + volume_version="$(${CONTAINER_TOOL} run --rm --entrypoint cat -v "${volume_name}:/mnt/volume:ro" "${IMAGE}:${VERSION}" /mnt/volume/.zwift-image-version 2> /dev/null || true)" if [[ ${image_version} != "${volume_version}" ]]; then msgbox info "Volume outdated (${volume_version:-empty}), syncing from image (${image_version})..." - if ${CONTAINER_TOOL} run --rm \ + if ${CONTAINER_TOOL} run --rm --entrypoint rsync \ -v "${volume_name}:/mnt/volume" \ "${IMAGE}:${VERSION}" \ - rsync -a --delete \ + -a --delete \ --exclude "AppData/Local/Zwift/Activities/" \ --exclude "AppData/Local/Zwift/Workouts/" \ --exclude "AppData/Local/Zwift/Logs/" \ @@ -668,10 +668,10 @@ if [[ ${ZWIFT_VARIANT} == "volume" ]]; then --exclude "Pictures/Zwift/" \ /home/user/ /mnt/volume/; then - ${CONTAINER_TOOL} run --rm \ + ${CONTAINER_TOOL} run --rm --entrypoint sh \ -v "${volume_name}:/mnt/volume" \ "${IMAGE}:${VERSION}" \ - sh -c "echo '${image_version}' > /mnt/volume/.zwift-image-version" + -c "echo '${image_version}' > /mnt/volume/.zwift-image-version" msgbox ok "Volume synced to image version ${image_version}" else From e50eba0c34e5d33360493dca565d8b4c780f6363 Mon Sep 17 00:00:00 2001 From: Kim Eik Date: Thu, 12 Mar 2026 22:33:41 +0100 Subject: [PATCH 8/8] Update docs/advanced/volume-variant.md Co-authored-by: Glenn --- docs/advanced/volume-variant.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/advanced/volume-variant.md b/docs/advanced/volume-variant.md index 358dbfb7..8d86fb21 100644 --- a/docs/advanced/volume-variant.md +++ b/docs/advanced/volume-variant.md @@ -19,13 +19,13 @@ volume from the image contents — no separate installation step needed. ## How it works -| | Default (`container`) | Volume (`volume`) | -| --- | --- | --- | -| **Image** | `netbrain/zwift:latest` | `netbrain/zwift:latest` (same image) | -| **Volume** | `zwift-$USER` mounted at Zwift docs directory | `zwift-home-$USER` mounted at `/home/user` | -| **First run** | Seconds | Seconds (volume auto-populated from image) | -| **Subsequent runs** | Seconds | Seconds | -| **chown on launch** | Every launch if uid/gid != 1000 | Skipped (ownership persists in volume) | +| | Default (`container`) | Volume (`volume`) | +|---------------------|-----------------------------------------------|--------------------------------------------| +| **Image** | `netbrain/zwift:latest` | `netbrain/zwift:latest` (same image) | +| **Volume** | `zwift-$USER` mounted at Zwift docs directory | `zwift-home-$USER` mounted at `/home/user` | +| **First run** | Seconds | Seconds (volume auto-populated from image) | +| **Subsequent runs** | Seconds | Seconds | +| **chown on launch** | Every launch if uid/gid != 1000 | Skipped (ownership persists in volume) | When a new named volume is mounted to a container path that already has data, Docker (and Podman) automatically copy the image contents into the volume. This means the first launch is just as fast as any other — no download or