Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd74c2c7c1 | ||
|
|
45719f284c | ||
|
|
a114a7d3d8 | ||
|
|
32037b1190 | ||
|
|
70e0182177 |
@@ -2,6 +2,7 @@ name: Release image
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [main]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -12,31 +13,60 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker
|
||||||
|
|
||||||
# secrets.GITEA_TOKEN cannot push to the Gitea container registry — a documented Gitea
|
# 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
|
# limitation, not a misconfiguration (see CLAUDE.md). REGISTRY_TOKEN is a separate PAT with
|
||||||
# package:write, expected to already exist as a repo secret.
|
# 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
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: 192.168.0.3:3000
|
registry: registry.bbergle.com:9537
|
||||||
username: BBergle
|
username: BBergle
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
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
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
|
IMG=registry.bbergle.com:9537/bbergle/bike-app
|
||||||
if [ "${{ gitea.ref_type }}" = "tag" ]; then
|
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
|
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
|
fi
|
||||||
|
{
|
||||||
|
echo "tags<<EOF"
|
||||||
|
echo "$IMG:$VALUE"
|
||||||
|
[ "$UPDATE_LATEST" = true ] && echo "$IMG:latest"
|
||||||
|
echo "EOF"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- uses: docker/build-push-action@v6
|
- uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: ${{ steps.tag.outputs.tags }}
|
||||||
192.168.0.3:3000/bbergle/bike-app:latest
|
|
||||||
192.168.0.3:3000/bbergle/bike-app:${{ steps.tag.outputs.value }}
|
|
||||||
|
|||||||
+14
-5
@@ -73,11 +73,20 @@ stays editable by hand afterward regardless of what the template pre-fills.
|
|||||||
## Publishing the image
|
## Publishing the image
|
||||||
|
|
||||||
`.gitea/workflows/release.yml` builds this Dockerfile and pushes it to the Gitea container
|
`.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
|
registry at `registry.bbergle.com:9537/bbergle/bike-app` on every push to `main` (tagged
|
||||||
`workflow_dispatch`. It does **not** SSH into the host and recreate the running container —
|
`main-<short-sha>`, and `latest`), on a `v*` tag push (tagged with the tag name, and `latest`), or
|
||||||
rolling out a new image on Unraid (pulling it and clicking "Apply" on the container, or via
|
on manual `workflow_dispatch` (tagged `manual-<timestamp>-<short-sha>` only — a manual dispatch
|
||||||
Unraid's own update-checking) is left as a manual/Unraid-side step, not something CI does
|
never moves `latest`, so testing a feature branch can't clobber what's actually deployable). Not
|
||||||
unattended.
|
`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
|
## What's not here yet
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
-->
|
-->
|
||||||
<Container version="2">
|
<Container version="2">
|
||||||
<Name>velodrome</Name>
|
<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>
|
<Registry>http://192.168.0.3:3000/BBergle/-/packages/container/bike-app</Registry>
|
||||||
<Network>bridge</Network>
|
<Network>bridge</Network>
|
||||||
<Privileged>false</Privileged>
|
<Privileged>false</Privileged>
|
||||||
<Support>https://192.168.0.3:3000/BBergle/bike-app/issues</Support>
|
<Support>https://192.168.0.3:3000/BBergle/bike-app/issues</Support>
|
||||||
<Project>http://192.168.0.3:3000/BBergle/bike-app</Project>
|
<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>
|
<Category>Productivity:</Category>
|
||||||
<WebUI>http://[IP]:[PORT:8080]/</WebUI>
|
<WebUI>http://[IP]:[PORT:8090]/</WebUI>
|
||||||
<Icon/>
|
<Icon/>
|
||||||
<ExtraParams/>
|
<ExtraParams/>
|
||||||
<PostArgs/>
|
<PostArgs/>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<DonateText/>
|
<DonateText/>
|
||||||
<DonateLink/>
|
<DonateLink/>
|
||||||
<Description>Self-hosted cycling app: Bryton ride sync, mileage tracking, spare-parts inventory, maintenance reminders.</Description>
|
<Description>Self-hosted cycling app: Bryton ride sync, mileage tracking, spare-parts inventory, maintenance reminders.</Description>
|
||||||
<Config Name="Web UI Port" Target="8080" Default="8080" Mode="tcp" Description="Container's HTTP port. Put a reverse proxy with TLS in front of this — the container itself only ever serves plain HTTP (docs/PLAN.md 'Service topology')." Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
<Config Name="Web UI Port" Target="8080" Default="8090" Mode="tcp" Description="Host port mapped to the container's internal 8080. Defaulted off 8080 since that's already taken by qBittorrent on this host — check for a free port before changing it. Put a reverse proxy with TLS in front of this — the container itself only ever serves plain HTTP (docs/PLAN.md 'Service topology')." Type="Port" Display="always" Required="true" Mask="false">8090</Config>
|
||||||
<Config Name="Data" Target="/data" Default="/mnt/user/appdata/velodrome" Mode="rw" Description="The SQLite database file (and, in a later phase, the raw-file blob store) live here. This is the only thing worth backing up." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/velodrome</Config>
|
<Config Name="Data" Target="/data" Default="/mnt/user/appdata/velodrome" Mode="rw" Description="The SQLite database file (and, in a later phase, the raw-file blob store) live here. This is the only thing worth backing up." Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/velodrome</Config>
|
||||||
<Config Name="VELODROME_PUBLIC_URL" Target="VELODROME_PUBLIC_URL" Default="" Mode="" Description="The externally-visible URL this instance is reachable at, e.g. https://bikes.example.com. Must match exactly what's in the browser's address bar — it's checked against the Origin header on cookie-authenticated requests to stop cross-site request forgery." Type="Variable" Display="always" Required="true" Mask="false"></Config>
|
<Config Name="VELODROME_PUBLIC_URL" Target="VELODROME_PUBLIC_URL" Default="" Mode="" Description="The externally-visible URL this instance is reachable at, e.g. https://bikes.example.com. Must match exactly what's in the browser's address bar — it's checked against the Origin header on cookie-authenticated requests to stop cross-site request forgery." Type="Variable" Display="always" Required="true" Mask="false"></Config>
|
||||||
<Config Name="VELODROME_SECRET_KEY" Target="VELODROME_SECRET_KEY" Default="" Mode="" Description="A random secret, 32+ bytes. Generate one with: openssl rand -hex 32. The image ships an insecure development placeholder — always override this before exposing the container to anything." Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
<Config Name="VELODROME_SECRET_KEY" Target="VELODROME_SECRET_KEY" Default="" Mode="" Description="A random secret, 32+ bytes. Generate one with: openssl rand -hex 32. The image ships an insecure development placeholder — always override this before exposing the container to anything." Type="Variable" Display="always" Required="true" Mask="true"></Config>
|
||||||
|
|||||||
@@ -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"
|
something CI does unattended — consistent with treating "affects a shared, already-running system"
|
||||||
as something a human triggers, not automation.
|
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
|
## Deliberately deferred
|
||||||
|
|||||||
Reference in New Issue
Block a user