From e32ba51a7db5ad62ed5f5a83704d452663add91f Mon Sep 17 00:00:00 2001 From: Emil Simeonov Date: Thu, 6 Aug 2026 00:05:04 +0200 Subject: [PATCH 1/3] fix: pre-configure daemon.json before rootless setuptool + add DBUS_SESSION_BUS_ADDRESS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fixes for rootless Docker setup on Arch Linux runners: 1. Write daemon.json (with containerd-snapshotter: false) BEFORE running dockerd-rootless-setuptool.sh, so Docker 29 starts with overlay2 from the very first boot instead of containerd snapshots. 2. Add DBUS_SESSION_BUS_ADDRESS to all systemctl --user tasks. Without it, systemctl --user fails with "Connection refused" on Arch Linux where the D-Bus session bus isn't auto-discovered. 3. Fix pre-configure override port driver: use 'builtin' for slirp4netns and 'implicit' only for pasta. The previous unconditional 'implicit' caused dockerd to exit immediately on slirp4netns hosts. 4. Fix prune timer schedule from '*-* * */6:00:00' (invalid) to '*-*-* 00/6:00:00' (every 6 hours, valid systemd calendar spec). 5. Add explicit Docker 29→28 downgrade task for Debian/Ubuntu using version-pinned apt install with allow_downgrades. Closes GRM-158 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ansible/roles/gitea_runner/defaults/main.yml | 2 +- .../roles/gitea_runner/tasks/deregister.yml | 1 + .../roles/gitea_runner/tasks/healthcheck.yml | 2 + .../gitea_runner/tasks/integration_test.yml | 1 + .../gitea_runner/tasks/pre_pull_images.yml | 1 + ansible/roles/gitea_runner/tasks/prune.yml | 2 + ansible/roles/gitea_runner/tasks/register.yml | 1 + .../gitea_runner/tasks/rootless_docker.yml | 90 ++++++++++++++++++- ansible/roles/gitea_runner/tasks/service.yml | 2 + .../gitea_runner/tasks/update_runner.yml | 1 + ansible/roles/gitea_runner/tasks/validate.yml | 1 + 11 files changed, 102 insertions(+), 2 deletions(-) diff --git a/ansible/roles/gitea_runner/defaults/main.yml b/ansible/roles/gitea_runner/defaults/main.yml index 8522194..43b8e96 100644 --- a/ansible/roles/gitea_runner/defaults/main.yml +++ b/ansible/roles/gitea_runner/defaults/main.yml @@ -22,7 +22,7 @@ gitea_runner_prune_until: "24h" # of images per day. Accumulation between daily runs can trigger Docker # daemon instability (containerd snapshotter GC holds locks, blocking # container operations). -gitea_runner_prune_schedule: "*-* * */6:00:00" +gitea_runner_prune_schedule: "*-*-* 00/6:00:00" gitea_runner_prune_label: "gitea-runner=true" # Service configuration diff --git a/ansible/roles/gitea_runner/tasks/deregister.yml b/ansible/roles/gitea_runner/tasks/deregister.yml index 517eabe..26ff643 100644 --- a/ansible/roles/gitea_runner/tasks/deregister.yml +++ b/ansible/roles/gitea_runner/tasks/deregister.yml @@ -29,6 +29,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock" when: - gitea_runner_file_stat.stat.exists | default(false) | bool diff --git a/ansible/roles/gitea_runner/tasks/healthcheck.yml b/ansible/roles/gitea_runner/tasks/healthcheck.yml index 1189437..a85d4e1 100644 --- a/ansible/roles/gitea_runner/tasks/healthcheck.yml +++ b/ansible/roles/gitea_runner/tasks/healthcheck.yml @@ -29,6 +29,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists @@ -40,6 +41,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists diff --git a/ansible/roles/gitea_runner/tasks/integration_test.yml b/ansible/roles/gitea_runner/tasks/integration_test.yml index 36a8c39..cdadd8c 100644 --- a/ansible/roles/gitea_runner/tasks/integration_test.yml +++ b/ansible/roles/gitea_runner/tasks/integration_test.yml @@ -24,6 +24,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" register: gitea_runner_service_check changed_when: false retries: 10 diff --git a/ansible/roles/gitea_runner/tasks/pre_pull_images.yml b/ansible/roles/gitea_runner/tasks/pre_pull_images.yml index f543928..ed0f265 100644 --- a/ansible/roles/gitea_runner/tasks/pre_pull_images.yml +++ b/ansible/roles/gitea_runner/tasks/pre_pull_images.yml @@ -16,6 +16,7 @@ environment: DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" register: gitea_runner_pre_pull_result changed_when: "'Status: Downloaded' in gitea_runner_pre_pull_result.stdout or 'Status: Downloaded' in gitea_runner_pre_pull_result.stderr" retries: 3 diff --git a/ansible/roles/gitea_runner/tasks/prune.yml b/ansible/roles/gitea_runner/tasks/prune.yml index 3494632..e668a67 100644 --- a/ansible/roles/gitea_runner/tasks/prune.yml +++ b/ansible/roles/gitea_runner/tasks/prune.yml @@ -21,6 +21,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists @@ -32,6 +33,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists diff --git a/ansible/roles/gitea_runner/tasks/register.yml b/ansible/roles/gitea_runner/tasks/register.yml index fd8c7d1..152d921 100644 --- a/ansible/roles/gitea_runner/tasks/register.yml +++ b/ansible/roles/gitea_runner/tasks/register.yml @@ -26,6 +26,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid | default(0) }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid | default(0) }}/docker.sock" when: not gitea_runner_registered.stat.exists register: gitea_runner_register_output diff --git a/ansible/roles/gitea_runner/tasks/rootless_docker.yml b/ansible/roles/gitea_runner/tasks/rootless_docker.yml index 94b40c6..d70ea0a 100644 --- a/ansible/roles/gitea_runner/tasks/rootless_docker.yml +++ b/ansible/roles/gitea_runner/tasks/rootless_docker.yml @@ -45,8 +45,20 @@ Pin: version 5:28.* Pin-Priority: 1000 mode: "0644" + register: gitea_runner_docker_pin when: ansible_facts['os_family'] == 'Debian' +- name: Update apt cache after pinning Docker (Debian/Ubuntu) + ansible.builtin.apt: + update_cache: true + when: + - ansible_facts['os_family'] == 'Debian' + - gitea_runner_docker_pin is changed + +# Install Docker packages. The APT pin (priority 1000) makes 28.x the +# candidate, so state: present installs 28.x on fresh systems. On systems +# where Docker 29 was previously installed, a separate downgrade task +# handles the version pin. - name: Install rootless Docker dependencies (Debian/Ubuntu) ansible.builtin.apt: name: @@ -61,8 +73,29 @@ - docker-compose-plugin - rsync state: present + register: gitea_runner_docker_install when: ansible_facts['os_family'] == 'Debian' +# Downgrade Docker 29 → 28 if needed. state: present won't downgrade, +# so we check the installed version and force-install the pinned version. +- name: Check installed Docker version (Debian/Ubuntu) + ansible.builtin.command: dpkg-query -W -f='${Version}' docker-ce + changed_when: false + register: gitea_runner_installed_docker_version + when: ansible_facts['os_family'] == 'Debian' + +- name: Downgrade Docker to 28.x if 29.x is installed (Debian/Ubuntu) + ansible.builtin.apt: + name: + - docker-ce=5:28.* + - docker-ce-cli=5:28.* + - docker-ce-rootless-extras=5:28.* + state: present + allow_downgrades: true + when: + - ansible_facts['os_family'] == 'Debian' + - "'29.' in gitea_runner_installed_docker_version.stdout" + - name: Update pacman cache (Arch Linux) community.general.pacman: update_cache: true @@ -149,8 +182,57 @@ content: | [Service] Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}" - Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=implicit" + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER={{ 'implicit' if gitea_runner_docker_rootless_net_driver == 'pasta' else 'builtin' }}" + {% if gitea_runner_docker_rootless_net_driver == 'pasta' %} Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=--ipv6" + {% endif %} + mode: "0644" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + when: + - gitea_runner_docker_rootless_setup + - not gitea_runner_rootless_docker_check.stat.exists + +# Write daemon.json BEFORE the setuptool starts dockerd, so Docker 29 +# starts with containerd snapshotter disabled from the very first boot. +# Without this, Docker 29 uses containerd snapshots by default, which +# causes instability in rootless mode. +- name: Ensure Docker config directory exists (pre-setup) + ansible.builtin.file: + path: "{{ gitea_runner_home }}/.config/docker" + state: directory + mode: "0755" + owner: "{{ gitea_runner_service_user }}" + group: "{{ gitea_runner_service_user }}" + when: + - gitea_runner_docker_rootless_setup + - not gitea_runner_rootless_docker_check.stat.exists + +- name: Pre-configure rootless Docker daemon.json (disable containerd snapshotter) + ansible.builtin.copy: + dest: "{{ gitea_runner_home }}/.config/docker/daemon.json" + content: | + { + "live-restore": {{ gitea_runner_docker_live_restore | to_json }}, + "shutdown-timeout": {{ gitea_runner_docker_shutdown_timeout }}, + "max-concurrent-downloads": {{ gitea_runner_docker_max_concurrent_downloads }}, + "max-concurrent-uploads": {{ gitea_runner_docker_max_concurrent_uploads }}, + "default-ulimits": { + "nofile": {"Name": "nofile", "Hard": {{ gitea_runner_docker_default_nofile }}, "Soft": {{ gitea_runner_docker_default_nofile }}} + }, + "features": { + "containerd-snapshotter": false + }, + {% if gitea_runner_docker_rootless_net_driver == 'pasta' %} + "ipv6": true, + "ip6tables": true, + "fixed-cidr-v6": "{{ gitea_runner_docker_ipv6_cidr }}", + "dns": ["10.0.2.3", "8.8.8.8"] + {% else %} + "ipv6": false, + "dns": ["8.8.8.8", "1.1.1.1"] + {% endif %} + } mode: "0644" owner: "{{ gitea_runner_service_user }}" group: "{{ gitea_runner_service_user }}" @@ -166,6 +248,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" DOCKERD_ROOTLESS_ROOTLESSKIT_NET: "{{ gitea_runner_docker_rootless_net_driver }}" when: - gitea_runner_docker_rootless_setup @@ -177,6 +260,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" changed_when: true when: gitea_runner_docker_rootless_setup @@ -186,6 +270,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" changed_when: true when: gitea_runner_docker_rootless_setup @@ -220,6 +305,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" changed_when: true when: - gitea_runner_docker_rootless_setup @@ -262,6 +348,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" changed_when: true when: - gitea_runner_docker_rootless_setup @@ -274,6 +361,7 @@ environment: DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid }}/bus" register: gitea_runner_docker_ready until: gitea_runner_docker_ready.rc == 0 retries: 10 diff --git a/ansible/roles/gitea_runner/tasks/service.yml b/ansible/roles/gitea_runner/tasks/service.yml index 8b63486..62a02b9 100644 --- a/ansible/roles/gitea_runner/tasks/service.yml +++ b/ansible/roles/gitea_runner/tasks/service.yml @@ -13,6 +13,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists @@ -24,6 +25,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" changed_when: true when: - gitea_runner_systemd_available.stat.exists diff --git a/ansible/roles/gitea_runner/tasks/update_runner.yml b/ansible/roles/gitea_runner/tasks/update_runner.yml index 5b306e7..dbd10f7 100644 --- a/ansible/roles/gitea_runner/tasks/update_runner.yml +++ b/ansible/roles/gitea_runner/tasks/update_runner.yml @@ -8,6 +8,7 @@ become_user: "{{ gitea_runner_service_user }}" environment: XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" when: - gitea_runner_systemd_available.stat.exists | default(false) | bool - gitea_runner_docker_rootless_setup diff --git a/ansible/roles/gitea_runner/tasks/validate.yml b/ansible/roles/gitea_runner/tasks/validate.yml index 7a3327b..d6ba1f2 100644 --- a/ansible/roles/gitea_runner/tasks/validate.yml +++ b/ansible/roles/gitea_runner/tasks/validate.yml @@ -21,6 +21,7 @@ environment: DOCKER_HOST: "unix:///run/user/{{ gitea_runner_uid }}/docker.sock" XDG_RUNTIME_DIR: "/run/user/{{ gitea_runner_uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ gitea_runner_uid | default(0) }}/bus" register: gitea_runner_docker_version_output changed_when: false when: gitea_runner_docker_rootless_setup -- 2.54.0 From 8ffd8436c87b0037bc30f606be2b140317cfcf6c Mon Sep 17 00:00:00 2001 From: Emil Simeonov Date: Thu, 6 Aug 2026 00:33:42 +0200 Subject: [PATCH 2/3] fix: lower nofile ulimit to 65536 for rootless Docker compatibility Rootless Docker cannot set RLIMIT_NOFILE to 1048576 on all hosts (notably Arch Linux with Docker 29), causing container creation to fail with "error setting rlimit type 7: operation not permitted". 65536 is a safe value that works across all supported platforms. Closes GRM-158 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ansible/roles/gitea_runner/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/gitea_runner/defaults/main.yml b/ansible/roles/gitea_runner/defaults/main.yml index 43b8e96..92f8111 100644 --- a/ansible/roles/gitea_runner/defaults/main.yml +++ b/ansible/roles/gitea_runner/defaults/main.yml @@ -50,7 +50,7 @@ gitea_runner_docker_live_restore: true gitea_runner_docker_shutdown_timeout: 30 gitea_runner_docker_max_concurrent_downloads: 3 gitea_runner_docker_max_concurrent_uploads: 3 -gitea_runner_docker_default_nofile: 1048576 +gitea_runner_docker_default_nofile: 65536 # Admin token for runner deregistration via Gitea API. # If not set, falls back to registration_token (which likely lacks admin scope). -- 2.54.0 From bdb6521d2a5abb854b1a9926bc167e9d42a2cce8 Mon Sep 17 00:00:00 2001 From: Emil Simeonov Date: Thu, 6 Aug 2026 01:34:46 +0200 Subject: [PATCH 3/3] fix(ansible): remove Docker 28.x pin/downgrade in gitea_runner Docker 28.x packages are not available for recent Ubuntu releases (e.g. 26.04/plucky) in the official Docker APT repo. Keep installing the latest Docker and rely on daemon.json to disable the containerd snapshotter and set a conservative nofile ulimit for rootless mode. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../gitea_runner/tasks/rootless_docker.yml | 53 ++----------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/ansible/roles/gitea_runner/tasks/rootless_docker.yml b/ansible/roles/gitea_runner/tasks/rootless_docker.yml index d70ea0a..8f087dc 100644 --- a/ansible/roles/gitea_runner/tasks/rootless_docker.yml +++ b/ansible/roles/gitea_runner/tasks/rootless_docker.yml @@ -31,34 +31,11 @@ - ansible_facts['os_family'] == 'Debian' - gitea_runner_docker_apt_repo is changed -# Pin Docker to 28.x — Docker 29 enables the containerd image store -# (containerd snapshotter) by default, which has critical instability -# issues with rootless Docker: "context deadline exceeded" when starting -# dockerd with many images, snapshot GC holding locks for 30+ seconds -# blocking container operations, and broken rootless overlayfs on -# kernels < 5.11. See moby/moby issues #48569, #11021, #53029. -- name: Pin Docker to 28.x to avoid containerd snapshotter instability (Debian/Ubuntu) - ansible.builtin.copy: - dest: /etc/apt/preferences.d/docker-pin - content: | - Package: docker-ce docker-ce-cli docker-ce-rootless-extras containerd.io - Pin: version 5:28.* - Pin-Priority: 1000 - mode: "0644" - register: gitea_runner_docker_pin - when: ansible_facts['os_family'] == 'Debian' - -- name: Update apt cache after pinning Docker (Debian/Ubuntu) - ansible.builtin.apt: - update_cache: true - when: - - ansible_facts['os_family'] == 'Debian' - - gitea_runner_docker_pin is changed - -# Install Docker packages. The APT pin (priority 1000) makes 28.x the -# candidate, so state: present installs 28.x on fresh systems. On systems -# where Docker 29 was previously installed, a separate downgrade task -# handles the version pin. +# Install Docker packages from the upstream Docker APT repository. +# We do NOT pin to 28.x because recent Ubuntu releases (e.g. 26.04/plucky) +# may not have 28.x packages in the Docker repo, and Docker 29 is safe +# for rootless mode when the daemon.json disables the containerd snapshotter +# and sets a conservative default nofile ulimit (see daemon.json tasks below). - name: Install rootless Docker dependencies (Debian/Ubuntu) ansible.builtin.apt: name: @@ -76,26 +53,6 @@ register: gitea_runner_docker_install when: ansible_facts['os_family'] == 'Debian' -# Downgrade Docker 29 → 28 if needed. state: present won't downgrade, -# so we check the installed version and force-install the pinned version. -- name: Check installed Docker version (Debian/Ubuntu) - ansible.builtin.command: dpkg-query -W -f='${Version}' docker-ce - changed_when: false - register: gitea_runner_installed_docker_version - when: ansible_facts['os_family'] == 'Debian' - -- name: Downgrade Docker to 28.x if 29.x is installed (Debian/Ubuntu) - ansible.builtin.apt: - name: - - docker-ce=5:28.* - - docker-ce-cli=5:28.* - - docker-ce-rootless-extras=5:28.* - state: present - allow_downgrades: true - when: - - ansible_facts['os_family'] == 'Debian' - - "'29.' in gitea_runner_installed_docker_version.stdout" - - name: Update pacman cache (Arch Linux) community.general.pacman: update_cache: true -- 2.54.0