Compare commits

..

7 commits

Author SHA1 Message Date
5ae0675705 Merge pull request 'fix(docker): enable uvicorn proxy-headers so CSS loads behind Traefik' (#35) from fix/proxy-headers into main
All checks were successful
deploy / build-push-deploy (push) Successful in 1m10s
Reviewed-on: #35
Reviewed-by: archeious <archeious@unbiasedgeek.com>
2026-04-19 18:16:15 -06:00
Jeff Smith
ee6eaaeba8 fix(docker): enable uvicorn proxy-headers so url_for works behind Traefik
Without --proxy-headers + --forwarded-allow-ips, uvicorn ignores the
X-Forwarded-Proto header Traefik sets, so Starlette's url_for() picks
up the internal scheme (http) and host (the container's bind address).
That makes every <link>/<img> href in templates point at an internal
URL with the wrong scheme — the browser refuses CSS/images as mixed
content and the public page renders unstyled.

With both flags the template output becomes
https://quartermaster.unbiasedgeek.com/static/… as expected.

The wildcard in --forwarded-allow-ips='*' is safe here because the
compose file publishes no host ports — only containers on proxy-net
(i.e. Traefik) can reach port 8000.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:15:30 -06:00
9f68351c80 Merge pull request 'feat(ci): Forgejo Actions deploy workflow (#30)' (#34) from feat/deploy-workflow into main
All checks were successful
deploy / build-push-deploy (push) Successful in 4m56s
Reviewed-on: #34
2026-04-19 18:05:58 -06:00
Jeff Smith
df4fcfc659 feat(ci): Forgejo Actions deploy workflow for home-ctr-onyx (#30)
On push to main, the homelab runner (container mode, docker socket
mounted) builds the image, pushes it to the Forgejo registry tagged
with the commit SHA and latest, then runs docker compose pull + up -d
directly against the host Docker daemon — no SSH hop, since the
runner already lives on the deploy host. Finishes with one
curl -u admin:... against https://quartermaster.unbiasedgeek.com/healthz
to catch TLS, Traefik routing, and basic-auth regressions in a
single probe. Two repo-scoped secrets required: REGISTRY_TOKEN for
docker login and QUARTERMASTER_SMOKE_PASSWORD for the public
healthz probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:01:58 -06:00
7abed176e3 Merge pull request 'feat(deploy): compose.yml for home-ctr-onyx (#29)' (#33) from feat/compose into main
Reviewed-on: #33
2026-04-19 17:32:16 -06:00
35f0c8fd79 Merge pull request 'feat(docker): Dockerfile + entrypoint for home-ctr-onyx image (#28)' (#32) from feat/dockerfile into main
Reviewed-on: #32
2026-04-19 17:32:07 -06:00
Jeff Smith
c7f9a56dc8 feat(deploy): add compose.yml for home-ctr-onyx (#29)
Consumes the image from #28 with the platform-contract bindings:
/mnt/quartermaster -> /data, QUARTERMASTER_DB_URL with four slashes,
proxy-net external, Traefik routed on quartermaster.unbiasedgeek.com
through the platform-owned basicauth + ratelimit middlewares, and the
required tenant / project / managed_by / watchtower-disable labels
for host hygiene. Image tag is parameterised via QUARTERMASTER_TAG
so the Actions workflow (#30) can pin a specific SHA per deploy
without editing the checked-in file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:29:53 -06:00
4 changed files with 169 additions and 0 deletions

View file

@ -0,0 +1,62 @@
name: deploy
on:
push:
branches:
- main
jobs:
build-push-deploy:
runs-on: homelab
env:
REGISTRY: forgejo.labbity.unbiasedgeek.com
IMAGE: forgejo.labbity.unbiasedgeek.com/archeious/quartermaster/quartermaster
COMPOSE_PROJECT_NAME: quartermaster
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Forgejo registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: archeious
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.sha }}
${{ env.IMAGE }}:latest
- name: Deploy
run: |
set -euo pipefail
printf 'QUARTERMASTER_TAG=%s\n' '${{ github.sha }}' > .env
docker compose pull
docker compose up -d
- name: Smoke test
env:
SMOKE_PASSWORD: ${{ secrets.QUARTERMASTER_SMOKE_PASSWORD }}
run: |
set -eu
for attempt in 1 2 3 4 5 6 7 8 9 10; do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
-u "admin:$SMOKE_PASSWORD" \
https://quartermaster.unbiasedgeek.com/healthz || echo "000")
if [ "$code" = "200" ]; then
echo "smoke OK after $attempt attempt(s)"
exit 0
fi
echo "attempt $attempt: got $code, retrying"
sleep 3
done
echo "smoke FAILED — last code $code"
exit 1

View file

@ -138,3 +138,63 @@ tests/ pytest suite
A transaction log that rolls up into `applied` on a per-entry per-month A transaction log that rolls up into `applied` on a per-entry per-month
basis is deferred. Once implemented it may replace the hand-edited applied basis is deferred. Once implemented it may replace the hand-edited applied
field. field.
## Deploy (home-ctr-onyx)
`compose.yml` at the repo root describes the `quartermaster-web`
service. The platform team has already provisioned `/mnt/quartermaster/`
(owned `1000:1000`), the `proxy-net` Docker network, and the
`quartermaster-basicauth@file` + `quartermaster-ratelimit@file` Traefik
middlewares on home-ctr-onyx. See the [platform contract
wiki page](https://forgejo.labbity.unbiasedgeek.com/homelab/homelab-IaC/wiki/PlatformContractQuartermaster)
for the canonical list.
Roll out a new build:
```sh
docker compose pull
docker compose up -d
```
`compose.yml` references the image as
`…/quartermaster:${QUARTERMASTER_TAG:-latest}`. The deploy workflow
writes `QUARTERMASTER_TAG=<git-sha>` to an `.env` file next to
`compose.yml` on the host, so each deploy pins a specific SHA without
editing the checked-in compose file.
**Four-slash gotcha.** `QUARTERMASTER_DB_URL` is
`sqlite:////data/quartermaster.db` — four slashes for an absolute
path. Three would resolve relative to the working directory, and the
SQLite file would NOT land on the bind mount (on next restart the
database would be empty).
## CI/CD
Push to `main` triggers `.forgejo/workflows/deploy.yml` on the
`homelab` runner. That runner lives on home-ctr-onyx itself in
container mode with the host's Docker socket mounted — so the
workflow talks to the same Docker daemon that hosts the production
container and no SSH round-trip is needed.
The workflow: checks out the repo, builds the image, pushes it to
the Forgejo registry tagged with the commit SHA and `latest`,
writes `QUARTERMASTER_TAG=<git-sha>` to a `.env` file next to the
checked-out `compose.yml`, runs `docker compose pull && docker
compose up -d`, and finishes with one
`curl -fsS -u admin:… https://quartermaster.unbiasedgeek.com/healthz`
against the public URL — catching TLS, Traefik routing, and the
basic-auth middleware in a single probe.
The workflow reads two Forgejo Actions secrets (repo-scoped under
`archeious/quartermaster`):
* `REGISTRY_TOKEN` — archeious Forgejo personal token with
`write:package` scope; used as the docker-login password.
* `QUARTERMASTER_SMOKE_PASSWORD` — plaintext basic-auth password
for the `admin` user, delivered to the tenant out-of-band by the
platform team.
Rollback is manual for v1: `git checkout` the previous SHA, set
`QUARTERMASTER_TAG` in `.env` to that SHA, and `docker compose up -d`
from a clone of the repo (or let the previous commit be the `main`
tip and the deploy workflow will roll it out).

45
compose.yml Normal file
View file

@ -0,0 +1,45 @@
services:
quartermaster:
image: forgejo.labbity.unbiasedgeek.com/archeious/quartermaster/quartermaster:${QUARTERMASTER_TAG:-latest}
container_name: quartermaster
restart: unless-stopped
mem_limit: 1g
memswap_limit: 1g
environment:
QUARTERMASTER_DB_URL: sqlite:////data/quartermaster.db
volumes:
- /mnt/quartermaster:/data
networks:
- proxy-net
healthcheck:
test:
- CMD
- python
- -c
- "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=3).status == 200 else 1)"
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
logging:
driver: json-file
options:
max-size: 50m
max-file: "3"
labels:
- "tenant=quartermaster"
- "project=quartermaster"
- "managed_by=quartermaster"
- "com.centurylinklabs.watchtower.enable=false"
- "traefik.enable=true"
- "traefik.docker.network=proxy-net"
- "traefik.http.routers.quartermaster.rule=Host(`quartermaster.unbiasedgeek.com`)"
- "traefik.http.routers.quartermaster.entrypoints=websecure"
- "traefik.http.routers.quartermaster.tls=true"
- "traefik.http.routers.quartermaster.tls.certresolver=letsencrypt"
- "traefik.http.routers.quartermaster.middlewares=quartermaster-basicauth@file,quartermaster-ratelimit@file"
- "traefik.http.services.quartermaster.loadbalancer.server.port=8000"
networks:
proxy-net:
external: true

View file

@ -8,4 +8,6 @@ alembic upgrade head
exec uvicorn quartermaster.main:app \ exec uvicorn quartermaster.main:app \
--host 0.0.0.0 \ --host 0.0.0.0 \
--port 8000 \ --port 8000 \
--proxy-headers \
--forwarded-allow-ips='*' \
--log-config src/quartermaster/logconfig.json --log-config src/quartermaster/logconfig.json