Files
maptool/docs/deployment.adoc
T
randy e13d1a1e97
CI / check (push) Successful in 6m28s
CI / deploy (push) Successful in 13s
Require Gitea login to write GPX data; IPv6-capable bind
Upload/delete/move/create-folder now require logging in via Gitea
OAuth2 (only MAPTOOL_ALLOWED_GITEA_USER is granted write access);
reading stays public. Deploy secrets (session secret, OAuth2 client
credentials, allowed user) are written to .env from Gitea Actions
repo secrets on every deploy instead of living on the server by hand.

Also switch the container's bind address to "[::]" (dual-stack) so
the app is reachable over IPv6, not just IPv4.
2026-08-27 18:02:37 +02:00

121 lines
5.2 KiB
Plaintext

= Deployment
:toc:
:toclevels: 2
MapTool deploys as a Docker container on the same server the Gitea Actions
runners run on. Two separate runners are registered against the Gitea
instance:
* The existing Docker-based runner (`gitea/runner:3`), labels
`ubuntu-latest`/`ubuntu-24.04`/`ubuntu-22.04` — runs the `check` job
in a fresh, isolated container per job, same as before.
* A dedicated native runner, installed directly on the host (no Docker
around it), label `deploy:host` — runs only the `deploy` job.
They're separate because the Docker-based runner's jobs (whether in
its usual isolated per-job containers, or tried directly inside its
own container via a `host`-scheme label) have no reliable access to
the host's filesystem, `systemctl`, or even a full toolset (its own
container image is missing `rsync`, `docker` CLI, Node.js — it's built
for spawning per-job containers via its `docker.sock` mount, not for
running arbitrary host commands). The native runner sidesteps all of
that: it *is* a host process, so `docker`, `git`, etc. are just
whatever's installed on the server.
== 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 native deploy runner
Installed from the official binary (see
https://docs.gitea.com/runner/installation/binary/), running as a
systemd service (`gitea-runner.service`) under a dedicated
`gitea-runner` system user, which needs:
* Docker access (member of the `docker` group).
* Write access to `/srv/docker/maptool`.
Its `config.yaml` (`/etc/gitea-runner/config.yaml`) declares a single
label:
[source,yaml]
----
runner:
labels:
- "deploy:host"
----
The `host` scheme means jobs run directly via the runner process's own
shell — no container spawned — which is exactly what a plain, single
purpose deploy runner needs.
== 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.
== Secrets (`.env`)
`docker-compose.yaml` reads `SESSION_SECRET`, `GITEA_OAUTH_CLIENT_ID`,
`GITEA_OAUTH_CLIENT_SECRET` and `MAPTOOL_ALLOWED_GITEA_USER` from the
environment (see `docs/architecture.adoc` for what they're for) —
docker compose loads these automatically from a `.env` file next to
`docker-compose.yaml`, i.e. `/srv/docker/maptool/.env`. That file is
gitignored and dockerignored on purpose: nothing here ever puts
secrets in the repository or a built image layer.
It's not created by hand on the server — the deploy job's "Write
.env" step regenerates it on every deploy from repo-level Gitea
Actions secrets (*Settings → Actions → Secrets*): `SESSION_SECRET`,
`OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `MAPTOOL_ALLOWED_GITEA_USER`.
The OAuth2 ones aren't named `GITEA_OAUTH_CLIENT_*` to match the `.env`
var names 1:1 — Gitea rejects secret names starting with `GITEA_` or
`GITHUB_` as reserved — but the workflow step maps them to the right
`.env` var names regardless. Gitea masks secret values in the job log
automatically. Set these once in the repo settings and every deploy
since keeps `.env` in sync — no server-side step needed beyond the
first-time OAuth2 application registration below.
`SESSION_SECRET` must stay the same value across deploys (regenerating
it every push would silently log everyone out every time `main`
changes) — generate it once (`openssl rand -hex 32`) and store that
same value as the secret from then on, don't regenerate per-deploy.
Register the OAuth2 application on the Gitea side (as the
`MAPTOOL_ALLOWED_GITEA_USER` account): *Settings → Applications →
Manage OAuth2 Applications → Create a new OAuth2 Application*, with
redirect URI `https://map.priesol.net/auth/callback`. That gives you
the client id/secret to store as the two `GITEA_OAUTH_*` secrets
above.
== CI deploy job
`.gitea/workflows/ci.yml`'s `deploy` job runs after `check` passes, only
on a push to `main`, with `runs-on: deploy`:
. Checks out the repo with plain `git` (`fetch` + `checkout`,
authenticated via a one-off `http.extraHeader`) rather than
`actions/checkout@v4` — a leftover from when this job ran in a
Node-less container; kept as-is since there's no reason to bring
Node back in for something this already does.
. Copies the checkout into `/srv/docker/maptool/` using plain `cp`/`rm`:
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 if nothing did.