Skip to content

Ship self-hostable Docker image (registry release + CI + consumer compose) #39

@mforce

Description

@mforce

Goal

Make Collectify trivially installable for a non-developer self-hoster: pull a tagged image from a registry, drop a small docker-compose.yml, set a few env vars, docker compose up -d. No clone, no toolchain, no docker build.

Do we need to split the build image and the consumer image?

No. The current Dockerfile is already multi-stage:

  • client-build (node:22-alpine) → builds the React bundle.
  • server-build (mcr.microsoft.com/dotnet/sdk:10.0) → restores + publishes the API; copies React dist/ into wwwroot/.
  • runtime (mcr.microsoft.com/dotnet/aspnet:10.0) → final image with only the published bits + non-root user.

Only the runtime stage ships. The SDKs are intermediate stages and get discarded. This is the standard "builder vs consumer" split, just inside one Dockerfile. Splitting into two top-level images would double the maintenance and buy nothing. Keep the single multi-stage Dockerfile.

What we do need is to publish the runtime image so self-hosters never run docker build themselves, plus a few quality-of-life bits below.

What's missing for a real release

1. Publish to a registry (Gitea-first, registry-agnostic workflow)

The maintainer's primary target is a self-hosted Gitea instance, but the release workflow should be parameterised by env/secrets so swapping to GHCR, Docker Hub, or any other OCI registry is a config change, not a code change.

  • Inputs: REGISTRY (e.g. git.example.com), IMAGE_NAME (e.g. mforce/collectify), REGISTRY_USER, REGISTRY_TOKEN.
  • docker login $REGISTRY -u "$REGISTRY_USER" -p "$REGISTRY_TOKEN", then build/push to $REGISTRY/$IMAGE_NAME:<tag>.
  • docker-compose.yml currently says build: ., which forces clone-and-build. Switch the consumer compose to image: ${REGISTRY:-ghcr.io}/${IMAGE_NAME:-mforce/collectify}:${TAG:-latest} with env-var defaults so the same file works against any registry.
  • Keep build: . available in a docker-compose.dev.yml (or as an override) for contributors.

2. Multi-arch builds

  • linux/amd64 (NAS, mini-PC, x86 home server).
  • linux/arm64 (Raspberry Pi 4/5, Apple Silicon, Ampere VMs).
  • Build via docker buildx + QEMU. Same workflow, two platforms in one manifest.

3. Release pipeline (.github/workflows/release.yml)

  • Trigger: push of tag matching v*.*.*.
  • Gate on: dotnet test + npm test -- --run.
  • Build multi-arch image once.
  • Push tags derived from the git tag: :vX.Y.Z, :vX.Y, :vX, :latest.
  • Inject build metadata via --build-arg / LABEL (commit SHA, build date, version).
  • Optional: SBOM (CycloneDX) + cosign provenance attestation.

4. Versioning

  • Tie release tag → image tag → Collectify.Api.dll assembly version. Either bump <Version> in Collectify.Api.csproj per release, or pass --version-suffix / -p:Version= at publish time from the workflow.
  • Surface the version somewhere the UI can show it (extend /api/auth/me, or add a tiny /api/version). Helps bug reports.

5. Healthcheck

There's no /api/health today. Add a cheap endpoint that returns { status: "ok", version } without touching the DB (avoid making the healthcheck a write/migration-stall trap). Then wire it in the Dockerfile:

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s \
  CMD wget -qO- http://localhost:8080/api/health || exit 1

Compose / Watchtower / k8s liveness probes all key off this.

6. Consumer-facing compose example + README rewrite

  • README "Quick start" should show the pull-from-registry flavour, not clone-and-build.
  • Document the /data volume (DB + cover BLOBs), the optional .env for provider keys (Collectify__Metadata__Tmdb__ApiKey etc), and how to override the registry/image/tag for users on different infra.
  • Sample .env already exists; cross-link it.

7. Auto-backup before EF Core migrations (nice-to-have)

Migrations are applied on startup. If a self-hoster pulls a new image and a migration breaks them, they need their previous collectify.db.

  • On boot, if assembly version > recorded version in DB, copy collectify.db/data/backups/collectify-<old-version>-<utc>.db before Database.MigrateAsync().
  • Rotate to last N backups (default 10) so the volume doesn't grow unbounded.
  • Document the recovery path: stop container, swap DB file, start.

8. CHANGELOG.md

Standard "Keep a Changelog" format. Self-hosters who pull a new tag want to know what changed without diffing.

9. Optional polish

  • Pin base-image digests for reproducibility (mcr.microsoft.com/dotnet/aspnet:10.0@sha256:…). Renovate/Dependabot can keep them fresh.
  • Try mcr.microsoft.com/dotnet/aspnet:10.0-alpine for a smaller image; benchmark before/after.
  • Add OCI labels (org.opencontainers.image.source/licenses/description/version) so the registry's package page looks right.
  • Run trivy or grype on the published image in CI; fail on HIGH/CRITICAL.

Suggested phasing (each independent enough to be its own PR)

  1. Health endpoint + Dockerfile HEALTHCHECK — no infra dependencies, smallest scope.
  2. Release workflow + multi-arch publish (registry-agnostic via env/secrets; default to maintainer's Gitea).
  3. Consumer compose example + README rewrite (depends on Phase 1: Foundation — auth, CRUD for movies/music/games, Docker #2).
  4. DB auto-backup before migrations.
  5. CHANGELOG + OCI labels + trivy scan.
  6. Optional: alpine runtime, digest pinning.

Out of scope

  • Kubernetes / Helm chart.
  • Reverse proxy bundling (Caddy/Traefik) — document in README, don't ship.
  • Auto-update via Watchtower — document, don't enforce.
  • Multi-instance / clustering — single-binary + SQLite + single volume is the design.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions