feat(zddc-server): publishable runtime image + Codeberg CI pipeline

Batch 1 of the chart-vs-project split. The project now ships a
hardened runtime image as part of every zddc-server release; downstream
deployments (e.g. the Burns & McDonnell Helm chart) will FROM this
image instead of cloning and building from source.

zddc/Containerfile (target: server)
- Tag the runtime stage `server` so `podman build --target server`
  is unambiguous (the existing `binaries` target still works).
- Bake the bundled landing + archive tool HTML at /opt/zddc-server/web.
  Useful for self-contained demos (`ZDDC_ROOT=/opt/zddc-server/web`)
  and as a fallback web root when no external mount is supplied.
- Set fixed UID/GID 1000 for the non-root zddc user so volume
  permissions are predictable across hosts.
- Add ENV ZDDC_ROOT=/srv default so a `podman run -v data:/srv` works
  with no further config; explicit ZDDC_ROOT overrides.
- Declare VOLUME /srv to make the data-mount expectation explicit.
- Add OCI image labels (title, description, source, documentation,
  license, vendor).
- Install ca-certificates so any future outbound HTTPS works.
- Add a HEALTHCHECK for `docker run` users (Kubernetes overrides).

build.sh
- Make the cross-platform podman binary build conditional on `podman`
  being present. CI doesn't need it (the runtime container image's
  own builder stage produces linux/amd64 internally), but having
  build.sh sh-only-runnable means CI doesn't have to do nested
  containers just to assemble dist/web.
- Reorder so `zddc/dist/web/` is assembled before the binary build
  (allows the binary build to be skipped without breaking the bundle).

.woodpecker.yml (new)
- Triggers on tag push matching `zddc-server-v*`.
- Step 1 (alpine + sh): runs `sh build.sh` to assemble dist/web,
  computes the image tag (`${TAG#zddc-server-v}` plus `latest`).
- Step 2 (docker-buildx plugin): builds and publishes
  codeberg.org/varasys/zddc-server:{X.Y.Z, latest}. Auth via the
  codeberg_user / codeberg_token Woodpecker secrets — these need
  one-time setup in repo Settings; documented in zddc/README.md.

zddc/README.md
- New "Container image" section: pull URL, image properties (alpine,
  non-root UID 1000, EXPOSE 8443, VOLUME /srv, baked web bundle),
  example `podman run` invocation.
- New "Env-var contract (for chart consumers)" table: the variables
  Helm charts and Compose files should set explicitly when running
  behind a TLS-terminating reverse proxy with SSO. This is the
  documented interface between project and downstream charts.
- "Release Tagging" section now points at .woodpecker.yml and lists
  the two Woodpecker secrets that must be configured.

Validated locally:
  podman build --target server -t zddc-server-test .
  podman run -e ZDDC_ROOT=/opt/zddc-server/web -e ZDDC_TLS_CERT=none \
             -e ZDDC_INSECURE_DIRECT=1 -e ZDDC_ADDR=:8080 \
             -p 18080:8080 zddc-server-test
  curl http://localhost:18080/ → HTTP 200, bundled landing tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-04-27 14:46:59 -05:00
parent d122804bdb
commit cc35f7179b
4 changed files with 186 additions and 12 deletions

54
.woodpecker.yml Normal file
View file

@ -0,0 +1,54 @@
# Woodpecker CI for ZDDC.
#
# This pipeline only runs on `zddc-server-v*` tag pushes — it builds the
# zddc-server runtime container image and publishes it to Codeberg's
# container registry. Other tags (archive-v*, transmittal-v*, etc.) and
# regular pushes are ignored here; the HTML tool releases happen by
# pushing static files to the website (no image involved).
#
# To enable: in Codeberg → repo Settings → Woodpecker → set the secrets
# codeberg_user = your Codeberg username (e.g. VARASYS)
# codeberg_token = a personal token with package:write scope
# Generate the token at https://codeberg.org/user/settings/applications.
#
# After setup, cut a release with:
# git tag zddc-server-v0.0.1
# git push --tags
# and the pipeline will publish:
# codeberg.org/varasys/zddc-server:0.0.1
# codeberg.org/varasys/zddc-server:latest
when:
- event: tag
ref: refs/tags/zddc-server-v*
steps:
prepare-bundle:
image: docker.io/alpine:3.20
commands:
# build.sh assembles zddc/dist/web/ from landing and archive
# built outputs (which are committed force-tracked dist files).
# Falls back gracefully when podman isn't present — we don't
# need the cross-compiled binaries here, the runtime container
# builds its own linux/amd64 binary internally.
- sh build.sh
# Image tag = the bare semver after the "zddc-server-v" prefix.
# Plus a "latest" tag for convenience.
- VERSION="${CI_COMMIT_TAG#zddc-server-v}"
- printf '%s\nlatest\n' "$VERSION" > .image-tags
- echo "Will tag image with: $(cat .image-tags | tr '\n' ' ')"
publish-image:
image: woodpeckerci/plugin-docker-buildx
settings:
registry: codeberg.org
repo: codeberg.org/varasys/zddc-server
dockerfile: zddc/Containerfile
context: zddc
target: server
tags_file: .image-tags
auto_tag: false
username:
from_secret: codeberg_user
password:
from_secret: codeberg_token

View file

@ -14,11 +14,6 @@ sh "$SCRIPT_DIR/classifier/build.sh" "${1:-}" "${2:-}"
sh "$SCRIPT_DIR/mdedit/build.sh" "${1:-}" "${2:-}" sh "$SCRIPT_DIR/mdedit/build.sh" "${1:-}" "${2:-}"
sh "$SCRIPT_DIR/landing/build.sh" "${1:-}" "${2:-}" sh "$SCRIPT_DIR/landing/build.sh" "${1:-}" "${2:-}"
echo ""
echo "=== Building zddc-server binaries ==="
mkdir -p "$SCRIPT_DIR/zddc/dist/web"
podman build --target binaries -o "$SCRIPT_DIR/zddc/dist/" "$SCRIPT_DIR/zddc/" 2>&1 | grep -v "^-->"
echo "" echo ""
echo "=== Assembling zddc/dist/web/ ===" echo "=== Assembling zddc/dist/web/ ==="
# Only landing and archive ship inside the server bundle: they call the # Only landing and archive ship inside the server bundle: they call the
@ -26,11 +21,25 @@ echo "=== Assembling zddc/dist/web/ ==="
# archive) and are useless without it. transmittal, classifier, and mdedit # archive) and are useless without it. transmittal, classifier, and mdedit
# are pure client-side tools that work from file:// or any static host; # are pure client-side tools that work from file:// or any static host;
# they are released to website/ for download but not bundled with the server. # they are released to website/ for download but not bundled with the server.
mkdir -p "$SCRIPT_DIR/zddc/dist/web"
cp "$SCRIPT_DIR/landing/dist/index.html" "$SCRIPT_DIR/zddc/dist/web/index.html" cp "$SCRIPT_DIR/landing/dist/index.html" "$SCRIPT_DIR/zddc/dist/web/index.html"
cp "$SCRIPT_DIR/archive/dist/archive.html" "$SCRIPT_DIR/zddc/dist/web/archive.html" cp "$SCRIPT_DIR/archive/dist/archive.html" "$SCRIPT_DIR/zddc/dist/web/archive.html"
echo "Wrote zddc/dist/web/index.html" echo "Wrote zddc/dist/web/index.html"
echo "Wrote zddc/dist/web/archive.html" echo "Wrote zddc/dist/web/archive.html"
# Cross-compiled zddc-server binaries are built via podman if available
# (no-op otherwise — CI builds the runtime container directly via the
# Containerfile's builder stage and doesn't need host-side binaries).
echo ""
echo "=== Building zddc-server binaries ==="
if command -v podman >/dev/null 2>&1; then
podman build --target binaries -o "$SCRIPT_DIR/zddc/dist/" "$SCRIPT_DIR/zddc/" 2>&1 | grep -v "^-->"
else
echo "podman not found — skipping cross-compiled binary build."
echo " (CI builds the container image directly; this step only matters"
echo " for releasing the standalone Linux/macOS/Windows binaries.)"
fi
# ─── Bootstrap zips ────────────────────────────────────────────────────────── # ─── Bootstrap zips ──────────────────────────────────────────────────────────
# Generated from bootstrap/level{1,2}.html.tmpl on every build so they are # Generated from bootstrap/level{1,2}.html.tmpl on every build so they are
# always in sync with the current bootstrap pattern. # always in sync with the current bootstrap pattern.

View file

@ -1,4 +1,20 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
#
# Multi-stage build with three useful targets:
#
# --target binaries — scratch image holding cross-compiled binaries.
# Use `podman build --target binaries -o dist/ .`
# to extract zddc-server-{linux,darwin,windows}-*
# to the host. No image published from this stage.
#
# --target server — alpine-based runtime. Default target. Published
# as codeberg.org/varasys/zddc-server:vX.Y.Z.
#
# Build context expectations (when targeting `server`):
# dist/web/index.html and dist/web/archive.html must exist —
# produced by `sh build.sh` from the repo root. CI assembles these
# before invoking podman. See .woodpecker.yml.
#
# ─── Stage 1: build ────────────────────────────────────────────────────────── # ─── Stage 1: build ──────────────────────────────────────────────────────────
FROM docker.io/library/golang:1.24-alpine AS builder FROM docker.io/library/golang:1.24-alpine AS builder
@ -35,16 +51,51 @@ RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w"
FROM scratch AS binaries FROM scratch AS binaries
COPY --from=builder /out/ / COPY --from=builder /out/ /
# ─── Stage 3: runtime ──────────────────────────────────────────────────────── # ─── Stage 3: runtime (published image) ─────────────────────────────────────
FROM docker.io/library/alpine:3.20 FROM docker.io/library/alpine:3.20 AS server
# Non-root user LABEL org.opencontainers.image.title="zddc-server" \
RUN addgroup -S zddc && adduser -S -G zddc zddc org.opencontainers.image.description="HTTP server for ZDDC archives — ACL via .zddc files, virtual archive index, audit logging" \
org.opencontainers.image.source="https://codeberg.org/VARASYS/ZDDC" \
org.opencontainers.image.documentation="https://zddc.varasys.io/zddc-server.html" \
org.opencontainers.image.licenses="AGPL-3.0-only" \
org.opencontainers.image.vendor="VARASYS"
# wget is in the base image (busybox); explicitly install ca-certificates
# so outbound HTTPS (e.g. an upstream auth check) works if the operator
# adds anything later. Keep the install footprint minimal.
RUN apk add --no-cache ca-certificates && rm -rf /var/cache/apk/*
# Non-root user. UID/GID are deliberately fixed so volume permissions are
# predictable across hosts.
RUN addgroup -S -g 1000 zddc && adduser -S -u 1000 -G zddc zddc
# Binary
COPY --from=builder /out/zddc-server-linux-amd64 /usr/local/bin/zddc-server COPY --from=builder /out/zddc-server-linux-amd64 /usr/local/bin/zddc-server
# Bundled landing + archive tools — useful for self-contained demos and as
# a fallback web root. Set ZDDC_ROOT=/opt/zddc-server/web to serve only
# these (no external data). For real archives, mount the data tree at
# /srv (the default ZDDC_ROOT below).
COPY dist/web/index.html /opt/zddc-server/web/index.html
COPY dist/web/archive.html /opt/zddc-server/web/archive.html
# Conventional mount point for the served archive. Operators mount their
# data here (Azure Files, NFS, hostPath, …). Override with ZDDC_ROOT.
VOLUME /srv
USER zddc USER zddc
# Default config: data mount at /srv. Override at run time as needed.
# Other env vars (ZDDC_TLS_CERT, ZDDC_EMAIL_HEADER, ZDDC_CORS_ORIGIN, …)
# are intentionally not defaulted — see zddc/README.md.
ENV ZDDC_ROOT=/srv
EXPOSE 8443 EXPOSE 8443
# Liveness probe for `docker run` users. Kubernetes deployments override
# this with their own livenessProbe / readinessProbe.
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-check-certificate -q --spider https://localhost:8443/ || exit 1
ENTRYPOINT ["/usr/local/bin/zddc-server"] ENTRYPOINT ["/usr/local/bin/zddc-server"]

View file

@ -292,12 +292,59 @@ the actual built tool from `install.zip`, or a level-1/level-2 bootstrap stub th
it. Then open it via the zddc-server URL; the app will auto-connect and scan the directory it. Then open it via the zddc-server URL; the app will auto-connect and scan the directory
tree. tree.
## Building ## Container image
### Run as a container Each `zddc-server-vX.Y.Z` git tag publishes a runtime image to Codeberg's
container registry via the Woodpecker CI pipeline at `.woodpecker.yml`:
```
codeberg.org/varasys/zddc-server:X.Y.Z
codeberg.org/varasys/zddc-server:latest
```
Pull and run:
```sh ```sh
podman build -t zddc-server . podman run --rm -p 8443:8443 \
-v /srv/archive:/srv:Z \
-e ZDDC_TLS_CERT=none \
-e ZDDC_ADDR=:8080 \
-e ZDDC_INSECURE_DIRECT=1 \
codeberg.org/varasys/zddc-server:latest
```
The image:
- alpine-based, runs as non-root (UID 1000)
- exposes 8443 by default
- defaults `ZDDC_ROOT=/srv` (override or mount your archive there)
- bundles the landing + archive tool HTML at `/opt/zddc-server/web` for
self-contained demos (`ZDDC_ROOT=/opt/zddc-server/web`)
- declares `VOLUME /srv` so the operator's data mount is explicit
- ships a `HEALTHCHECK` for `docker run`; Kubernetes deployments override it
### Env-var contract (for chart consumers)
Downstream Helm charts and Compose files should set these explicitly rather
than relying on image defaults:
| Variable | Typical value (behind ingress + SSO) | Purpose |
|---|---|---|
| `ZDDC_ROOT` | `/srv` | Path of the served archive (volume mount) |
| `ZDDC_TLS_CERT` | `none` | TLS terminated upstream |
| `ZDDC_INSECURE_DIRECT` | `1` | Acknowledge plain HTTP behind a trusted proxy |
| `ZDDC_ADDR` | `:8080` | Match service / probe port |
| `ZDDC_EMAIL_HEADER` | `X-Auth-Request-Email` | Header your auth proxy sets |
| `ZDDC_CORS_ORIGIN` | `https://your-host` | Origins permitted to call back into the server |
See "Environment Variables" above for the full list.
## Building
### Run as a container (build locally)
```sh
podman build --target server -t zddc-server .
``` ```
### Compile native binaries (no Go installation required) ### Compile native binaries (no Go installation required)
@ -346,6 +393,19 @@ git tag zddc-server-v1.0.0
git push --tags git push --tags
``` ```
The `zddc-server-v*` tag triggers the `.woodpecker.yml` pipeline which
builds and publishes the container image. See "Container image" above
for the resulting image URLs.
The first time the pipeline runs you must configure two Woodpecker
secrets in repo Settings → Woodpecker:
- `codeberg_user` — your Codeberg username (e.g. `VARASYS`)
- `codeberg_token` — a personal token with `package:write` scope, generated at <https://codeberg.org/user/settings/applications>
These never live in the repo; they are referenced from the pipeline via
`from_secret:`.
--- ---
**Notes:** **Notes:**