Mirror drawbar CI/CD: push to LAN registry, pull via public FQDN
Split the registry endpoints like the drawbar containers. Per-component Gitea Actions workflows (build-backend, build-frontend; runs-on docker, path-filtered) push images to the LAN endpoint 192.168.0.2:1234 over plain HTTP (buildx insecure/http) to bypass Cloudflare's request-body limit, then link each package to the repo via the Gitea API. Auth via the REGISTRY_TOKEN Actions secret (the same token drawbar uses). Tag scheme: test-main / test-sha-<long> / version / latest (v* tags).
The deploy compose now PULLS git.jpaul.io/justin/provenance-{backend,frontend}:${IMAGE_TAG:-test-main} (no host build); docker-compose.dev.yml is a local-build override for dev / pre-CI. Replaces the previous single build.yml. Docs + memory updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
name: build-backend
|
||||
|
||||
# Builds + pushes the backend image to justin/provenance-backend's package area
|
||||
# on Gitea on every merge to main. Servers pull from git.jpaul.io.
|
||||
#
|
||||
# Push goes to the LAN registry endpoint 192.168.0.2:1234 (plain HTTP) to bypass
|
||||
# Cloudflare's request-body limit; pulls use the public git.jpaul.io FQDN. Same
|
||||
# Gitea registry either way. Mirrors the drawbar setup.
|
||||
#
|
||||
# Tag scheme: test-main | test-sha-<long> | <semver from pyproject> | latest (v* tags)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- '.gitea/workflows/build-backend.yml'
|
||||
|
||||
concurrency:
|
||||
group: build-backend-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version from pyproject.toml
|
||||
id: ver
|
||||
run: |
|
||||
v=$(grep -oP '^version = "\K[^"]+' backend/pyproject.toml | head -1)
|
||||
if [ -z "$v" ]; then echo "could not parse version from backend/pyproject.toml"; exit 1; fi
|
||||
echo "semver=$v" >> "$GITHUB_OUTPUT"
|
||||
echo "backend semver: $v"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# LAN registry serves plain HTTP on :1234 (git.jpaul.io is the only TLS
|
||||
# endpoint, via Cloudflare). Treat the LAN endpoint as insecure so
|
||||
# buildkit doesn't try to upgrade the push to HTTPS.
|
||||
config-inline: |
|
||||
[registry."192.168.0.2:1234"]
|
||||
http = true
|
||||
insecure = true
|
||||
|
||||
- name: Configure registry credentials for buildx
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_TOKEN" | base64 -w0)
|
||||
cat > ~/.docker/config.json <<EOF
|
||||
{
|
||||
"auths": {
|
||||
"192.168.0.2:1234": {
|
||||
"auth": "$AUTH"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: 192.168.0.2:1234/justin/provenance-backend
|
||||
tags: |
|
||||
type=ref,event=branch,prefix=test-
|
||||
type=sha,prefix=test-sha-,format=long
|
||||
type=raw,value=${{ steps.ver.outputs.semver }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://git.jpaul.io/justin/provenance
|
||||
org.opencontainers.image.version=${{ steps.ver.outputs.semver }}
|
||||
|
||||
- name: Build and push (amd64)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Link package to the provenance repo
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
code=$(curl -s -o /tmp/link.out -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"https://git.jpaul.io/api/v1/packages/justin/container/provenance-backend/-/link/provenance")
|
||||
echo "link -> provenance: HTTP $code"
|
||||
case "$code" in
|
||||
201) echo "OK — newly linked" ;;
|
||||
400|409) echo "OK — already linked" ;;
|
||||
*) cat /tmp/link.out; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,102 @@
|
||||
name: build-frontend
|
||||
|
||||
# Builds + pushes the Next.js image to justin/provenance-frontend's package area
|
||||
# on Gitea on every merge to main. Servers pull from git.jpaul.io.
|
||||
#
|
||||
# Push -> LAN registry 192.168.0.2:1234 (plain HTTP); pull -> git.jpaul.io.
|
||||
# Mirrors the drawbar setup; see build-backend.yml for the rationale.
|
||||
#
|
||||
# Tag scheme: test-main | test-sha-<long> | <version from package.json> | latest (v* tags)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- '.gitea/workflows/build-frontend.yml'
|
||||
|
||||
concurrency:
|
||||
group: build-frontend-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract version from package.json
|
||||
id: ver
|
||||
run: |
|
||||
v=$(grep -oP '"version"\s*:\s*"\K[^"]+' frontend/package.json | head -1)
|
||||
if [ -z "$v" ]; then echo "could not parse version from frontend/package.json"; exit 1; fi
|
||||
echo "semver=$v" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend version: $v"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# See build-backend.yml for why these flags are needed.
|
||||
config-inline: |
|
||||
[registry."192.168.0.2:1234"]
|
||||
http = true
|
||||
insecure = true
|
||||
|
||||
- name: Configure registry credentials for buildx
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: |
|
||||
mkdir -p ~/.docker
|
||||
AUTH=$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_TOKEN" | base64 -w0)
|
||||
cat > ~/.docker/config.json <<EOF
|
||||
{
|
||||
"auths": {
|
||||
"192.168.0.2:1234": {
|
||||
"auth": "$AUTH"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Compute tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: 192.168.0.2:1234/justin/provenance-frontend
|
||||
tags: |
|
||||
type=ref,event=branch,prefix=test-
|
||||
type=sha,prefix=test-sha-,format=long
|
||||
type=raw,value=${{ steps.ver.outputs.semver }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://git.jpaul.io/justin/provenance
|
||||
org.opencontainers.image.version=${{ steps.ver.outputs.semver }}
|
||||
|
||||
- name: Build and push (amd64)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./frontend
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Link package to the provenance repo
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
code=$(curl -s -o /tmp/link.out -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"https://git.jpaul.io/api/v1/packages/justin/container/provenance-frontend/-/link/provenance")
|
||||
echo "link -> provenance: HTTP $code"
|
||||
case "$code" in
|
||||
201) echo "OK — newly linked" ;;
|
||||
400|409) echo "OK — already linked" ;;
|
||||
*) cat /tmp/link.out; exit 1 ;;
|
||||
esac
|
||||
@@ -1,63 +0,0 @@
|
||||
name: build-images
|
||||
|
||||
# Gitea Actions build container images and push to the Gitea registry on
|
||||
# git.jpaul.io. Servers pull to deploy — no build on the host.
|
||||
#
|
||||
# Requires repo/org secrets: REGISTRY_USERNAME, REGISTRY_PASSWORD (a token with
|
||||
# package:write). Adjust the runner label to one your Gitea runner advertises.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
|
||||
env:
|
||||
REGISTRY: git.jpaul.io
|
||||
IMAGE_BASE: git.jpaul.io/${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_BASE }}/backend:latest
|
||||
${{ env.IMAGE_BASE }}/backend:${{ github.sha }}
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Gitea registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./frontend
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_BASE }}/frontend:latest
|
||||
${{ env.IMAGE_BASE }}/frontend:${{ github.sha }}
|
||||
@@ -29,7 +29,7 @@ These are product invariants, not preferences. Do not violate them, and flag any
|
||||
- **Object storage:** S3-compatible (MinIO for self-host).
|
||||
- **Edge:** Caddy reverse proxy; optional Cloudflare Tunnel (preferred ingress, never required).
|
||||
- **Email:** operator-configured SMTP.
|
||||
- **CI/CD:** Gitea Actions on `git.jpaul.io` build container images to the Gitea registry; servers pull to deploy.
|
||||
- **CI/CD:** Gitea Actions build per-component images. **Push** to the LAN registry `192.168.0.2:1234` (plain HTTP, bypasses Cloudflare's body limit); **pull** via the public `git.jpaul.io` FQDN. Servers pull to deploy — no host build. Mirrors the drawbar setup; see [[gitea-lan-push-fqdn-pull]].
|
||||
|
||||
Pick libraries consistent with this stack. If you introduce a significant dependency or a new service, note it in ARCHITECTURE.md in the same change.
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
# --- Core ---
|
||||
APP_ENV=development
|
||||
|
||||
# --- Images (pulled from git.jpaul.io; CI pushes to the LAN registry) ---
|
||||
# test-main = current main build; or pin a semver / test-sha-<sha> for rollback.
|
||||
IMAGE_TAG=test-main
|
||||
|
||||
# --- Database (Postgres) ---
|
||||
POSTGRES_USER=provenance
|
||||
POSTGRES_PASSWORD=change-me
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
name: provenance
|
||||
|
||||
# Dev override: build the backend/frontend images locally instead of pulling
|
||||
# them from the registry. Layer it on top of the base compose:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
#
|
||||
# Use this before CI has published images, or to test local changes.
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
@@ -2,6 +2,11 @@ name: provenance
|
||||
|
||||
# One stack stands up the whole system. Configuration is entirely env-driven
|
||||
# (see .env.example). Run from this directory: `docker compose up -d`.
|
||||
#
|
||||
# backend/frontend are PULLED from the public registry (git.jpaul.io); CI pushes
|
||||
# them to the LAN endpoint (192.168.0.2:1234). For local building instead of
|
||||
# pulling, layer the dev override:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -36,9 +41,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main}
|
||||
environment:
|
||||
APP_ENV: ${APP_ENV:-development}
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance}
|
||||
@@ -58,9 +61,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../frontend
|
||||
dockerfile: Dockerfile
|
||||
image: git.jpaul.io/justin/provenance-frontend:${IMAGE_TAG:-test-main}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
|
||||
@@ -170,8 +170,10 @@ Jobs are idempotent and retryable; an external failure degrades gracefully rathe
|
||||
|
||||
## 12. Deployment & CI/CD
|
||||
|
||||
- **Images** are built by **Gitea Actions** on `git.jpaul.io` and pushed to the **Gitea container registry**.
|
||||
- Servers **pull** new images to deploy — no build on the host.
|
||||
- **Images** are built by **Gitea Actions** (`runs-on: docker`) and pushed to the **Gitea container registry**, one package per component (`provenance-backend`, `provenance-frontend`) linked to the repo.
|
||||
- **Split push/pull endpoints** (mirrors the drawbar setup): CI **pushes** to the LAN registry endpoint `192.168.0.2:1234` over plain HTTP (buildx configured `insecure`/`http`) to bypass the Cloudflare request-body limit; servers **pull** from the public `git.jpaul.io` FQDN (TLS via Cloudflare). Same Gitea registry, two front doors. Auth uses the `REGISTRY_TOKEN` Actions secret.
|
||||
- Tag scheme: `test-main` (current main), `test-sha-<long>` (rollback pins), the component version, and `latest` on `v*` tags.
|
||||
- Servers **pull** new images to deploy — no build on the host. The deploy compose references `git.jpaul.io/justin/provenance-{backend,frontend}:${IMAGE_TAG:-test-main}`; `docker-compose.dev.yml` is a local-build override.
|
||||
- **Caddy** terminates TLS and reverse-proxies frontend + backend. **Cloudflare Tunnel** is the preferred ingress (no open inbound ports) but is never required; a plain Caddy-on-a-public-host deployment is equally supported.
|
||||
- **Configuration** is entirely environment-driven (twelve-factor). One `.env` plus the compose file is enough to stand up a deployment.
|
||||
- **Migrations** run on backend start (or via an explicit job) so an image pull + restart is a complete upgrade.
|
||||
|
||||
Reference in New Issue
Block a user