mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) (#986)
Signed-off-by: Tyler Longwell <tlongwell@squareup.com> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc
parent
84f499cb6e
commit
1fa63badad
+50
-3
@@ -1,7 +1,54 @@
|
||||
# Build outputs
|
||||
target/
|
||||
**/target/
|
||||
desktop/src-tauri/target/
|
||||
|
||||
# Native non-relay UI builds (not part of the public image)
|
||||
desktop/
|
||||
mobile/
|
||||
|
||||
# Node artifacts (built inside the image; host outputs would invalidate cache)
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
web/dist/
|
||||
|
||||
# VCS, IDE, scratch
|
||||
.git/
|
||||
.github/
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.vscode/
|
||||
.idea/
|
||||
.scratch/
|
||||
*.md
|
||||
!README.md
|
||||
.env*
|
||||
|
||||
# Local environment / secrets — never want these in the build context
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pem
|
||||
*.key
|
||||
secrets/
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# CI artifacts and tooling that the image doesn't need
|
||||
.cache/
|
||||
coverage/
|
||||
dist/
|
||||
playwright-report/
|
||||
test-results/
|
||||
bin/
|
||||
sidecars/
|
||||
|
||||
# Docs the image doesn't ship. examples/ stays — examples/countdown-bot is
|
||||
# a Cargo workspace member, so cargo-chef needs its manifest. Markdown files
|
||||
# inside crates (Cargo.toml `readme = ...`) likewise need to be present to
|
||||
# avoid manifest warnings; the savings from excluding them are negligible.
|
||||
docs/
|
||||
|
||||
# Old Dockerfile noise (this Dockerfile is THE source of truth now)
|
||||
docker-compose.yml
|
||||
docker-compose.*.yml
|
||||
prometheus.yml
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
name: Docker image
|
||||
|
||||
# Builds and publishes the public Buzz relay image as ghcr.io/block/buzz.
|
||||
#
|
||||
# Strategy: each architecture builds on its native runner (ubuntu-24.04 for
|
||||
# amd64, ubuntu-24.04-arm for arm64), pushes to GHCR by digest, then a final
|
||||
# job stitches the per-arch digests into a single multi-arch manifest.
|
||||
# This avoids QEMU emulation (~10× slower for Rust) at zero cost on free
|
||||
# GitHub-hosted runners.
|
||||
#
|
||||
# Triggers:
|
||||
# - push to main → :main + :sha-<7>
|
||||
# - push tags v*.*.* → :latest + :{version} + :{major}.{minor} + :{major}
|
||||
# - pull_request → build only (no push), cache stays warm
|
||||
# - workflow_dispatch → manual canary
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v[0-9]*"]
|
||||
pull_request:
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
- ".dockerignore"
|
||||
- ".github/workflows/docker.yml"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "rust-toolchain.toml"
|
||||
- "crates/**"
|
||||
- "web/**"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "patches/**"
|
||||
workflow_dispatch:
|
||||
|
||||
# One image build per ref; cancel superseded PR builds, but never cancel
|
||||
# tag/main builds (publishing must not be aborted mid-flight).
|
||||
concurrency:
|
||||
group: docker-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# Single source of truth for the image name. Set GHCR_IMAGE as a repo
|
||||
# variable to override (e.g., for forks that want to push to their own
|
||||
# namespace without forking this file).
|
||||
IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push to GHCR
|
||||
id-token: write # OIDC for build provenance attestation
|
||||
attestations: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
|
||||
outputs:
|
||||
# Used downstream by `merge` to stitch the manifest.
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
with:
|
||||
# Default parallelism of 4 OOMs the 7GB GitHub runner during Rust
|
||||
# compiles (see moby/buildkit#3969). Vaultwarden hit this; we will
|
||||
# too without the cap.
|
||||
buildkitd-config-inline: |
|
||||
[worker.oci]
|
||||
max-parallelism = 2
|
||||
|
||||
- name: Log in to GHCR
|
||||
# Skip on pull_request from forks — no GHCR creds, build-only.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
# Tag matrix — every main commit gets sha-<7>, releases get full
|
||||
# semver family. Pull requests get nothing (push: false below).
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
labels: |
|
||||
org.opencontainers.image.title=Buzz
|
||||
org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Push by digest, not by tag — the merge job assembles the tags
|
||||
# into one multi-arch manifest. This is what makes the native-arm
|
||||
# matrix possible.
|
||||
outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }}
|
||||
cache-to: |
|
||||
type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:${{ matrix.arch }},mode=max,compression=zstd
|
||||
|
||||
- name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
DIGEST: ${{ steps.build.outputs.digest }}
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
touch "/tmp/digests/${DIGEST#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Merge multi-arch manifest
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push the merged manifest
|
||||
id-token: write # OIDC for provenance attestation on the manifest
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Download all per-arch digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
||||
with:
|
||||
images: ${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=sha-,format=short
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Create and push manifest list
|
||||
id: manifest
|
||||
working-directory: /tmp/digests
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Build -t flags from the metadata-action output.
|
||||
tags=()
|
||||
while IFS= read -r tag; do
|
||||
[ -n "$tag" ] && tags+=("-t" "$tag")
|
||||
done <<< "$META_TAGS"
|
||||
|
||||
# Build the digest refs from the per-arch artifacts.
|
||||
digests=()
|
||||
for digest in *; do
|
||||
digests+=("${IMAGE_NAME}@sha256:${digest}")
|
||||
done
|
||||
|
||||
docker buildx imagetools create "${tags[@]}" "${digests[@]}"
|
||||
|
||||
# Capture the merged manifest digest for the attestation step.
|
||||
first_tag=$(echo "$META_TAGS" | head -n1)
|
||||
merged_digest=$(docker buildx imagetools inspect "$first_tag" \
|
||||
--format '{{json .Manifest}}' | jq -r '.digest')
|
||||
echo "digest=${merged_digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Attest provenance for the merged image
|
||||
# Sigstore-signed in-toto attestation, verifiable with:
|
||||
# gh attestation verify oci://ghcr.io/block/buzz:<tag> --owner block
|
||||
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||
with:
|
||||
subject-name: ${{ env.IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.manifest.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
IMAGE_NAME: ${{ env.IMAGE_NAME }}
|
||||
MERGED_DIGEST: ${{ steps.manifest.outputs.digest }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
{
|
||||
echo "### Published \`${IMAGE_NAME}\`"
|
||||
echo
|
||||
echo "**Digest:** \`${MERGED_DIGEST}\`"
|
||||
echo
|
||||
echo "**Tags:**"
|
||||
echo '```'
|
||||
echo "${META_TAGS}"
|
||||
echo '```'
|
||||
echo
|
||||
echo "Verify provenance:"
|
||||
echo '```'
|
||||
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
+79
-30
@@ -1,46 +1,95 @@
|
||||
# ── Build stage (Rust) ──────────────────────────────────────
|
||||
# Hard-code --platform to prevent exec format error on ARM Macs.
|
||||
FROM --platform=linux/amd64 rust:1.95-bookworm AS builder
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Public Buzz relay image — published as ghcr.io/block/buzz:<tag>.
|
||||
#
|
||||
# Builds the `buzz-relay` binary (Rust 1.95) and the `buzz-web` static bundle
|
||||
# (pnpm + vite), then assembles them into a small debian-slim runtime with
|
||||
# `git` available (the relay shells out to git for repo hydrate / receive-pack
|
||||
# / upload-pack — see crates/buzz-relay/src/api/git).
|
||||
#
|
||||
# Multi-arch is handled by running this same Dockerfile on native amd64 and
|
||||
# native arm64 runners (see .github/workflows/docker.yml). The Dockerfile
|
||||
# itself is platform-agnostic; do not add --platform pins.
|
||||
|
||||
ARG RUST_VERSION=1.95
|
||||
ARG NODE_VERSION=24
|
||||
ARG DEBIAN_VERSION=bookworm
|
||||
|
||||
# ─── Stage 1: cargo-chef base ───────────────────────────────────────────────
|
||||
FROM rust:${RUST_VERSION}-${DEBIAN_VERSION} AS chef
|
||||
RUN cargo install cargo-chef --locked --version 0.1.71
|
||||
WORKDIR /build
|
||||
|
||||
# ─── Stage 2: plan dependency graph ─────────────────────────────────────────
|
||||
# Only the manifests are needed to compute the recipe; this layer rebuilds
|
||||
# only when Cargo.{toml,lock} or crate manifests change, not on every source
|
||||
# edit.
|
||||
FROM chef AS planner
|
||||
COPY . .
|
||||
RUN cargo build --release -p buzz-relay \
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
# ─── Stage 3: cook dependencies, then build the binary ──────────────────────
|
||||
FROM chef AS builder
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
ca-certificates \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=planner /build/recipe.json recipe.json
|
||||
# Cook the full workspace recipe — relay deps include workspace siblings, so
|
||||
# scoping to -p buzz-relay misses transitive deps and re-builds them later.
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
COPY . .
|
||||
RUN cargo build --release --locked -p buzz-relay --bin buzz-relay \
|
||||
&& strip target/release/buzz-relay
|
||||
|
||||
# ── Web build stage (Node/pnpm) ────────────────────────────
|
||||
FROM --platform=linux/amd64 node:24-bookworm-slim AS web-builder
|
||||
# ─── Stage 4: web bundle (pnpm + vite) ──────────────────────────────────────
|
||||
# Independent of the Rust layers so a CSS change doesn't bust Rust cache and
|
||||
# vice versa.
|
||||
FROM node:${NODE_VERSION}-${DEBIAN_VERSION}-slim AS web-builder
|
||||
WORKDIR /build
|
||||
RUN corepack enable
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY patches/ patches/
|
||||
COPY web/package.json web/
|
||||
RUN pnpm install --frozen-lockfile --filter buzz-web
|
||||
COPY web/ web/
|
||||
RUN corepack enable && pnpm install --frozen-lockfile --filter buzz-web
|
||||
RUN pnpm -C web build
|
||||
|
||||
# ── Runtime stage ───────────────────────────────────────────
|
||||
FROM --platform=linux/amd64 debian:bookworm-slim
|
||||
# ─── Stage 5: runtime ───────────────────────────────────────────────────────
|
||||
FROM debian:${DEBIAN_VERSION}-slim AS runtime
|
||||
|
||||
# CAKE: non-root UID 1000 (numeric, not username)
|
||||
RUN groupadd -g 1000 buzz && useradd -u 1000 -g buzz -m buzz
|
||||
# OCI annotations: required for GHCR to auto-link the image to this repo and
|
||||
# inherit its visibility. org.opencontainers.image.source is the load-bearing
|
||||
# one — without it GHCR keeps the image private even when the repo is public.
|
||||
LABEL org.opencontainers.image.title="Buzz" \
|
||||
org.opencontainers.image.description="WebSocket relay server for the Buzz communications platform" \
|
||||
org.opencontainers.image.source="https://github.com/block/buzz" \
|
||||
org.opencontainers.image.url="https://github.com/block/buzz" \
|
||||
org.opencontainers.image.documentation="https://github.com/block/buzz#readme" \
|
||||
org.opencontainers.image.licenses="Apache-2.0"
|
||||
|
||||
# CAKE: writable dirs
|
||||
RUN mkdir -p /cache /tmp && chown buzz:buzz /cache /tmp
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --system --gid 1000 buzz \
|
||||
&& useradd --system --uid 1000 --gid 1000 --home-dir /var/lib/buzz \
|
||||
--create-home --shell /usr/sbin/nologin buzz
|
||||
|
||||
# git: relay shells out to `git` for hydrate/receive-pack/upload-pack (S3-backed repos)
|
||||
# socat: Istio abstract→file socket bridge
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates git socat && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /build/target/release/buzz-relay /usr/local/bin/buzz-relay
|
||||
COPY --from=web-builder /build/web/dist /srv/buzz/web
|
||||
|
||||
COPY --from=builder /build/target/release/buzz-relay /code/buzz-relay
|
||||
COPY --from=web-builder /build/web/dist /code/web
|
||||
COPY script/start /code/start
|
||||
RUN chmod +x /code/start
|
||||
ENV BUZZ_WEB_DIR=/srv/buzz/web
|
||||
|
||||
ENV BUZZ_WEB_DIR="/code/web"
|
||||
# 3000: app (WS + REST + web UI) · 8080: /_liveness, /_readiness · 9102: /metrics
|
||||
EXPOSE 3000 8080 9102
|
||||
|
||||
# CAKE: required Envoy env vars (overridden at runtime by CAKE).
|
||||
ENV ENVOY_ADMIN_SOCKET_PATH="@envoy-admin.sock" \
|
||||
ENVOY_INGRESS_PORT="20001" \
|
||||
ENVOY_HTTP_EGRESS_SOCKET_PATH="@egress.sock" \
|
||||
ENVOY_DATADOG_PORT="3030" \
|
||||
CASH_FRAMEWORK="rust"
|
||||
USER buzz:buzz
|
||||
WORKDIR /var/lib/buzz
|
||||
|
||||
USER 1000
|
||||
ENTRYPOINT ["/code/start"]
|
||||
ENTRYPOINT ["/usr/local/bin/buzz-relay"]
|
||||
|
||||
Reference in New Issue
Block a user