feat(P03): docker release — multi-stage Dockerfile + Gitea container registry publish
REQ-046: Docker image published to Gitea container registry per release. Dockerfile: multi-stage (golang:1.25 -> distroless/static-debian12:nonroot). CGO_ENABLED=0, ORCA_HOME=/var/lib/orca, ENTRYPOINT [/orca]. Image size: ~28MB. Runs as nonroot. .coreci.yml: new container-publish step in release pipeline (docker:24-cli, builds + tags + login + push + logout). scripts/release.sh: docker build + push after Gitea release. Graceful skip if docker absent or GITEA_TOKEN unset. Env-overridable registry. .dockerignore: excludes .git, bin/, .env, .ciagent/, testdata/, *.tar.gz. docs/docker.md: pull, run, state persistence (volume mount), local build, manual publish guide. Verified: docker build + run version/init with volume persistence. ---ci--- project: orca phase: 3 milestone: v0.5 status: verify ---/ci---
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"phase": 2,
|
||||
"stage": "complete",
|
||||
"phase": 3,
|
||||
"stage": "verify",
|
||||
"milestone": "v0.5",
|
||||
"milestone_slug": "distribution",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-03T18:55:00Z",
|
||||
"updated_at": "2026-08-03T19:00:00Z",
|
||||
"milestone_complete": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Phase 3 Verification: Docker Release (v0.5 P3)
|
||||
|
||||
**Phase**: 3 (docker release)
|
||||
**Milestone**: v0.5 Distribution
|
||||
**Requirements covered**: REQ-046
|
||||
**Date**: 2026-08-03
|
||||
|
||||
## Structural Layer
|
||||
|
||||
- `go vet ./...` → clean.
|
||||
- `go build ./...` → succeeds.
|
||||
- New files: `Dockerfile`, `.dockerignore`, `docs/docker.md`.
|
||||
- Modified files: `.coreci.yml` (container-publish step), `scripts/release.sh` (docker publish).
|
||||
- `.dockerignore` excludes `.git`, `bin/`, `.env`, `.ciagent/`, `testdata/`, `*.tar.gz`.
|
||||
|
||||
## Behavioral Layer
|
||||
|
||||
### Docker build
|
||||
- `docker build --build-arg VERSION=v0.4.4-test ... -t orca-test:v0.4.4 .` → succeeds.
|
||||
- Multi-stage build: `golang:1.25` (builder) → `gcr.io/distroless/static-debian12:nonroot` (runtime).
|
||||
- `CGO_ENABLED=0` guarantees static binary (modernc/sqlite is pure Go).
|
||||
|
||||
### Docker run
|
||||
- `docker run --rm orca-test:v0.4.4 version` → `orca version v0.4.4-test` ✓
|
||||
- `docker run --rm orca-test:v0.4.4 version --json` → valid JSON with version/commit/build_time ✓
|
||||
- `docker run --rm -v orca-test-data:/var/lib/orca orca-test:v0.4.4 init` → creates `/var/lib/orca` ✓
|
||||
- Volume persistence: state dir created in named volume, verified with alpine container ✓
|
||||
|
||||
### Image metrics
|
||||
- Image size: 27.9MB (distroless static + Go binary).
|
||||
- Runs as `nonroot` user (distroless default).
|
||||
- `ENV ORCA_HOME=/var/lib/orca` set for volume-mountable state.
|
||||
|
||||
### .coreci.yml release pipeline
|
||||
- New `container-publish` step added after `gitea-release`.
|
||||
- Uses `docker:24-cli` image with `GITEA_TOKEN` as registry credential.
|
||||
- Builds, tags (`<version>` + `latest`), logs in, pushes, logs out.
|
||||
|
||||
### scripts/release.sh extension
|
||||
- After Gitea release: `docker build` + `docker login` + `docker push`.
|
||||
- Skips gracefully if `docker` not on PATH (local dev without docker).
|
||||
- Skips push if `GITEA_TOKEN` not set (builds locally only).
|
||||
- Env-overridable: `CONTAINER_REGISTRY`, `CONTAINER_OWNER`, `CONTAINER_IMAGE`.
|
||||
|
||||
### Regression — Go tests
|
||||
- `internal/cli/` ✓ (cached)
|
||||
- `internal/store/` ✓ (cached)
|
||||
|
||||
## Security Layer
|
||||
|
||||
- `.dockerignore` excludes `.env`, `.gitleaks-baseline.json`, `bin/` — no secrets in image.
|
||||
- Image runs as `nonroot` (distroless default) — least privilege.
|
||||
- `docker login` uses `--password-stdin` (no password in process args / shell history).
|
||||
- `docker logout` after push — no credential leakage.
|
||||
- No secret material baked into the image — `GITEA_TOKEN` is used at push time only, not in the build.
|
||||
|
||||
## Quality Layer
|
||||
|
||||
- **Reproducible build**: `--build-arg VERSION/GIT_COMMIT/BUILD_TIME` injected via `-ldflags`.
|
||||
- **Minimal image**: distroless static-debian12 — no shell, no package manager, ~28MB total.
|
||||
- **Graceful degradation**: `release.sh` skips docker publish when docker is absent.
|
||||
- **CI integration**: `.coreci.yml` container-publish step uses `docker:24-cli` (has docker CLI).
|
||||
- **Documentation**: `docs/docker.md` covers pull, run, state persistence, local build, manual publish.
|
||||
|
||||
## Must-Haves Checklist
|
||||
|
||||
- [x] `docker build -t orca-test .` succeeds locally.
|
||||
- [x] `docker run --rm orca-test version` prints the version.
|
||||
- [x] `scripts/release.sh vX.Y.Z` publishes both the Gitea release AND the container image.
|
||||
- [x] `.coreci.yml` release pipeline includes the container-publish step.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** — all 4 verification layers pass. REQ-046 is satisfied. Ready
|
||||
to ship as `v0.4.4`.
|
||||
+20
@@ -112,3 +112,23 @@ pipelines:
|
||||
--title "Orca ${VERSION}"
|
||||
--note-file CHANGELOG.md
|
||||
--asset orca-${VERSION}-linux-amd64.tar.gz
|
||||
- name: container-publish
|
||||
description: Build and publish OCI image to Gitea container registry (REQ-046)
|
||||
image: docker:24-cli
|
||||
env:
|
||||
GITEA_TOKEN: ${GITEA_TOKEN}
|
||||
VERSION: ${CI_COMMIT_TAG}
|
||||
GIT_COMMIT: ${CI_COMMIT_SHA}
|
||||
BUILD_TIME: ${CI_BUILD_TIME}
|
||||
commands:
|
||||
- docker build
|
||||
--build-arg VERSION=${VERSION}
|
||||
--build-arg GIT_COMMIT=${GIT_COMMIT}
|
||||
--build-arg BUILD_TIME=${BUILD_TIME}
|
||||
-t git.cloudinit.dev/coreci/orca:${VERSION}
|
||||
-t git.cloudinit.dev/coreci/orca:latest
|
||||
.
|
||||
- echo "${GITEA_TOKEN}" | docker login git.cloudinit.dev -u cloudinit-bot --password-stdin
|
||||
- docker push git.cloudinit.dev/coreci/orca:${VERSION}
|
||||
- docker push git.cloudinit.dev/coreci/orca:latest
|
||||
- docker logout git.cloudinit.dev
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.git
|
||||
.githooks
|
||||
.bin
|
||||
bin/
|
||||
*.tar.gz
|
||||
*.tar.gz.asc
|
||||
.env
|
||||
.env.*
|
||||
.gitleaks-baseline.json
|
||||
.gitleaks.toml
|
||||
.golangci.yml
|
||||
.ciagent/
|
||||
testdata/
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
LICENSE
|
||||
coverage.out
|
||||
orca
|
||||
orca-v*
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# Dockerfile — multi-stage build for orca
|
||||
#
|
||||
# Stage 1: build the static binary with golang:1.25
|
||||
# Stage 2: distroless static runtime (CGO-free, ~2MB image)
|
||||
#
|
||||
# Build args:
|
||||
# VERSION — semver tag injected via -ldflags (e.g. v0.4.4)
|
||||
# GIT_COMMIT — short commit hash
|
||||
# BUILD_TIME — ISO 8601 build timestamp
|
||||
#
|
||||
# Build:
|
||||
# docker build --build-arg VERSION=v0.4.4 -t git.cloudinit.dev/coreci/orca:v0.4.4 .
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm git.cloudinit.dev/coreci/orca:v0.4.4 version
|
||||
# docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG GIT_COMMIT=unknown
|
||||
ARG BUILD_TIME=unknown
|
||||
|
||||
# --- Stage 1: build -------------------------------------------------------
|
||||
|
||||
FROM golang:1.25 AS builder
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_COMMIT
|
||||
ARG BUILD_TIME
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Cache module downloads — copy go.mod/go.sum first, download, then copy source.
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
# CGO_ENABLED=0 guarantees a static binary (modernc/sqlite is pure Go).
|
||||
RUN CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags="-s -w \
|
||||
-X git.cloudinit.dev/coreci/orca/internal/cli.version=${VERSION} \
|
||||
-X git.cloudinit.dev/coreci/orca/internal/cli.gitCommit=${GIT_COMMIT} \
|
||||
-X git.cloudinit.dev/coreci/orca/internal/cli.buildTime=${BUILD_TIME}" \
|
||||
-o /orca ./cmd/orca
|
||||
|
||||
# --- Stage 2: runtime -----------------------------------------------------
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
# ORCA_HOME points to a volume-mountable path inside the container.
|
||||
# Mount a volume at /var/lib/orca to persist state across container restarts.
|
||||
ENV ORCA_HOME=/var/lib/orca
|
||||
|
||||
COPY --from=builder /orca /orca
|
||||
|
||||
ENTRYPOINT ["/orca"]
|
||||
@@ -0,0 +1,96 @@
|
||||
# Docker Guide
|
||||
|
||||
Orca is available as a container image on the Gitea container registry.
|
||||
The image is a minimal distroless static build (~2MB runtime layer)
|
||||
that runs the orca binary directly.
|
||||
|
||||
## Image
|
||||
|
||||
```
|
||||
git.cloudinit.dev/coreci/orca:<version>
|
||||
git.cloudinit.dev/coreci/orca:latest
|
||||
```
|
||||
|
||||
The image is built from the `Dockerfile` in the repo root:
|
||||
- **Build stage**: `golang:1.25` — compiles a static binary with
|
||||
`CGO_ENABLED=0` (modernc/sqlite is pure Go, no CGO).
|
||||
- **Runtime stage**: `gcr.io/distroless/static-debian12:nonroot` —
|
||||
~2MB, no shell, runs as `nonroot` user.
|
||||
|
||||
## Pull
|
||||
|
||||
```bash
|
||||
docker pull git.cloudinit.dev/coreci/orca:latest
|
||||
# or pin a version
|
||||
docker pull git.cloudinit.dev/coreci/orca:v0.4.4
|
||||
```
|
||||
|
||||
The repo is public (REQ-045), so anonymous pull works without login.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# Print version
|
||||
docker run --rm git.cloudinit.dev/coreci/orca:v0.4.4 version
|
||||
|
||||
# Initialize state (creates /var/lib/orca/ inside the container)
|
||||
docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init
|
||||
|
||||
# Run the daemon (persist state via volume)
|
||||
docker run -d --name orca \
|
||||
-p 8080:8080 \
|
||||
-v orca-data:/var/lib/orca \
|
||||
git.cloudinit.dev/coreci/orca:v0.4.4 daemon --addr=:8080
|
||||
```
|
||||
|
||||
## State Persistence
|
||||
|
||||
The image sets `ENV ORCA_HOME=/var/lib/orca`. All orca state (SQLite
|
||||
database, CA certs, server certs) is written under this path. To
|
||||
persist state across container restarts, mount a volume:
|
||||
|
||||
```bash
|
||||
docker volume create orca-data
|
||||
docker run --rm -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 init
|
||||
docker run -d --name orca -p 8080:8080 -v orca-data:/var/lib/orca git.cloudinit.dev/coreci/orca:v0.4.4 daemon
|
||||
```
|
||||
|
||||
Without a volume, state is lost when the container exits.
|
||||
|
||||
## System-Level Namespace Inside Containers
|
||||
|
||||
The `--system` flag is not needed inside containers — the image already
|
||||
sets `ORCA_HOME=/var/lib/orca`. Use `--system` only if you want a
|
||||
different namespace root (e.g., `/root/.orca`), which requires running
|
||||
as root (the distroless image runs as `nonroot` by default).
|
||||
|
||||
## Build Locally
|
||||
|
||||
```bash
|
||||
docker build --build-arg VERSION=v0.4.4 -t orca-local:v0.4.4 .
|
||||
docker run --rm orca-local:v0.4.4 version
|
||||
```
|
||||
|
||||
Build args:
|
||||
- `VERSION` — semver tag (injected via `-ldflags`)
|
||||
- `GIT_COMMIT` — short commit hash
|
||||
- `BUILD_TIME` — ISO 8601 build timestamp
|
||||
|
||||
## Publish (for maintainers)
|
||||
|
||||
The `.coreci.yml` release pipeline includes a `container-publish` step
|
||||
that builds and pushes the image on every tag release. To publish
|
||||
manually:
|
||||
|
||||
```bash
|
||||
export GITEA_TOKEN=<token>
|
||||
docker build --build-arg VERSION=v0.4.4 -t git.cloudinit.dev/coreci/orca:v0.4.4 -t git.cloudinit.dev/coreci/orca:latest .
|
||||
echo "$GITEA_TOKEN" | docker login git.cloudinit.dev -u cloudinit-bot --password-stdin
|
||||
docker push git.cloudinit.dev/coreci/orca:v0.4.4
|
||||
docker push git.cloudinit.dev/coreci/orca:latest
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Install Guide](install.md) — binary install (alternative to Docker).
|
||||
- [Namespace and Paths](namespace.md) — `ORCA_HOME` and `--system` flag.
|
||||
@@ -136,3 +136,39 @@ tea releases create "$VERSION" \
|
||||
--asset "$TARBALL"
|
||||
|
||||
info "✓ release $VERSION published"
|
||||
|
||||
# --- publish container image to gitea registry (REQ-046) ------------------
|
||||
# Skipped gracefully if docker is not on PATH (e.g. local dev without docker).
|
||||
# The .coreci.yml release pipeline has a dedicated container-publish step
|
||||
# that runs in a docker:24-cli image with docker-in-docker.
|
||||
|
||||
CONTAINER_REGISTRY="${CONTAINER_REGISTRY:-git.cloudinit.dev}"
|
||||
CONTAINER_OWNER="${CONTAINER_OWNER:-coreci}"
|
||||
CONTAINER_IMAGE="${CONTAINER_IMAGE:-orca}"
|
||||
IMAGE="${CONTAINER_REGISTRY}/${CONTAINER_OWNER}/${CONTAINER_IMAGE}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
info "docker not found on PATH — skipping container image publish (CI handles it)."
|
||||
else
|
||||
info "building container image ${IMAGE}:${VERSION}..."
|
||||
docker build \
|
||||
--build-arg VERSION="$VERSION" \
|
||||
--build-arg GIT_COMMIT="$GIT_COMMIT" \
|
||||
--build-arg BUILD_TIME="$BUILD_TIME" \
|
||||
-t "${IMAGE}:${VERSION}" \
|
||||
-t "${IMAGE}:latest" \
|
||||
"$REPO_ROOT"
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
info "GITEA_TOKEN not set — skipping docker push (image built locally only)."
|
||||
else
|
||||
info "logging in to ${CONTAINER_REGISTRY}..."
|
||||
echo "$GITEA_TOKEN" | docker login "$CONTAINER_REGISTRY" -u cloudinit-bot --password-stdin
|
||||
info "pushing ${IMAGE}:${VERSION}..."
|
||||
docker push "${IMAGE}:${VERSION}"
|
||||
info "pushing ${IMAGE}:latest..."
|
||||
docker push "${IMAGE}:latest"
|
||||
docker logout "$CONTAINER_REGISTRY"
|
||||
info "✓ container image ${IMAGE}:${VERSION} published"
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user