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.
121 lines
5.2 KiB
Plaintext
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.
|