Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 99 additions & 30 deletions .github/workflows/publish-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,29 @@ concurrency:
cancel-in-progress: true

jobs:
publish:
runs-on: ubuntu-latest
# One job per image per architecture, each on a native runner: the arm64 image is
# built and boot-checked on arm64 hardware, not under emulation, so "it boots" means
# the same thing on a Graviton instance as on x86. Each job pushes its image by
# digest only; `manifest` below gives the pair its tags.
build:
runs-on: ${{ matrix.runner }}
strategy:
# One failing image should not hide whether the other built.
# One failing image or architecture should not hide whether the others built.
fail-fast: false
matrix:
include:
image:
- name: gateway
dockerfile: deploy/Dockerfile
context: .
- name: dashboard
dockerfile: deploy/Dockerfile.dashboard
context: ./dashboard
arch: [amd64, arm64]
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4

Expand All @@ -58,27 +68,20 @@ jobs:
# the run, so there is no long-lived credential to rotate or leak.
password: ${{ secrets.GITHUB_TOKEN }}

- name: Work out the tags
- name: Work out the labels
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/${{ matrix.name }}
# `edge` tracks main; a vX.Y.Z tag also moves `latest` and publishes the
# major.minor so a deployment can pin how precisely it wants to.
tags: |
type=raw,value=edge,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
images: ghcr.io/${{ github.repository }}/${{ matrix.image.name }}
labels: |
org.opencontainers.image.title=agentfox-${{ matrix.name }}
org.opencontainers.image.description=AgentFox ${{ matrix.name }} — runtime firewall for AI agents
org.opencontainers.image.title=agentfox-${{ matrix.image.name }}
org.opencontainers.image.description=AgentFox ${{ matrix.image.name }} — runtime firewall for AI agents
org.opencontainers.image.licenses=Apache-2.0

# The gateway image carries ~6GB of model weights; the runner's preinstalled
# toolchains leave too little disk to load it into the local daemon.
- name: Free disk for the gateway image
if: matrix.name == 'gateway'
if: matrix.image.name == 'gateway'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
df -h /
Expand All @@ -87,28 +90,94 @@ jobs:
# the `deps` stage, so nothing ran the image until somebody pulled it — which is
# how a gateway whose detector warm-up crashed its own startup got this far.
- name: Build the gateway image locally
if: matrix.name == 'gateway'
if: matrix.image.name == 'gateway'
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
context: ${{ matrix.image.context }}
file: ${{ matrix.image.dockerfile }}
load: true
tags: agentfox-gateway:boot-check
cache-from: type=gha
# amd64 keeps the default scope, shared with ci.yml's docker-smoke.
cache-from: type=gha${{ matrix.arch == 'arm64' && ',scope=arm64' || '' }}

- name: The gateway image boots
if: matrix.name == 'gateway'
if: matrix.image.name == 'gateway'
run: scripts/check/boot_gateway_image.sh agentfox-gateway:boot-check

- name: Build and push
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
context: ${{ matrix.image.context }}
file: ${{ matrix.image.dockerfile }}
platforms: linux/${{ matrix.arch }}
labels: ${{ steps.meta.outputs.labels }}
# Shared with ci.yml's docker-smoke, so a PR that already built this layer
# set does not make the publish pay for it again.
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=image,name=ghcr.io/${{ github.repository }}/${{ matrix.image.name }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha${{ matrix.arch == 'arm64' && ',scope=arm64' || '' }}
cache-to: type=gha,mode=max${{ matrix.arch == 'arm64' && ',scope=arm64' || '' }}

- name: Keep the digest for the manifest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.image.name }}-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

# One tag, both architectures: `docker pull` picks the right one for the machine.
manifest:
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
name: [gateway, dashboard]
steps:
- uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-${{ matrix.name }}-*
merge-multiple: true

- uses: docker/setup-buildx-action@v3

- name: Sign in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Work out the tags
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/${{ matrix.name }}
# `edge` tracks main; a vX.Y.Z tag also moves `latest` and publishes the
# major.minor so a deployment can pin how precisely it wants to.
tags: |
type=raw,value=edge,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=sha,prefix=sha-

- name: Publish the multi-arch tags
working-directory: /tmp/digests
run: |
image="ghcr.io/${{ github.repository }}/${{ matrix.name }}"
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf "${image}@sha256:%s " *)

- name: Both architectures are there
run: |
image="ghcr.io/${{ github.repository }}/${{ matrix.name }}"
tag="$(jq -r '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON" | sed 's/.*://')"
docker buildx imagetools inspect "${image}:${tag}" | tee /tmp/inspect
grep -q "linux/amd64" /tmp/inspect && grep -q "linux/arm64" /tmp/inspect
9 changes: 6 additions & 3 deletions deploy/model-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
MODEL_SERVER_DOMAIN=models.example.com

# Shared with the gateway (AGENTFOX_MODEL_SERVER_TOKEN there too).
# Generate one: python -c "import secrets; print(secrets.token_urlsafe(32))"
# Generate one: python3 -c "import secrets; print(secrets.token_urlsafe(32))"
AGENTFOX_MODEL_SERVER_TOKEN=

# A pinned release, so this machine changes only when you pull on purpose.
AGENTFOX_IMAGE=ghcr.io/architsharm/agentfox/gateway:latest
# Which image to run. `edge` is main. Once it works, pin what you tested so this
# machine changes only when you decide: a commit (`:sha-<7 chars>`) or a release
# (`:0.5.0`). `latest` moves only on releases and can lag main by weeks.
# Published for amd64 and arm64 (Graviton); `docker pull` picks the right one.
AGENTFOX_IMAGE=ghcr.io/architsharm/agentfox/gateway:edge

# auto | cpu | cuda
AGENTFOX_MODEL_DEVICE=auto
22 changes: 15 additions & 7 deletions deploy/model-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ with its URL. The detectors that need it are then skipped, never silently passed
The classifier and backstop take about 1.5–2 GB of RAM and 25–50 ms per short prompt
on CPU.

- **Instance:** c7i.large or c6i.large (2 vCPU, 4 GB). Avoid burstable `t3`/`t4g`:
once CPU credits run out, inference slows and requests start timing out.
- **Instance:** c7g.large (Graviton, arm64) or c7i.large / c6i.large (x86), each
2 vCPU and 4 GB. The image is published for both architectures. Avoid burstable
`t3`/`t4g`: once CPU credits run out, inference slows and requests start timing out.
- **Disk:** at least 30 GB. The image is about 6 GB with its weights.
- **Region:** the same region as the gateway. Vercel's default `iad1` is AWS
`us-east-1`. Each hop across regions adds tens of milliseconds to a 300 ms budget.
- **Security group:** inbound 443 and 80 (Let's Encrypt), and SSH from your address
Expand All @@ -32,14 +34,19 @@ on CPU.
Then, on the instance:

```bash
sudo dnf install -y docker && sudo systemctl enable --now docker # Amazon Linux 2023
# docker compose plugin: https://docs.docker.com/compose/install/linux/
# Docker and the compose plugin: https://docs.docker.com/engine/install/
# (Amazon Linux 2023: sudo dnf install -y docker && sudo systemctl enable --now docker)
sudo usermod -aG docker $USER && newgrp docker # run docker without sudo
git clone https://github.com/architsharm/agentfox && cd agentfox/deploy/model-server
cp .env.example .env # set MODEL_SERVER_DOMAIN and AGENTFOX_MODEL_SERVER_TOKEN
docker compose up -d
docker compose pull && docker compose up -d
docker compose logs -f models # ready once each model logs "warm in …"
curl -H "Authorization: Bearer $TOKEN" https://$MODEL_SERVER_DOMAIN/health
```

Generate the token on the machine and paste it only into `.env` and the gateway's
settings. Not into chat, tickets or shell history you share.

`/health` lists each detector, whether it can run, and the device it runs on.

Then point the gateway at it. On Vercel, these are project environment variables:
Expand All @@ -61,8 +68,9 @@ every gateway deploy:
docker compose pull && docker compose up -d
```

Pin `AGENTFOX_IMAGE` to a release tag (`ghcr.io/architsharm/agentfox/gateway:0.4.0`)
so a pull is always a decision. Mismatched versions are tolerated: the gateway sends
`.env` starts on `:edge`, which is main. Once it works, pin what you tested so a pull
is always a decision: a commit (`:sha-1a2b3c4`, one is published for every push to
main) or a release (`:0.5.0`). Avoid `:latest`: it moves only on releases. Mismatched versions are tolerated: the gateway sends
a detector name and text, and reads back detections.

## GPU
Expand Down
6 changes: 3 additions & 3 deletions deploy/model-server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
services:
models:
# The published gateway image already carries every permissive-licence model's
# weights. Pin a release tag rather than :edge, so this machine changes only
# when you decide it should.
image: ${AGENTFOX_IMAGE:-ghcr.io/architsharm/agentfox/gateway:latest}
# weights, for amd64 and arm64. Pin a `sha-` or version tag in .env once it works,
# so this machine changes only when you decide it should.
image: ${AGENTFOX_IMAGE:-ghcr.io/architsharm/agentfox/gateway:edge}
command: ["agentfox", "serve", "models", "--host", "0.0.0.0", "--port", "8090"]
restart: unless-stopped
environment:
Expand Down
Loading