Compare commits

...
2 Commits
Author SHA1 Message Date
randy 049d54e44e Use the short maptool:latest image name in the deploy job
CI / deploy (push) Successful in 4s
CI / check (push) Successful in 43s
docker.io/library/maptool:latest and maptool:latest resolve to the
same local image (verified on cube.local) - the docker.io/library
prefix is just podman's normalization of the unqualified name, not an
actual registry reference. Short form matches what docker-compose.yaml
itself uses and avoids implying a Docker Hub push/pull that never
happens.
2026-09-08 20:38:14 +02:00
randy e4764c64e4 Drive maptool's deploy through the podman-compose@ systemd service
The service's own ExecStart deliberately runs a plain podman compose
up (no --build) since ExecStartPre already tears down the previous
containers/networks/volumes on every start — but that leaves the
built image alone, so the deploy job now removes it directly first
(what forces a fresh build) and drives start/stop through systemctl
rather than calling podman/docker compose by hand. Also switches
/srv/docker/maptool to /srv/podman/maptool per the symlink now in
place, and updates deployment.adoc to match.
2026-09-08 20:33:03 +02:00
2 changed files with 62 additions and 29 deletions

No files matched your search

+24 -10
View File
@@ -35,7 +35,7 @@ jobs:
# Handled by a separate, native (non-Docker) runner registered with the # Handled by a separate, native (non-Docker) runner registered with the
# single "cube-host" label (see docs/deployment.adoc) — it runs directly # single "cube-host" label (see docs/deployment.adoc) — it runs directly
# on the server, so it has docker/git/etc. from the host, no socket/bind # on the server, so it has podman/git/etc. from the host, no socket/bind
# mount tricks needed. The Docker-based runner keeps handling `check`. # mount tricks needed. The Docker-based runner keeps handling `check`.
deploy: deploy:
needs: check needs: check
@@ -68,15 +68,15 @@ jobs:
# alone too — the glob below doesn't match them — and neither is ever # alone too — the glob below doesn't match them — and neither is ever
# part of the git checkout in the first place, since both are # part of the git checkout in the first place, since both are
# gitignored. # gitignored.
- name: Sync to /srv/docker/maptool - name: Sync to /srv/podman/maptool
run: | run: |
mkdir -p /srv/docker/maptool mkdir -p /srv/podman/maptool
for f in /srv/docker/maptool/*; do for f in /srv/podman/maptool/*; do
[ "$(basename "$f")" = "data" ] && continue [ "$(basename "$f")" = "data" ] && continue
rm -rf "$f" rm -rf "$f"
done done
cp -a . /srv/docker/maptool/ cp -a . /srv/podman/maptool/
rm -rf /srv/docker/maptool/.git rm -rf /srv/podman/maptool/.git
# Regenerated on every deploy from repo secrets (Settings -> Actions # Regenerated on every deploy from repo secrets (Settings -> Actions
# -> Secrets) rather than created once by hand on the server — see # -> Secrets) rather than created once by hand on the server — see
@@ -88,16 +88,30 @@ jobs:
# actually reads. # actually reads.
- name: Write .env - name: Write .env
run: | run: |
cat > /srv/docker/maptool/.env <<EOF cat > /srv/podman/maptool/.env <<EOF
SESSION_SECRET=${{ secrets.SESSION_SECRET }} SESSION_SECRET=${{ secrets.SESSION_SECRET }}
GITEA_OAUTH_CLIENT_ID=${{ secrets.OAUTH_CLIENT_ID }} GITEA_OAUTH_CLIENT_ID=${{ secrets.OAUTH_CLIENT_ID }}
GITEA_OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }} GITEA_OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}
MAPTOOL_ALLOWED_GITEA_USER=${{ secrets.MAPTOOL_ALLOWED_GITEA_USER }} MAPTOOL_ALLOWED_GITEA_USER=${{ secrets.MAPTOOL_ALLOWED_GITEA_USER }}
LIVETRACK_WEBHOOK_SECRET=${{ secrets.LIVETRACK_WEBHOOK_SECRET }} LIVETRACK_WEBHOOK_SECRET=${{ secrets.LIVETRACK_WEBHOOK_SECRET }}
EOF EOF
chmod 600 /srv/docker/maptool/.env chmod 600 /srv/podman/maptool/.env
# podman-compose@maptool.service (/etc/systemd/system/podman-compose@.service)
# owns the container lifecycle — its ExecStartPre already tears down
# the previous containers/networks/volumes on every start, but not
# the built image, and its ExecStart runs a plain `podman compose up`
# (deliberately no `--build`), which only builds when the image tag
# isn't already present locally. So a bare restart would just
# relaunch the previous image; removing that tag first forces a
# fresh build from the code just synced above — purely local, no
# registry involved (same `maptool:latest` docker-compose.yaml
# already builds/tags; podman rmi never talks to a registry for a
# locally-built image). sudo is needed since the unit — and thus the
# image — is root's, not gitea-runner's own (gitea-runner has
# passwordless sudo for this on cube-host).
- name: Deploy - name: Deploy
run: | run: |
cd /srv/docker/maptool sudo systemctl stop podman-compose@maptool.service
docker compose -p maptool up --build -d sudo podman rmi -f maptool:latest || true
sudo systemctl start podman-compose@maptool.service
+38 -19
View File
@@ -2,9 +2,10 @@
:toc: :toc:
:toclevels: 2 :toclevels: 2
MapTool deploys as a Docker container on the same server the Gitea Actions MapTool deploys as a Podman container (via `podman compose`, driven by a
runners run on. Two separate runners are registered against the Gitea `podman-compose@.service` systemd template — see below) on the same server
instance: the Gitea Actions runners run on. Two separate runners are registered
against the Gitea instance:
* The existing Docker-based runner (`gitea/runner:3`), labels * The existing Docker-based runner (`gitea/runner:3`), labels
`ubuntu-latest`/`ubuntu-24.04`/`ubuntu-22.04` — runs the `check` job `ubuntu-latest`/`ubuntu-24.04`/`ubuntu-22.04` — runs the `check` job
@@ -24,14 +25,19 @@ whatever's installed on the server.
== Layout on the server == Layout on the server
`/srv/docker/maptool/` holds: `/srv/podman/maptool/` (`/srv/podman` is a symlink to `/srv/docker`, a
leftover from before the Docker → Podman migration — kept around so
existing paths/tooling that still assume `/srv/docker` keep working;
expect this to flip the other way, `/srv/docker` symlinked to
`/srv/podman`, once nothing depends on the old name anymore) holds:
* A checkout of this repository (kept in sync by the deploy job, see * A checkout of this repository (kept in sync by the deploy job, see
below) — `Dockerfile`, `docker-compose.yaml`, application code. below) — `Dockerfile`, `docker-compose.yaml`, application code.
`podman compose` reads the same `docker-compose.yaml` — no
Podman-specific compose file needed.
* `data/`, *not* part of that sync — uploaded GPX files and * `data/`, *not* part of that sync — uploaded GPX files and
`offline.pmtiles`. Persisted via a bind mount (`./data:/app/data` in `offline.pmtiles`. Persisted via a bind mount (`./data:/app/data` in
`docker-compose.yaml`), so a redeploy (`docker compose up --build`) `docker-compose.yaml`), so a redeploy never touches it.
never touches it.
== The native deploy runner == The native deploy runner
@@ -40,8 +46,11 @@ https://docs.gitea.com/runner/installation/binary/), running as a
systemd service (`gitea-runner.service`) under a dedicated systemd service (`gitea-runner.service`) under a dedicated
`gitea-runner` system user, which needs: `gitea-runner` system user, which needs:
* Docker access (member of the `docker` group). * Passwordless `sudo` (confirmed via `sudo -l -U gitea-runner`) —
* Write access to `/srv/docker/maptool`. `podman-compose@maptool.service` is a root-owned systemd unit (see
below), and the image it builds lives in root's Podman storage, so
starting/stopping it and removing its image both require root.
* Write access to `/srv/podman/maptool`.
Its `config.yaml` (`/etc/gitea-runner/config.yaml`) declares a single Its `config.yaml` (`/etc/gitea-runner/config.yaml`) declares a single
label: label:
@@ -61,20 +70,22 @@ purpose deploy runner needs.
`config/puma.rb` binds to `127.0.0.1` by default (see `config/puma.rb` binds to `127.0.0.1` by default (see
`docs/architecture.adoc`) — fine for local dev, but inside a container `docs/architecture.adoc`) — fine for local dev, but inside a container
that would make the app unreachable through Docker's published port that would make the app unreachable through the published port
(loopback is scoped to the container's own network namespace). (loopback is scoped to the container's own network namespace).
`docker-compose.yaml` sets `BIND_HOST=0.0.0.0` to override that for `docker-compose.yaml` sets `BIND_HOST="[::]"` to override that for the
the deployed container specifically; Docker's own port publishing deployed container specifically — the IPv6 wildcard, which (dual-stack
(`ports:` in the compose file) is the network boundary there instead being the Linux default) also accepts IPv4 connections, covering both
of Puma's bind address. with a single listener; the compose file's own port publishing
(`ports:`) is the network boundary there instead of Puma's bind
address.
== Secrets (`.env`) == Secrets (`.env`)
`docker-compose.yaml` reads `SESSION_SECRET`, `GITEA_OAUTH_CLIENT_ID`, `docker-compose.yaml` reads `SESSION_SECRET`, `GITEA_OAUTH_CLIENT_ID`,
`GITEA_OAUTH_CLIENT_SECRET` and `MAPTOOL_ALLOWED_GITEA_USER` from the `GITEA_OAUTH_CLIENT_SECRET` and `MAPTOOL_ALLOWED_GITEA_USER` from the
environment (see `docs/architecture.adoc` for what they're for) — environment (see `docs/architecture.adoc` for what they're for) —
docker compose loads these automatically from a `.env` file next to `podman compose` loads these automatically from a `.env` file next to
`docker-compose.yaml`, i.e. `/srv/docker/maptool/.env`. That file is `docker-compose.yaml`, i.e. `/srv/podman/maptool/.env`. That file is
gitignored and dockerignored on purpose: nothing here ever puts gitignored and dockerignored on purpose: nothing here ever puts
secrets in the repository or a built image layer. secrets in the repository or a built image layer.
@@ -208,9 +219,17 @@ on a push to `main`, with `runs-on: cube-host`:
`actions/checkout@v4` — a leftover from when this job ran in a `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-less container; kept as-is since there's no reason to bring
Node back in for something this already does. Node back in for something this already does.
. Copies the checkout into `/srv/docker/maptool/` using plain `cp`/`rm`: . Copies the checkout into `/srv/podman/maptool/` using plain `cp`/`rm`:
clears everything in the target except `data/`, copies the fresh clears everything in the target except `data/`, copies the fresh
checkout over, then drops the copied `.git`. checkout over, then drops the copied `.git`.
. `cd /srv/docker/maptool && docker compose -p maptool up --build -d` . Stops `podman-compose@maptool.service`, force-removes the
— rebuilds the image and recreates the container if anything `maptool:latest` image directly via `podman rmi`, then starts the
changed; a no-op if nothing did. service again. The service (`/etc/systemd/system/podman-compose@.service`,
a systemd template instantiated as `podman-compose@maptool.service`)
owns the actual container lifecycle — its `ExecStartPre` tears down
the previous containers/networks/volumes on every start, and its
`ExecStart` runs a plain `podman compose up` (deliberately no
`--build`), which only builds when the image tag isn't already
present locally. Removing the tag first is what forces a fresh build
from the code just synced above; without it, a restart alone would
just relaunch the previous image unchanged.