Host-mode jobs run directly in the runner container instead of a spawned per-job container, so JS actions like actions/checkout@v4 have no Node.js runtime to execute with. Replace it with a plain git fetch/checkout in the deploy job.
5.3 KiB
Deployment
MapTool deploys as a Docker container on the same server the Gitea Actions
runner runs on. The deploy job in .gitea/workflows/ci.yml runs directly
inside the runner’s own container (see "The runner’s host label" below) and
calls docker compose directly against the host’s Docker daemon — no
systemd unit is involved for this app.
Layout on the server
/srv/docker/maptool/ holds:
-
A checkout of this repository (kept in sync by the deploy job, see below) —
Dockerfile,docker-compose.yaml, application code. -
data/, not part of that sync — uploaded GPX files andoffline.pmtiles. Persisted via a bind mount (./data:/app/dataindocker-compose.yaml), so a redeploy (docker compose up --build) never touches it.
The runner’s "host" label
The Gitea Actions runner (gitea/runner:3) normally runs each job in a
fresh, isolated container spawned via its mounted
/var/run/docker.sock — that’s how the check job runs (runs-on:
ubuntu-latest). Such a job container has no access to the host
filesystem or to a host systemctl, so it can’t reach
/srv/docker/maptool or manage a systemd unit.
The deploy job instead uses runs-on: host, a label configured in
the runner’s own config.yaml that makes the runner (gitea-runner,
the binary inside the gitea/runner:3 image) execute the job’s
steps directly inside the runner’s own container, without spawning a
new one. That container already has /var/run/docker.sock mounted, so
docker/docker compose commands run there act directly on the
host’s Docker daemon — this is the same runner used for check, just
with a second execution mode, not a separate runner.
For this to work, the runner’s own docker-compose.yaml (the one that
starts the runner itself, not this app) needs one more bind mount so
/srv/docker inside the runner container is the same path as on the
real host:
services:
runner:
image: docker.io/gitea/runner:3
# ...
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
- /srv/docker:/srv/docker
And the runner’s config.yaml needs the host label added alongside
whatever docker-mode labels it already has. If config.yaml is empty
(no prior customization), generate a real default first rather than
hand-writing one — schemas vary across runner versions:
docker exec <runner-container> gitea-runner config generate > config.yaml
(config generate — not generate-config, which is deprecated; and
the binary is gitea-runner, not act_runner.)
Then add the label to the generated runner: labels: list — the
generated defaults are commented out, and this list fully replaces
the runner’s built-in defaults rather than adding to them, so all of
them need to be uncommented too, not just the new one:
runner:
labels:
- "host:host"
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
The 1:1 path match matters: the deploy job’s working directory
(/srv/docker/maptool) needs to resolve to the same path whether
read from inside the runner container or by the Docker daemon on the
real host, because docker-compose.yaml’s relative bind mount
(./data`) gets resolved to an absolute path by the docker compose
CLI before being sent to the daemon — if that path only existed
inside the runner container, the daemon (running on the bare host)
would create/mount the wrong (or a nonexistent) directory.
After editing both files, recreate the runner container so it picks
up the new volume and label (docker compose up -d in the runner’s
own directory).
Puma’s bind address
config/puma.rb binds to 127.0.0.1 by default (see
docs/architecture.adoc) — fine for local dev, but inside a container
that would make the app unreachable through Docker’s published port
(loopback is scoped to the container’s own network namespace).
docker-compose.yaml sets BIND_HOST=0.0.0.0 to override that for
the deployed container specifically; Docker’s own port publishing
(ports: in the compose file) is the network boundary there instead
of Puma’s bind address.
CI deploy job
.gitea/workflows/ci.yml’s `deploy job runs after check passes, only
on a push to main, with runs-on: host:
-
Checks out the repo with plain
git(fetch+checkout, authenticated via a one-offhttp.extraHeader), notactions/checkout@v4— that’s a JS action, and host-mode jobs run directly in the runner container, which hasgitbut no Node.js runtime to execute JS actions with. -
Copies the checkout into
/srv/docker/maptool/using plaincp/rm(norsyncin the runner image): clears everything in the target exceptdata/, copies the fresh checkout over, then drops the copied.git. -
cd /srv/docker/maptool && docker compose -p maptool up --build -d— rebuilds the image and recreates the container if anything changed; a no-op restart if nothing did.
No sudo, no systemd, no SSH — the runner container’s existing access
to /var/run/docker.sock and (once added) /srv/docker is enough.