How to upgrade act_runner to Gitea Runner 5 in Docker without breaking workflows
Table of contents
- Key takeaways
- Why is your runner still on act_runner 0.6.1?
- What changes between act_runner 0.6.1 and Gitea Runner 5
- How I reproduced the upgrade
- Error: parse config file: ${DEPLOY_ENV} is not set
- is not a valid volume, will be ignored
- job requires a non-empty 'runs-on'
- exec: "act_runner": executable file not found in $PATH
- How to check your config.yaml before you switch images
- What happens to the registration and how to roll back
- Frequently asked questions
- Do I have to register the runner again when moving to gitea/runner?
- Can I keep using gitea/act_runner:latest?
- Does Gitea Runner 5 work with a Gitea server older than 28?
- Conclusion
- Sources
The gitea/act_runner image has been frozen at v0.6.1 since 30 April 2026; the runner now lives in gitea/runner and is at 5.2.4. The jump keeps your registration, but three changes break jobs: ${VAR} in config.yaml, the valid_volumes wildcard and jobs without runs-on.
To upgrade act_runner to Gitea Runner 5, swap the gitea/act_runner image for gitea/runner:5.2.3, keep the /data volume, and check three things in your config.yaml and workflows before you start it. The old image stopped being published on 30 April 2026 and is still at v0.6.1, so latest no longer brings you anything new. The jump keeps your registration, but in my test it broke three jobs that passed on 0.6.1: an unset ${VAR} in the config, a valid_volumes wildcard and a job without runs-on. I tested it on 27 October 2026 on arm64 Linux with Gitea 28.2.0 and Docker 29.5.2. There is a Spanish version of this guide.
Key takeaways
gitea/act_runner:latestis still v0.6.1, published on 30 April 2026; Docker Hub labels it "Deprecated Gitea runner. Moved to gitea/runner".- There are 36 releases and five major versions between v0.6.1 and v5.2.3, published in under six months.
- The 0.6.1
.runnerfile works as is: the runner kept id 1 in Gitea and did not need registering again. - An unset
${VAR}inconfig.yamlleaves the container looping on${DEPLOY_ENV} is not set; a set${HOME}expands without warning. - Since v4.0.0,
*invalid_volumesno longer crosses/, and since v5.0.0 a job withoutruns-onfails instead of running.
If you do not have Gitea running yet, start with the guide to install Gitea with Docker. If you are also upgrading the server, follow the steps to upgrade Gitea 1.27 to Gitea 28 with Docker first: runner 5 worked against Gitea 28.2.0 in this test.
Why is your runner still on act_runner 0.6.1?
Because the project changed its name and the old image stood still. On 5 May 2026 Gitea released version 1.0.0 under the new name. bircni, from Gitea’s Technical Oversight Committee, signed the Gitea blog announcement[1]. This was the summary: "The project formerly known as act_runner has been renamed to Gitea Runner, with the repository now available at gitea/runner." The same announcement described the migration as "a straight rename", and at 1.0.0 that was true.
The project has moved fast since then. These are the figures I read on 28 October 2026 from the Docker Hub and Gitea APIs:
gitea/act_runner: 11,109,390 pulls, last updated on 30 April 2026, newest tag0.6.1gitea/runner: 1,742,227 pulls, last updated on 28 October 2026, newest tag5.2.4, which fixesupload-artifactv4 in host mode (this guide’s test uses 5.2.3)- Releases published: 37 since v0.6.1, with major versions 1.0.0, 2.0.0, 3.0.0, 4.0.0 and 5.0.0
The first figure matters: far more runners use the old image than the new one. Check what yours runs with this command:
$ docker run --rm --entrypoint act_runner gitea/act_runner:latest --version
act_runner version v0.6.1
What changes between act_runner 0.6.1 and Gitea Runner 5
The rename is the least of it; what breaks jobs is the major versions that came afterwards. These are the ones that affect a Docker install, taken from each release’s notes:
| Version | Date | Breaking change |
|---|---|---|
| 1.0.0 | 5 May 2026 | Binary act_runner becomes gitea-runner, image becomes gitea/runner, metrics act_runner_* become gitea_runner_* |
| 2.0.0 | 30 Jun 2026 | The DOCKER_USERNAME and DOCKER_PASSWORD secrets are no longer used implicitly for image pulls |
| 3.0.0 | 31 Jul 2026 | One process per .runner file; container options that reach the host require privileged mode |
| 4.0.0 | 24 Sep 2026 | * in valid_volumes no longer crosses /; the cache goes through the runner |
| 5.0.0 | 8 Oct 2026 | ${NAME} expands in config.yaml, runs-on is mandatory, Git 2.34.1 or newer is required |
The image’s environment variables do not change (GITEA_INSTANCE_URL, GITEA_RUNNER_REGISTRATION_TOKEN, GITEA_RUNNER_NAME, CONFIG_FILE), and neither does the registration file format. That is why the image swap starts, and the failures show up later, in the config and in the jobs.
How I reproduced the upgrade
I set up Gitea 28.2.0 and a Docker-in-Docker runner (dind, the runner ships its own Docker daemon inside) in one Compose project. I first started it with gitea/act_runner:0.6.1-dind, registered it and ran a two-job workflow until it was green. Then I changed only the image and ran it again. This is the runner service:
runner:
image: ${RUNNER_IMAGE:-gitea/act_runner:0.6.1-dind}
privileged: true
environment:
GITEA_INSTANCE_URL: "http://172.30.202.10:3000"
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN:-}
GITEA_RUNNER_NAME: "b10p02-runner"
GITEA_RUNNER_LABELS: "ubuntu-latest:docker://node:24-bookworm"
CONFIG_FILE: /config/config.yaml
volumes:
- runner-data:/data
- ./config.yaml:/config/config.yaml:ro
The starting config.yaml had two variables meant to expand inside the job, not in the runner, and an allowed volume with a single asterisk:
runner:
file: .runner
envs:
PIP_CACHE_DIR: "${HOME}/.cache/pip"
DEPLOY_PATH: "/srv/${DEPLOY_ENV}/app"
container:
valid_volumes:
- "/srv/cache/*"
The workflow had a build job that mounts /srv/cache/npm/v24 at /cache, and a notify job without runs-on. On 0.6.1 both finished fine: notify ran in the default image docker.gitea.com/runner-images:ubuntu-latest, and the build log printed DEPLOY_PATH=/srv/${DEPLOY_ENV}/app verbatim.
Error: parse config file: ${DEPLOY_ENV} is not set
This is the first failure, and it appears as soon as you switch the image to gitea/runner:5.2.3-dind. The container stays Up, so docker ps does not warn you; the warning only shows in the log:
$ docker logs b10p02-runner
Error: parse config file "/config/config.yaml": line 8: ${DEPLOY_ENV} is not set
is missing or not a regular file
level=info msg="Registering runner, arch=arm64, os=linux, version=v5.2.3."
Error: parse config file "/config/config.yaml": line 8: ${DEPLOY_ENV} is not set
Waiting to retry ...
The cause is v5.0.0: a config.yaml value containing ${NAME} now reads that variable from the runner’s environment, and loading fails when it is unset. Without a config, the image’s start script cannot find the .runner path. It then assumes the runner is not registered and retries registration every 5 s.
The fix is to write $${NAME} when you want the literal text. Escape the variables that do exist in the runner too: in my test, ${HOME} raised no error, but the job received PIP_CACHE_DIR=/root/.cache/pip, the HOME of the runner container, instead of the unexpanded text. This is how the two lines end up:
envs:
PIP_CACHE_DIR: "$${HOME}/.cache/pip"
DEPLOY_PATH: "/srv/$${DEPLOY_ENV}/app"
Recreate the container after editing the file, with docker compose up -d --force-recreate runner. If you edit a mounted file with sed -i, the editor writes a new file and a restart can keep seeing the old one.
is not a valid volume, will be ignored
With the config loading again, the build job failed for a different reason. The runner dropped the workflow’s volume and the step that used it did not find the directory:
[/srv/cache/npm/v24] is not a valid volume, will be ignored
ls: cannot access '/cache': No such file or directory
##[error]Process completed with exit code 2.
The change comes from v4.0.0: * in valid_volumes no longer matches across /. On 0.6.1, /srv/cache/* accepted /srv/cache/npm/v24; now it only accepts one level, such as /srv/cache/npm. Use ** for nested paths:
container:
valid_volumes:
- "/srv/cache/**"
v5.0.0 also adds :ro entries, which allow read-only mounts only. If a volume still does not mount with **, check Gitea Runner issue 1251[2], which tracks a 4.0.0 case with job container volumes.
job requires a non-empty ‘runs-on’
The third failure is the one in the cover screenshot. The notify job had no runs-on, and 0.6.1 ran it in the runner’s default image. Version 5.2.3 stops it during preparation:
b10p02-runner(version:v5.2.3)
workflow prepared
job "notify" requires a non-empty 'runs-on'
v5.0.0 does this to behave like GitHub Actions. Jobs that call a reusable workflow are exempt. The fix is to add the label to every job:
notify:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "job without runs-on"
With the three changes, run number 6 went green with both jobs on Gitea Runner 5.2.3.
exec: "act_runner": executable file not found in $PATH
This error appears if your Compose file, systemd unit or a script calls the binary by its old name. In the new image the binary is gitea-runner, and there is no link under the old name:
$ docker run --rm --entrypoint act_runner gitea/runner:5.2.3 daemon
docker: Error response from daemon: failed to create task for
container: failed to create shim task: OCI runtime create failed:
runc create failed: unable to start container process: error during
container init: exec: "act_runner": executable file not found in $PATH
Your terminal prints it on one line; here it is split to fit. Change act_runner daemon to gitea-runner daemon, or drop the entrypoint and keep the image’s own. Watch out for passing the old binary as command: the image’s script ignores the arguments and tries to register the runner in a loop. Change generate-config too, which on 5.2.3 prints a deprecation notice pointing to config generate.
How to check your config.yaml before you switch images
The config runner-file subcommand loads the whole config, so it works as a pre-flight check without touching the running runner. Run it with the new image against your file:
docker run --rm -v "$PWD/config.yaml:/c.yaml:ro" \
--entrypoint gitea-runner gitea/runner:5.2.3 \
--config /c.yaml config runner-file
echo "exit=$?"
With my original config it returned the same ${DEPLOY_ENV} is not set and exit=1; with the fixed one it returned the .runner path and exit=0. Then search your workflows for jobs without runs-on and for valid_volumes paths with a single *. This list covers the remaining changes I did not reproduce and that you should review by hand:
DOCKER_USERNAME/DOCKER_PASSWORD: since 2.0.0 they no longer authenticate image pulls; usecontainer.credentialsin the job- One
.runnerper process: since 3.0.0 the file is locked at startup, so every runner needs its own registration container.network: bridge: since 4.0.0 jobs onbridgecannot reach the runner’s cache; leave the value empty or use your own networkbuiltin:checkout: since 5.0.0 it removes untracked files unlessclean: falseis set- Metrics:
act_runner_*becamegitea_runner_*in 1.0.0, and 5.0.0 adopts the OpenTelemetry names - Log and OOM options: without
container.privileged, 5.0.0 drops--log-driver,--log-opt,--oom-score-adjand--oom-kill-disablewith a warning
What happens to the registration and how to roll back
The registration survives as long as you keep the /data volume. After the image swap, the runner started with the .runner file that 0.6.1 had written, reported version: v5.2.3, and Gitea’s admin API still showed a single runner with id 1, online. You do not need a new token.
To roll back, pin the previous image again and recreate the container; the registration file is the same. Revert the $${...} in config.yaml too, because 0.6.1 does not understand them and would pass the double dollar through as is. Pin the image by version, gitea/runner:5.2.3 or gitea/runner:5, not latest: the project shipped five major versions in five months.
If you are comparing with Forgejo, its runner has its own version line; the guide to install Forgejo with Docker, Caddy and Forgejo Runner covers that case.
Frequently asked questions
Do I have to register the runner again when moving to gitea/runner?
No, as long as you keep the volume with the .runner file. In the test, Gitea Runner 5.2.3 reused the act_runner 0.6.1 registration and Gitea kept the same id. You only register again if you delete /data or start two processes on the same file, which 3.0.0 blocks.
Can I keep using gitea/act_runner:latest?
You can, but you will get no fixes: the tag points to v0.6.1 from April 2026. v5.2.0, for example, was built with Go 1.27.3 to close CVE-2026-111943 on intermediate certificates, and the old image lacks that fix.
Does Gitea Runner 5 work with a Gitea server older than 28?
The test only covers Gitea 28.2.0. v5.2.1 moved to connectrpc.com/connect/v2, and its notes say it works with servers on connect v1 and v2. If your server is old, try a separate runner first before switching the main one.
Conclusion
Moving from act_runner 0.6.1 to Gitea Runner 5 is an image swap that keeps the registration, and the failures show up in the config and the workflows. Escape the ${...} in config.yaml with $$, change * to ** in valid_volumes and add runs-on to every job. Validate the file with config runner-file before recreating the container, and pin the image version. If you then need to import repositories into Gitea 28, the guide to the You cannot import from disallowed hosts error covers that release’s other breaking change.
Sources: [1] Gitea, Gitea Runner 1.0.0 announcement[1], [2] Gitea Runner, v5.0.0 notes[3], [3] Gitea Runner, v4.0.0 notes[4], [4] Gitea Runner, v3.0.0 notes[5], [5] Gitea Runner, v2.0.0 notes[6], [6] Docker Hub, gitea/act_runner image[7], [7] Docker Hub, gitea/runner image[8], [8] Gitea documentation, Actions runner[9], [9] Gitea Runner, issue 1251 on volumes in 4.0.0[2].