Files
maptool/docs/deployment.adoc
T
randy 3a79c3d6c1
CI / check (push) Successful in 5m47s
CI / deploy (push) Failing after 1s
Add Docker deployment via Gitea Actions host-mode runner
Containerize the app (Dockerfile, docker-compose.yaml) and add a CI
deploy job that syncs the checkout to /srv/docker/maptool and runs
docker compose directly, using the runner's new "host" execution
label to reach the host's Docker daemon and filesystem.
2026-08-27 14:54:21 +02:00

5.0 KiB
Raw Blame History

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 and offline.pmtiles. Persisted via a bind mount (./data:/app/data in docker-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:

  1. Copies the checkout into /srv/docker/maptool/ using plain cp/rm (no rsync in the runner image): clears everything in the target except data/, copies the fresh checkout over, then drops the copied .git.

  2. 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.