= Deployment :toc: :toclevels: 2 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: [source,yaml] ---- 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: [source,shell] ---- docker exec 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: [source,yaml] ---- 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-off `http.extraHeader`), not `actions/checkout@v4` — that's a JS action, and host-mode jobs run directly in the runner container, which has `git` but no Node.js runtime to execute JS actions with. . 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`. . `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.