--- - name: Ensure keyrings directory exists (Debian/Ubuntu) ansible.builtin.file: path: "/etc/apt/keyrings" state: directory mode: "0755" when: ansible_facts['os_family'] == 'Debian' - name: Download and dearmor Docker GPG key (Debian/Ubuntu) ansible.builtin.shell: | set -o pipefail curl -fsSL "https://download.docker.com/linux/{{ ansible_facts['distribution'] | lower }}/gpg" \ | gpg --dearmor --yes -o {{ gitea_runner_docker_gpg_key_path }} args: creates: "{{ gitea_runner_docker_gpg_key_path }}" executable: /bin/bash when: ansible_facts['os_family'] == 'Debian' - name: Add Docker APT repository (Debian/Ubuntu) ansible.builtin.copy: dest: /etc/apt/sources.list.d/docker.list content: "{{ gitea_runner_docker_apt_source_line }}\n" mode: "0644" register: gitea_runner_docker_apt_repo when: ansible_facts['os_family'] == 'Debian' - name: Update apt cache after adding Docker repo (Debian/Ubuntu) ansible.builtin.apt: update_cache: true when: - ansible_facts['os_family'] == 'Debian' - gitea_runner_docker_apt_repo is changed # 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: - uidmap - slirp4netns - passt - fuse-overlayfs - docker-ce - docker-ce-cli - docker-ce-rootless-extras - containerd.io - docker-compose-plugin - rsync state: present register: gitea_runner_docker_install when: ansible_facts['os_family'] == 'Debian' - name: Update pacman cache (Arch Linux) community.general.pacman: update_cache: true when: ansible_facts['os_family'] == 'Archlinux' changed_when: false - name: Install rootless Docker dependencies (Arch Linux) community.general.pacman: name: - docker - docker-compose - slirp4netns - passt - fuse-overlayfs - rsync # rootlesskit is the userspace networking/namespace driver for rootless Docker. # It is NOT a dependency of the "docker" package on Arch and must be installed explicitly. - rootlesskit state: present when: ansible_facts['os_family'] == 'Archlinux' # The tun kernel module is required by rootlesskit (both slirp4netns and pasta # drivers create a tap device inside a user namespace). On Arch Linux, CONFIG_TUN=m # so the module must be loaded. If the running kernel doesn't match the installed # kernel (e.g. after a pacman -Syu that upgraded linux but didn't reboot), modprobe # will fail — in that case we warn but don't fail, as a reboot will fix it. - name: Load tun kernel module for rootless networking community.general.modprobe: name: tun state: present ignore_errors: true register: gitea_runner_tun_module when: ansible_facts['os_family'] == 'Archlinux' - name: Warn if tun module could not be loaded (kernel mismatch — reboot needed) ansible.builtin.debug: msg: >- WARNING: Could not load the tun kernel module. This is likely because the running kernel ({{ ansible_facts['kernel'] }}) does not match the installed kernel modules. A reboot is required before rootless Docker can start. when: - ansible_facts['os_family'] == 'Archlinux' - gitea_runner_tun_module is failed # Arch Linux's "docker" package does not ship the rootless setup scripts (dockerd-rootless-setuptool.sh # and dockerd-rootless.sh), unlike Debian/Ubuntu's docker-ce-rootless-extras. No official Arch package # provides them, so fetch them from the upstream moby/moby contrib/ directory. They are installed # alongside the docker binaries (/usr/bin) because dockerd-rootless-setuptool.sh derives its BIN # directory from its own location and expects docker/dockerd/rootlesskit to be co-located there. - name: Fetch rootless Docker setup scripts (Arch Linux) ansible.builtin.get_url: url: "https://raw.githubusercontent.com/moby/moby/{{ gitea_runner_rootless_scripts_ref }}/contrib/{{ item.name }}" dest: "{{ gitea_runner_rootless_scripts_install_dir }}/{{ item.name }}" mode: "0755" owner: root group: root loop: - name: dockerd-rootless-setuptool.sh - name: dockerd-rootless.sh when: ansible_facts['os_family'] == 'Archlinux' - name: Check if rootless Docker is already set up ansible.builtin.stat: path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service" register: gitea_runner_rootless_docker_check # Create the systemd override BEFORE running the setuptool so that when # the setuptool starts docker.service, it picks up the pasta network driver # instead of the default slirp4netns (which may fail on some kernels). - name: Ensure systemd user override directory exists ansible.builtin.file: path: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d" 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 network driver override ansible.builtin.copy: dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf" content: | [Service] Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}" 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 }}" when: - gitea_runner_docker_rootless_setup - not gitea_runner_rootless_docker_check.stat.exists - name: Set up rootless Docker for runner user ansible.builtin.command: dockerd-rootless-setuptool.sh install args: creates: "{{ gitea_runner_home }}/.config/systemd/user/docker.service" become: true 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 - not gitea_runner_rootless_docker_check.stat.exists - name: Start rootless Docker daemon (systemd user service) ansible.builtin.command: systemctl --user start docker become: true 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 - name: Enable rootless Docker daemon (systemd user service) ansible.builtin.command: systemctl --user enable docker become: true 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 - name: Ensure Docker config directory exists 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 - name: Configure rootless Docker network driver ansible.builtin.copy: dest: "{{ gitea_runner_home }}/.config/systemd/user/docker.service.d/override.conf" content: | [Service] Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET={{ gitea_runner_docker_rootless_net_driver }}" 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 }}" register: gitea_runner_docker_network_override when: gitea_runner_docker_rootless_setup - name: Reload systemd user daemon if network config changed ansible.builtin.command: systemctl --user daemon-reload become: true 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 - gitea_runner_docker_network_override is changed - name: Configure rootless Docker daemon 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 }}" register: gitea_runner_docker_ipv6_config when: gitea_runner_docker_rootless_setup - name: Restart rootless Docker if config changed ansible.builtin.command: systemctl --user restart docker become: true 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 - gitea_runner_docker_ipv6_config is changed or gitea_runner_docker_network_override is changed - name: Wait for rootless Docker daemon to be ready ansible.builtin.command: docker version become: true become_user: "{{ gitea_runner_service_user }}" 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 delay: 2 changed_when: false when: gitea_runner_docker_rootless_setup