diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..385c95d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitea +.claude +.github +dist +*.md +install.sh diff --git a/.gitea/workflows/container.yaml b/.gitea/workflows/container.yaml new file mode 100644 index 0000000..0aceb79 --- /dev/null +++ b/.gitea/workflows/container.yaml @@ -0,0 +1,73 @@ +name: Container image + +# Builds the multi-arch container image. On tag push (v*) it logs in +# to the Gitea registry on this host and publishes the image as +# git.cer.sh//: plus :latest. On pull requests +# it builds without pushing — purely a smoke test that the Dockerfile +# still works. +on: + push: + tags: + - 'v*' + pull_request: + +permissions: + contents: read + packages: write + +jobs: + image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + # github.repository is owner/name with the repo's original casing; + # registries require lowercase, so normalise once here and reuse + # the result in metadata-action below. + - name: Resolve image name + id: img + run: | + repo='${{ github.repository }}' + echo "ref=git.cer.sh/${repo,,}" >> "$GITHUB_OUTPUT" + + - name: Login to Gitea registry + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: git.cer.sh + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.img.outputs.ref }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build (and push on tag) + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} + # Inline cache embeds layer metadata into the pushed image + # itself — no external cache server needed, which keeps the + # workflow self-contained on the Gitea runner. + cache-from: type=inline + cache-to: type=inline diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..48d222d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# syntax=docker/dockerfile:1.7 + +# Build stage. Runs on the runner's native arch (BUILDPLATFORM) and +# cross-compiles the Go binary for whichever target the manifest list +# is being assembled for (TARGETOS/TARGETARCH). Keeps multi-arch +# builds fast — only the final link is per-arch, the Go toolchain is +# always native. +FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder +ARG TARGETOS +ARG TARGETARCH +ARG VERSION=dev + +WORKDIR /src + +# Module cache layer — re-uses unless go.mod/go.sum change. +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build \ + -trimpath \ + -ldflags "-s -w -X main.version=${VERSION}" \ + -o /out/qu \ + ./cmd/qu + +# Runtime stage. distroless/static has CA roots for HTTPS probes and +# nothing else — no shell, no package manager. Runs as root so the +# daemon can open ICMP sockets and write under /etc/quptime; operators +# can override at deploy time with `docker run --user`. +FROM gcr.io/distroless/static-debian12:latest + +COPY --from=builder /out/qu /usr/local/bin/qu + +ENV QUPTIME_DIR=/etc/quptime +VOLUME ["/etc/quptime"] +EXPOSE 9901 + +ENTRYPOINT ["/usr/local/bin/qu"] +CMD ["serve"]