Build & CI
The Ultron Infra node is arm64, so every image is built linux/arm64. The build
cross-compiles a static binary, drops it into a distroless runtime, and (if the app
has migrations) bundles the migrate CLI alongside it so the same image can run
schema migrations. The result is pushed to GHCR.
Build flow
Section titled “Build flow”flowchart LR src([<app> repo]) --> bx[buildx<br/>linux/arm64] bx --> b1[stage 1: build<br/>cross-compile static binary] b1 --> b2[stage 2: distroless<br/>binary + migrate + migrations] b2 --> push[/ghcr.io/owner/<app>:sha-XXXX/]
- Add the Dockerfile to the app repo — multi-stage, distroless, migrate bundled.
- Add the .dockerignore to keep secrets and build output out of the context.
- Add the GitHub Actions workflow that builds
linux/arm64and pushes to GHCR. - Push to
main, then grab thesha-<short>tag for the next page.
Dockerfile
Section titled “Dockerfile”Multi-stage: build on the runner’s native arch, cross-compile to the target. CGO
off yields a static binary that runs on distroless/static. Drop the migrate
download and the migrations copy if <app> has no DB.
# syntax=docker/dockerfile:1
# ---- build ----------------------------------------------------------------# Build on native arch, cross-compile to target.FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build
ARG TARGETOS=linuxARG TARGETARCH=arm64ARG VERSION=devARG MIGRATE_VERSION=v4.19.1
WORKDIR /src
# Dependencies first, for layer caching.COPY go.mod go.sum ./RUN go mod download
COPY . .
# CGO off → static binary that runs on distroless/static.RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \ -o /out/<app> ./cmd/api
# golang-migrate CLI, bundled so the migrations Job can run it.RUN curl -fsSL "https://github.com/golang-migrate/migrate/releases/download/${MIGRATE_VERSION}/migrate.${TARGETOS}-${TARGETARCH}.tar.gz" \ | tar -xz -C /out migrate
# ---- runtime --------------------------------------------------------------FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /appCOPY --from=build /out/<app> /app/<app>COPY --from=build /out/migrate /app/migrateCOPY --from=build /src/migrations /app/migrations
EXPOSE 8080USER nonroot:nonrootENTRYPOINT ["/app/<app>"]Notes:
$BUILDPLATFORM+TARGETARCHlet buildx build on an x86 runner and emit arm64. SettingTARGETARCH=arm64as the default keeps a plaindocker buildarm64-correct too.VERSIONis injected at link time (-X main.version=...) from a CI-derivedgit describe.- distroless
:nonroothas no shell or package manager — the smallest viable attack surface. The container runs asnonroot:nonroot.
.dockerignore
Section titled “.dockerignore”Keep secrets, VCS, and build output out of the build context:
# Build outputbin/tmp/<app>*.exe*.test
# Environment / secrets.env.env.local.env.*.local
# VCS & CI (not needed in the build context).git/.github/
# Docs & specs*.mdopenapi.yaml
# IDE / editor / OS.idea/.vscode/*.swp*.swo.DS_Store._*
# Logs / coverage*.logcoverage.**.out*.profGitHub Actions → GHCR
Section titled “GitHub Actions → GHCR”Builds linux/arm64 with buildx, logs in with the workflow’s GITHUB_TOKEN, and
tags the image sha-<short> (plus latest on the default branch and the semver on a
v* tag). The sha- tag is what you pin in rollout.yaml.
name: build-image
on: push: branches: [ main ] tags: [ 'v*' ]
env: IMAGE: ghcr.io/<owner>/<app>
jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history for `git describe`
- name: Derive version id: ver run: echo "version=$(git describe --tags --always --dirty)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3
- name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Image metadata (tags + labels) id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE }} tags: | type=sha type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{version}}
- name: Build and push (linux/arm64) uses: docker/build-push-action@v6 with: context: . platforms: linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | VERSION=${{ steps.ver.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=maxtype=sha yields a tag like sha-ed927c2. Grab it from the Actions run (or the GHCR
package page) and use it in the next step.
Image in GHCR → GitOps & deploy to point Argo CD at it.