ci: build + publish aisix container image to GHCR - #27
Conversation
Adds a Dockerfile at the repo root and a docker-image.yml workflow that publishes the aisix AI gateway image to ghcr.io/moonming/ai-gateway. Motivation: downstream consumers (notably the aisix.cloud integrated E2E suite in api7/AISIX-Cloud#3) need a pre-built container image to exercise "AISIX DP ↔ control plane" cooperation in CI. Previously each consumer had to clone this repo and cargo build themselves. Dockerfile (root): Stage 1 rust:1.93-bookworm cargo build --release --bin aisix (rust-toolchain.toml pins 1.93.1) protoc installed for prost/tonic-build BuildKit cache mounts on cargo registry and target/ to speed incremental builds Stage 2 debian:bookworm-slim runs as uid 10001 non-root tini as PID 1 for signal forwarding ca-certificates for outbound TLS expects /etc/aisix/config.yaml at runtime (override via --config or AISIX_CONFIG env) Workflow (.github/workflows/docker-image.yml): Triggers push main → :main + :sha-<shortsha> tags v*.*.* → :X.Y.Z + :X.Y + :X + :latest pull_request → build-only, no push (sanity check) workflow_dispatch → manual; 'tag' input publishes an additional custom tag (e.g. aisix-e2e) Permissions contents: read, packages: write Cache type=gha (GitHub Actions cache backend) Build docker/build-push-action@v6 with metadata action@v5 for OCI labels + tag matrix Image destination is ghcr.io/moonming/ai-gateway (same GHCR namespace as this repo). Downstream orgs republishing under their own namespace can do so in their own workflows via `docker pull` + `docker push`. No runtime code changes.
There was a problem hiding this comment.
Pull request overview
Adds container build + publishing automation so downstream systems can consume aisix as a pre-built GHCR image instead of building from source.
Changes:
- Introduces a multi-stage
Dockerfileto build theaisixrelease binary and run it as a non-root user withtini. - Adds a GitHub Actions workflow to build (PRs) and publish (main/tags/manual) the image to GHCR with a tag/label strategy.
- Adds a
.dockerignoreto reduce Docker build context noise.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| Dockerfile | Multi-stage Rust build (builder + slim runtime) for the aisix binary. |
| .github/workflows/docker-image.yml | Build/push workflow using buildx + metadata-action + GHA cache. |
| .dockerignore | Excludes common non-image inputs from the Docker build context. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Leverage Docker layer cache: copy manifests first so dependency | ||
| # compilation only re-runs when Cargo.toml / Cargo.lock change. |
There was a problem hiding this comment.
The comment about leveraging layer cache is currently inaccurate: this layer copies crates/ before the build, so any source change will invalidate the dependency build cache. Consider copying only manifests first (workspace + per-crate Cargo.toml), doing a cargo fetch/dummy build to cache deps, then copying full sources (or update the comment if you intentionally accept full invalidation).
| # Leverage Docker layer cache: copy manifests first so dependency | |
| # compilation only re-runs when Cargo.toml / Cargo.lock change. | |
| # Copy top-level manifests and then the full workspace sources. | |
| # Because `crates/` is copied before the build, changes under `crates/` | |
| # will invalidate the subsequent cargo build layer as well. |
| # The release build is what we ship. | ||
| RUN --mount=type=cache,target=/usr/local/cargo/registry \ | ||
| --mount=type=cache,target=/src/target \ | ||
| cargo build --release --bin aisix \ |
There was a problem hiding this comment.
For reproducible container builds in CI, it’s safer to add --locked to the cargo build invocation so the build fails if Cargo.lock is out of date instead of silently resolving new versions during the image build.
| cargo build --release --bin aisix \ | |
| cargo build --locked --release --bin aisix \ |
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates tini \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin aisix | ||
|
|
||
| COPY --from=builder /usr/local/bin/aisix /usr/local/bin/aisix | ||
|
|
||
| # Proxy + admin listeners from config.example.yaml. | ||
| EXPOSE 3000 3001 | ||
|
|
||
| USER aisix | ||
|
|
||
| # tini forwards signals cleanly to the aisix process. | ||
| ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/aisix"] | ||
| CMD ["--config", "/etc/aisix/config.yaml"] |
There was a problem hiding this comment.
The image runs as a non-root user and the default CMD points at /etc/aisix/config.yaml, but the Dockerfile never creates /etc/aisix (or /etc/aisix/tls). Creating these directories at build time (and ensuring they’re readable by uid 10001) makes bind-mounting a config directory and adding TLS certs much more reliable.
| RUN --mount=type=cache,target=/usr/local/cargo/registry \ | ||
| --mount=type=cache,target=/src/target \ | ||
| cargo build --release --bin aisix \ |
There was a problem hiding this comment.
RUN --mount=type=cache requires BuildKit; without an explicit Dockerfile frontend directive, local docker build runs with BuildKit disabled will fail. Consider adding a # syntax=docker/dockerfile:1.x header (and/or documenting the BuildKit requirement) to make this more robust for local users.
| tags: | | ||
| type=ref,event=branch | ||
| type=ref,event=pr | ||
| type=sha,prefix=sha-,format=short | ||
| type=semver,pattern={{version}} | ||
| type=semver,pattern={{major}}.{{minor}} | ||
| type=semver,pattern={{major}} | ||
| type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} | ||
| type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '' }} |
There was a problem hiding this comment.
Workflow comment/tag strategy says tag pushes produce only :X.Y.Z, :X.Y, :X, :latest, but docker/metadata-action is also configured to always add a sha-<short> tag. Either update the documented strategy comment/PR description or gate the sha tag to branch pushes so releases match the stated tags.
| ui/node_modules | ||
| ui/.next | ||
| ui/dist |
There was a problem hiding this comment.
The Docker build only copies Cargo* + crates/, so keeping ui/ sources in the build context adds transfer time without affecting the image. Consider ignoring ui/ entirely (or at least ui/src) to speed up CI builds and reduce cache churn.
| ui/node_modules | |
| ui/.next | |
| ui/dist | |
| ui |
- Dockerfile: add `# syntax=docker/dockerfile:1.7` so BuildKit cache
mounts are available on older `docker build` invocations that don't
enable BuildKit by default.
- Dockerfile: pass `--locked` to `cargo build --release` so a stale
`Cargo.lock` fails CI fast instead of silently resolving new deps
in the image.
- Dockerfile: create `/etc/aisix/{,tls}` and `/var/lib/aisix` at build
time, owned by the aisix user (uid 10001). Makes bind-mounting
config/TLS directories ergonomic without post-install chown.
- Dockerfile: clarify the layer-cache / BuildKit comment — the cache
mounts carry `target/` + `$CARGO_HOME/registry`, so source changes
still reuse compiled dependencies; the extra manifests-only warm
stage is a diminishing-return win for this workspace.
- .dockerignore: exclude `ui/` entirely. The Dockerfile only copies
`Cargo.*` + `crates/`, so shipping ~50 MB of `ui/node_modules`
(or even just the ui sources) into the build context is wasted
transfer time.
- docker-image.yml: clarify tag-strategy comment — `:sha-<shortsha>`
is always appended regardless of trigger; prior comment read as if
it only applied to main-branch pushes.
Review response + CI statusAddressed Copilot review comments in
|
| # | Comment | Fix |
|---|---|---|
| 1 | Dockerfile: add # syntax=docker/dockerfile:1.7 for BuildKit cache mounts on older clients |
✅ Added header |
| 2 | Dockerfile: --locked on cargo build |
✅ Now cargo build --locked --release --bin aisix |
| 3 | Dockerfile: create /etc/aisix{,/tls} + /var/lib/aisix owned by aisix uid |
✅ Added to the runtime-stage RUN |
| 4 | Dockerfile: clarify the layer-cache comment — source changes DO invalidate the COPY crates layer |
✅ Comment now explains that BuildKit cache mounts on target/ + $CARGO_HOME/registry carry compiled deps across builds; an extra manifests-only warm stage is a diminishing-return win for this workspace (cargo workspaces don't support the manifest-only copy trick cleanly; would need a per-crate COPY Cargo.toml dance) |
| 5 | Workflow: tag-strategy comment said :sha-<shortsha> only on main; it's actually always appended |
✅ Comment corrected |
| 6 | .dockerignore: exclude ui/ entirely (build context only needs Cargo + crates) |
✅ Added ui |
About the two failing checks
Both build ui and rust unit + coverage failed at the artifact upload step with:
##[error]Failed to CreateArtifact: Artifact storage quota has been hit.
Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.
The tests themselves passed cleanly — e.g. rust unit ended with test result: ok. 57 passed; 0 failed. The failure is a repo-level Actions storage quota issue, unrelated to this PR's changes (Dockerfile + workflow YAML + .dockerignore cannot cause Rust / Node build artifacts to hit quota).
Two unrelated workarounds that may help the repo going forward:
- Set
actions/upload-artifact@v4withretention-days: 3(or 7) to cycle out coverage + ui-dist artifacts faster. - Purge old artifacts via the Settings → Actions → Storage page.
The new docker-image workflow this PR adds passed its PR-trigger build job (exit code 0, validates the Dockerfile without publishing).
Ready to merge
From my side the PR is ready. Once merged to main, the first push will publish ghcr.io/moonming/ai-gateway:main + :sha-<shortsha> automatically. For the aisix.cloud E2E suite I'll need the :aisix-e2e tag — I can trigger that via workflow_dispatch with tag=aisix-e2e after merge.
Summary
Adds a root
Dockerfileand a.github/workflows/docker-image.ymlworkflow that publishes the aisix AI gateway image toghcr.io/moonming/ai-gateway.Motivation
Downstream consumers need a pre-built container image to wire aisix into integrated test stacks without cloning this repo and running
cargo buildfrom scratch each time.The immediate consumer is the aisix.cloud integrated E2E suite at api7/AISIX-Cloud#3, whose
e2e/compose.ymlassumesghcr.io/api7/aisix-dp:aisix-e2eis available. After this PR lands and publishes, api7.cloud will just pull from this namespace (or re-tag underapi7/in a small downstream workflow). The AISIX.cloud PR #3 also documents the overall E2E strategy (5 happy-path scenarios verifying DP ↔ control plane cooperation).Dockerfile design
rust:1.93-bookworm, installsprotobuf-compilerforprost/tonic-build, uses BuildKit cache mounts on$CARGO_HOME/registryandtarget/so incremental pushes reuse deps.debian:bookworm-slim, non-root uid10001,tinias PID 1 for clean signal forwarding,ca-certificatesfor outbound TLS. Exposes:3000(proxy) and:3001(admin) to matchconfig.example.yaml. Config path is/etc/aisix/config.yamlby default; override with--configorAISIX_CONFIG.Workflow tag strategy
pushtomain:main,:sha-<shortsha>pushtagvX.Y.Z:X.Y.Z,:X.Y,:X,:latestpull_requeston Dockerfile / crates / workflowworkflow_dispatchtaginput publishes an additional custom tag (e.g.aisix-e2efor downstream integration testing)Uses
docker/metadata-action@v5+docker/build-push-action@v6with GitHub Actions cache (type=gha).Image destination
ghcr.io/moonming/ai-gateway— same GHCR namespace as this repository. Downstream organisations can re-tag under their own namespace with a small follow-up workflow if desired (e.g.docker pull+docker tag+docker pushtoghcr.io/api7/aisix-dp:<tag>).Test plan
cargo fmt --check/cargo clippyuntouched (no code changes, existingci.ymlstill runs)pushtomainpublishesghcr.io/moonming/ai-gateway:mainand:sha-<shortsha>workflow_dispatchwithtag=aisix-e2epublishes the tag used by aisix.cloud E2ENo runtime code changes
This PR is strictly build + CI. The binary's behaviour, CLI flags, and config schema are unchanged.