fix(deploy): push through a TLS-terminating proxy, not raw Gitea HTTP #8
@@ -2,6 +2,7 @@ name: Release image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -12,31 +13,60 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# driver: docker (not the action's default docker-container driver) so buildx reuses the
|
||||
# host's own dockerd instead of spinning up an isolated builder container — the isolated
|
||||
# one doesn't see the host's /etc/docker/certs.d, which is how the login step below trusts
|
||||
# the registry's self-signed cert (docs/DECISIONS.md D17). We don't need multi-platform
|
||||
# builds, so nothing the docker-container driver offers is actually lost here.
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker
|
||||
|
||||
# secrets.GITEA_TOKEN cannot push to the Gitea container registry — a documented Gitea
|
||||
# limitation, not a misconfiguration (see CLAUDE.md). REGISTRY_TOKEN is a separate PAT with
|
||||
# package:write, expected to already exist as a repo secret.
|
||||
#
|
||||
# registry.bbergle.com:9537, not the raw 192.168.0.3:3000 Gitea talks HTTP on directly —
|
||||
# Docker refuses any non-localhost registry over plain HTTP by default. This hostname is an
|
||||
# NPMplus proxy host in front of Gitea's registry, terminating TLS with a self-signed cert;
|
||||
# the runner host trusts it via /etc/docker/certs.d/registry.bbergle.com:9537/ca.crt (not
|
||||
# committed here — host-local trust material, docs/DECISIONS.md D17 has the full setup).
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: 192.168.0.3:3000
|
||||
registry: registry.bbergle.com:9537
|
||||
username: BBergle
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Resolve image tag
|
||||
# `latest` should only ever mean "what's actually on main" (or a tagged release) — not
|
||||
# whatever a manual test dispatch off some feature branch happened to build. Learned the
|
||||
# hard way: a manual dispatch off this very branch, while verifying the fix above, silently
|
||||
# overwrote `latest` under the old unconditional-tags logic. Building the full tag list here
|
||||
# in bash (rather than a conditional expression inline in the tags: block below) means there's
|
||||
# never a blank line for build-push-action to choke on when latest isn't included.
|
||||
- name: Resolve image tags
|
||||
id: tag
|
||||
run: |
|
||||
IMG=registry.bbergle.com:9537/bbergle/bike-app
|
||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
||||
echo "value=${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
VALUE="${{ gitea.ref_name }}"
|
||||
UPDATE_LATEST=true
|
||||
elif [ "${{ gitea.ref_name }}" = "main" ] && [ "${{ gitea.event_name }}" = "push" ]; then
|
||||
VALUE="main-$(git rev-parse --short HEAD)"
|
||||
UPDATE_LATEST=true
|
||||
else
|
||||
echo "value=manual-$(date -u +%Y%m%d%H%M%S)" >> "$GITHUB_OUTPUT"
|
||||
VALUE="manual-$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short HEAD)"
|
||||
UPDATE_LATEST=false
|
||||
fi
|
||||
{
|
||||
echo "tags<<EOF"
|
||||
echo "$IMG:$VALUE"
|
||||
[ "$UPDATE_LATEST" = true ] && echo "$IMG:latest"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
192.168.0.3:3000/bbergle/bike-app:latest
|
||||
192.168.0.3:3000/bbergle/bike-app:${{ steps.tag.outputs.value }}
|
||||
tags: ${{ steps.tag.outputs.tags }}
|
||||
|
||||
+14
-5
@@ -73,11 +73,20 @@ stays editable by hand afterward regardless of what the template pre-fills.
|
||||
## Publishing the image
|
||||
|
||||
`.gitea/workflows/release.yml` builds this Dockerfile and pushes it to the Gitea container
|
||||
registry (`192.168.0.3:3000/bbergle/bike-app`) on a `v*` tag push, or on manual
|
||||
`workflow_dispatch`. It does **not** SSH into the host and recreate the running container —
|
||||
rolling out a new image on Unraid (pulling it and clicking "Apply" on the container, or via
|
||||
Unraid's own update-checking) is left as a manual/Unraid-side step, not something CI does
|
||||
unattended.
|
||||
registry at `registry.bbergle.com:9537/bbergle/bike-app` on every push to `main` (tagged
|
||||
`main-<short-sha>`, and `latest`), on a `v*` tag push (tagged with the tag name, and `latest`), or
|
||||
on manual `workflow_dispatch` (tagged `manual-<timestamp>-<short-sha>` only — a manual dispatch
|
||||
never moves `latest`, so testing a feature branch can't clobber what's actually deployable). Not
|
||||
`192.168.0.3:3000` (Gitea's own plain-HTTP address) directly — Docker
|
||||
refuses any non-localhost registry over plain HTTP by default, so `registry.bbergle.com:9537` is
|
||||
an NPMplus proxy host in front of Gitea's registry that terminates TLS with a self-signed cert.
|
||||
See `docs/DECISIONS.md` D17 for the full setup (cert, NPMplus proxy host, `certs.d` trust, and the
|
||||
buildx driver change this required) — none of it is committed here, since it's host-local trust
|
||||
material and NPMplus config, not something this repo can or should own.
|
||||
|
||||
It does **not** SSH into the host and recreate the running container — rolling out a new image on
|
||||
Unraid (pulling it and clicking "Apply" on the container, or via Unraid's own update-checking) is
|
||||
left as a manual/Unraid-side step, not something CI does unattended.
|
||||
|
||||
## What's not here yet
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
-->
|
||||
<Container version="2">
|
||||
<Name>velodrome</Name>
|
||||
<Repository>192.168.0.3:3000/bbergle/bike-app:latest</Repository>
|
||||
<Repository>registry.bbergle.com:9537/bbergle/bike-app:latest</Repository>
|
||||
<Registry>http://192.168.0.3:3000/BBergle/-/packages/container/bike-app</Registry>
|
||||
<Network>bridge</Network>
|
||||
<Privileged>false</Privileged>
|
||||
<Support>https://192.168.0.3:3000/BBergle/bike-app/issues</Support>
|
||||
<Project>http://192.168.0.3:3000/BBergle/bike-app</Project>
|
||||
<Overview>Self-hosted cycling app: Bryton Rider 650 ride sync, mileage tracking, spare-parts inventory, and maintenance reminders. One container: Caddy + the FastAPI app + a SQLite database file on the Data path below. See docs/PLAN.md and docs/DECISIONS.md (D15/D16) in the repo for the design.</Overview>
|
||||
<Overview>Self-hosted cycling app: Bryton Rider 650 ride sync, mileage tracking, spare-parts inventory, and maintenance reminders. One container: Caddy + the FastAPI app + a SQLite database file on the Data path below. See docs/PLAN.md and docs/DECISIONS.md (D15/D16/D17) in the repo for the design.</Overview>
|
||||
<Category>Productivity:</Category>
|
||||
<WebUI>http://[IP]:[PORT:8090]/</WebUI>
|
||||
<Icon/>
|
||||
|
||||
@@ -233,6 +233,62 @@ image out is a manual/Unraid-side action (pull + Apply, or Unraid's own update c
|
||||
something CI does unattended — consistent with treating "affects a shared, already-running system"
|
||||
as something a human triggers, not automation.
|
||||
|
||||
### D17 — Registry TLS: self-signed cert behind NPMplus, not `insecure-registries`, not a real domain
|
||||
|
||||
**Problem:** `release.yml`'s first real run failed — `docker/login-action` against
|
||||
`192.168.0.3:3000` (Gitea's plain-HTTP address) hit `server gave HTTP response to HTTPS client`.
|
||||
Docker refuses TLS-less registries by default; this was never a workflow misconfiguration, it's
|
||||
expected Docker behaviour for any non-localhost registry.
|
||||
|
||||
**Rejected: `insecure-registries` in `daemon.json`.** The obvious fix. Rejected after actually
|
||||
reading `/etc/rc.d/rc.docker` on the Unraid host rather than assuming: applying a `daemon.json`
|
||||
change requires a full `dockerd` restart, and (with `Live Restore` disabled on this host) both
|
||||
Unraid's own restart path *and* a raw `kill` of `dockerd` stop every one of the ~40 other
|
||||
containers running on the box first, as part of the restart/shutdown sequence — Plex, Home
|
||||
Assistant, Vaultwarden, everything. Correct fix for the narrow problem, unacceptable blast radius
|
||||
for this specific host.
|
||||
|
||||
**Rejected: a real Let's Encrypt cert on a new `bbergle.com` subdomain routed publicly.** The
|
||||
user's other NPMplus-fronted subdomains resolve through Cloudflare's proxy (orange-cloud), not
|
||||
directly to the home IP. A Cloudflare-proxied hostname would have terminated TLS at Cloudflare's
|
||||
edge with Cloudflare's own cert, never reaching our self-signed cert or NPMplus's own TLS
|
||||
config at all — the entire trust chain would depend on Cloudflare's origin SSL mode, and likely on
|
||||
firewall rules restricting port 443 to Cloudflare's IP ranges, neither of which this problem
|
||||
needed to involve.
|
||||
|
||||
**Chosen:** a small, fully self-contained fix, scoped to touch nothing already working:
|
||||
- A 10-year self-signed cert for `registry.bbergle.com` (SAN-only, no real domain dependency).
|
||||
- An NPMplus proxy host (`registry.bbergle.com` -> `192.168.0.3:3000` over plain HTTP internally)
|
||||
terminating TLS with that cert, on NPMplus's existing HTTPS port (`9537` on this host — found by
|
||||
reading `docker port NPMplus` rather than assuming 443, which is a *different* nginx process on
|
||||
this box entirely).
|
||||
- `/etc/hosts` on the Unraid host mapping `registry.bbergle.com` -> `192.168.0.103` (itself) —
|
||||
chosen over a real DNS record specifically because the only client that ever needs to resolve
|
||||
this hostname is the Unraid host's own `dockerd` (Gitea Actions runs in DooD mode against that
|
||||
same host's Docker socket). This sidesteps Cloudflare, the router's NAT/hairpin behaviour, and
|
||||
any port-forwarding question entirely — verified separately that hairpin NAT works by default on
|
||||
this user's UniFi gateway, but it turned out to be unnecessary for this fix regardless.
|
||||
- `/etc/docker/certs.d/registry.bbergle.com:9537/ca.crt` on the Unraid host, trusting that cert for
|
||||
that host:port specifically. Confirmed (Docker's own docs) that `certs.d` is read per-connection,
|
||||
not baked in at daemon start — no `dockerd` restart, no impact on any other container.
|
||||
- `docker/setup-buildx-action@v3` pinned to `driver: docker` in `release.yml` instead of its
|
||||
default `docker-container` driver — the default runs BuildKit in an isolated builder container
|
||||
that does not see the host's `/etc/docker/certs.d`, which would have silently defeated the whole
|
||||
point of the trust setup above. We don't build multi-platform images, so nothing the
|
||||
`docker-container` driver offers is actually needed here.
|
||||
|
||||
**Not persisted across a reboot, deliberately, for now:** neither the `/etc/hosts` line nor the
|
||||
`certs.d` file are wired into `/boot/config/go` — both live under `/`, which Unraid rebuilds fresh
|
||||
from `/boot` on every boot. Raised explicitly rather than assumed: the user was (rightly) wary of
|
||||
hand-editing anything under `/boot` after an earlier, unrelated discussion of what a broken `go`
|
||||
script could do to boot. Persisting this is a five-minute follow-up (append two lines to `go`) once
|
||||
they're ready to make that call deliberately, not bundled into this fix.
|
||||
|
||||
**What's unaffected:** Gitea's own web UI, git remote, and API — all still plain
|
||||
`http://192.168.0.3:3000`, exactly as CLAUDE.md documents. NPMplus's existing public proxy hosts
|
||||
and certs (`vaultwarden.bbergle.com` etc.) — untouched, new proxy host only. No other container on
|
||||
the Unraid host was restarted, reconfigured, or otherwise touched to make this work.
|
||||
|
||||
---
|
||||
|
||||
## Deliberately deferred
|
||||
|
||||
Reference in New Issue
Block a user