Skip to content

Docker images

Docker images #23

# Three Alpine-based images: raw (uvicorn), nginx (nginx+mkcert+uvicorn), granian (granian+mkcert).
# Triggers:
# - workflow_call: CI (main/testing) and release → always build, test, push (parallel jobs).
# - workflow_dispatch: manual — test-only (no Hub) or full publish (parallel jobs, same as call).
name: Docker images
on:
workflow_call:
inputs:
source_ref:
description: Branch, tag, or SHA to build
type: string
required: false
default: ""
mode:
description: >-
build-and-test-only — amd64 build + in-container tests, no Docker Hub.
publish — build, smoke-test, and push raw + nginx + granian images.
type: string
required: false
default: publish
workflow_dispatch:
inputs:
source_ref:
description: Branch, tag, or SHA to build
type: string
required: true
default: main
mode:
description: >-
build-and-test-only — amd64 build + in-container tests, no Docker Hub.
publish — same as CI/release (push raw + nginx + granian, multi-arch).
type: choice
required: true
options:
- build-and-test-only
- publish
default: build-and-test-only
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Required since v0.0.10: proxbox-api refuses to start without an
# encryption key (or PROXBOX_ALLOW_PLAINTEXT_CREDENTIALS=1). Use a
# constant CI-only key so smoke containers go through the same code
# path operators use in production.
PROXBOX_ENCRYPTION_KEY: proxbox-ci-only-encryption-key-not-a-secret
jobs:
# Sequential local test (manual); no registry login or push.
docker-manual:
if: inputs.mode == 'build-and-test-only'
name: Docker — build and test only (manual)
runs-on: ubuntu-latest
steps:
- name: Resolve checkout ref
id: checkout_source
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.sha }}
run: |
set -euo pipefail
encoded_ref="$(python3 - <<'PY'
import os
from urllib.parse import quote
print(quote(os.environ["SOURCE_REF"], safe=""))
PY
)"
sha="$(gh api "repos/${{ github.repository }}/commits/$encoded_ref" --jq .sha)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.checkout_source.outputs.sha }}
- name: Validate pyproject version vs tag ref
id: version
env:
GIT_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.sha }}
run: |
python3 <<'PY'
import os
import pathlib
import tomllib
git_ref = os.environ["GIT_REF"].strip()
version = tomllib.loads(
pathlib.Path("pyproject.toml").read_text(encoding="utf-8")
)["project"]["version"]
expected_tag = f"v{version}"
if git_ref.startswith("v") and git_ref != expected_tag:
raise SystemExit(
f"git_ref/version mismatch: ref={git_ref!r}, expected {expected_tag!r}"
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"tag={version}\n")
PY
- name: Keep image tag in env
run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV
- name: Create Docker test script
run: |
cat <<'EOF' > /tmp/docker-tests.sh
#!/bin/sh
set -eux
# tests/ is bind-mounted read-only; keep bytecode out of the tree
export PYTHONPYCACHEPREFIX=/tmp/pycache
mkdir -p "$PYTHONPYCACHEPREFIX"
export PROXBOX_SKIP_NETBOX_BOOTSTRAP=1
uv sync --frozen --extra test --group dev
uv run ruff check .
uv run ruff format --check .
uv run ty check proxbox_api/types proxbox_api/utils/retry.py proxbox_api/schemas/sync.py
uv run python -m compileall proxbox_api scripts tests
uv run python -c "import proxbox_api.main"
uv run python -c "from proxbox_api.proxmox_to_netbox.proxmox_schema import load_proxmox_generated_openapi; assert load_proxmox_generated_openapi().get('paths')"
uv run pytest --cov=proxbox_api --cov-report=term-missing --cov-report=xml tests --ignore=tests/e2e \
--deselect=tests/test_qemu_guest_agent_sync.py::test_vm_sync_prefers_guest_agent_ip \
--deselect=tests/test_qemu_guest_agent_sync.py::test_vm_sync_falls_back_to_config_when_guest_agent_unavailable \
--deselect=tests/test_qemu_guest_agent_sync.py::test_vm_sync_skips_guest_agent_call_when_disabled \
--deselect=tests/test_qemu_guest_agent_sync.py::test_vm_sync_ignore_ipv6_link_local_false_includes_fe80
EOF
chmod +x /tmp/docker-tests.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
# --- raw image ---
- name: Build amd64 raw image for in-container tests
uses: docker/build-push-action@v6
with:
context: .
target: raw
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-raw-pre
cache-from: type=gha,scope=raw
cache-to: type=gha,mode=max,scope=raw
- name: Run tests in raw image
run: |
docker run --rm \
-e PROXBOX_ENCRYPTION_KEY \
-w /workspace \
-v /tmp/docker-tests.sh:/tmp/docker-tests.sh \
-v "${{ github.workspace }}:/workspace" \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-raw-pre \
/tmp/docker-tests.sh
- name: Smoke-test raw image startup
run: |
set -eux
docker rm -f proxbox-smoke-raw 2>/dev/null || true
docker run -d --name proxbox-smoke-raw \
-p 127.0.0.1:18080:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-raw-pre
for i in $(seq 1 40); do
if curl -fsS --max-time 5 http://127.0.0.1:18080/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-raw
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-raw
docker rm -f proxbox-smoke-raw
exit 1
# --- nginx image ---
- name: Build amd64 nginx image for in-container tests
uses: docker/build-push-action@v6
with:
context: .
target: nginx
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx-pre
cache-from: type=gha,scope=nginx
cache-to: type=gha,mode=max,scope=nginx
- name: Run tests in nginx image
run: |
docker run --rm \
-e PROXBOX_ENCRYPTION_KEY \
-w /workspace \
-v /tmp/docker-tests.sh:/tmp/docker-tests.sh \
-v "${{ github.workspace }}:/workspace" \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx-pre \
/tmp/docker-tests.sh
- name: Smoke-test nginx image startup
run: |
set -eux
docker rm -f proxbox-smoke-nginx 2>/dev/null || true
docker run -d --name proxbox-smoke-nginx \
-p 127.0.0.1:18443:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx-pre
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-nginx
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-nginx
docker rm -f proxbox-smoke-nginx
exit 1
# --- granian image ---
- name: Build amd64 granian image for in-container tests
uses: docker/build-push-action@v6
with:
context: .
target: granian
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian-pre
cache-from: type=gha,scope=granian
cache-to: type=gha,mode=max,scope=granian
- name: Run tests in granian image
run: |
docker run --rm \
-e PROXBOX_ENCRYPTION_KEY \
-w /workspace \
-v /tmp/docker-tests.sh:/tmp/docker-tests.sh \
-v "${{ github.workspace }}:/workspace" \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian-pre \
/tmp/docker-tests.sh
- name: Smoke-test granian image startup
run: |
set -eux
docker rm -f proxbox-smoke-granian 2>/dev/null || true
docker run -d --name proxbox-smoke-granian \
-p 127.0.0.1:18444:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian-pre
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18444/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-granian
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-granian
docker rm -f proxbox-smoke-granian
exit 1
# Parallel publish jobs — one per image variant.
docker-raw:
if: inputs.mode == 'publish'
name: Docker Hub — raw (uvicorn only, HTTP)
runs-on: ubuntu-latest
steps:
- name: Resolve checkout ref
id: checkout_source
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.sha }}
run: |
set -euo pipefail
encoded_ref="$(python3 - <<'PY'
import os
from urllib.parse import quote
print(quote(os.environ["SOURCE_REF"], safe=""))
PY
)"
sha="$(gh api "repos/${{ github.repository }}/commits/$encoded_ref" --jq .sha)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.checkout_source.outputs.sha }}
- name: Resolve source revision
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Resolve image version from pyproject
id: version
env:
GIT_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.ref_name }}
run: |
python3 <<'PY'
import os
import pathlib
import tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
version = data["project"]["version"]
expected_tag = f"v{version}"
git_ref = os.environ["GIT_REF"].strip()
if git_ref.startswith("v") and git_ref != expected_tag:
raise SystemExit(
f"git ref / pyproject mismatch: ref={git_ref!r}, expected {expected_tag!r}"
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"tag={version}\n")
PY
- name: Keep image tag in env
run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV
- name: Build amd64 raw image for pre-release tests
uses: docker/build-push-action@v6
with:
context: .
target: raw
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-raw-pre
cache-from: type=gha,scope=raw
cache-to: type=gha,mode=max,scope=raw
- name: Smoke-test raw image before push
run: |
set -eux
docker rm -f proxbox-smoke-raw 2>/dev/null || true
docker run -d --name proxbox-smoke-raw \
-p 127.0.0.1:18080:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-raw-pre
for i in $(seq 1 40); do
if curl -fsS --max-time 5 http://127.0.0.1:18080/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-raw
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-raw
docker rm -f proxbox-smoke-raw
exit 1
- name: Build and push raw Docker image
uses: docker/build-push-action@v6
with:
context: .
target: raw
platforms: linux/amd64,linux/arm64
push: true
tags: |
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}
emersonfelipesp/proxbox-api:latest
emersonfelipesp/proxbox-api:sha-${{ steps.source.outputs.sha }}
cache-from: type=gha,scope=raw
cache-to: type=gha,mode=max,scope=raw
- name: Smoke-test pushed raw images
run: |
set -eux
for TAG in "${{ env.IMAGE_TAG }}" latest "sha-${{ steps.source.outputs.sha }}"; do
NAME=$(printf 'proxbox-smoke-%s' "$TAG" | tr '.:' '-')
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" \
-p 127.0.0.1:18080:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:"$TAG"
ok=0
for i in $(seq 1 40); do
if curl -fsS --max-time 5 http://127.0.0.1:18080/ >/dev/null 2>&1; then
ok=1
break
fi
sleep 2
done
if [ "$ok" -ne 1 ]; then
docker logs "$NAME"
docker rm -f "$NAME"
exit 1
fi
docker rm -f "$NAME"
done
docker-nginx:
if: inputs.mode == 'publish'
name: Docker Hub — nginx (nginx + mkcert HTTPS + uvicorn)
runs-on: ubuntu-latest
steps:
- name: Resolve checkout ref
id: checkout_source
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.sha }}
run: |
set -euo pipefail
encoded_ref="$(python3 - <<'PY'
import os
from urllib.parse import quote
print(quote(os.environ["SOURCE_REF"], safe=""))
PY
)"
sha="$(gh api "repos/${{ github.repository }}/commits/$encoded_ref" --jq .sha)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.checkout_source.outputs.sha }}
- name: Resolve source revision
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Resolve image version from pyproject
id: version
env:
GIT_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.ref_name }}
run: |
python3 <<'PY'
import os
import pathlib
import tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
version = data["project"]["version"]
expected_tag = f"v{version}"
git_ref = os.environ["GIT_REF"].strip()
if git_ref.startswith("v") and git_ref != expected_tag:
raise SystemExit(
f"git ref / pyproject mismatch: ref={git_ref!r}, expected {expected_tag!r}"
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"tag={version}\n")
PY
- name: Keep image tag in env
run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV
- name: Build amd64 nginx image for pre-release tests
uses: docker/build-push-action@v6
with:
context: .
target: nginx
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx-pre
cache-from: type=gha,scope=nginx
cache-to: type=gha,mode=max,scope=nginx
- name: Smoke-test nginx image before push
run: |
set -eux
docker rm -f proxbox-smoke-nginx 2>/dev/null || true
docker run -d --name proxbox-smoke-nginx \
-p 127.0.0.1:18443:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx-pre
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-nginx
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-nginx
docker rm -f proxbox-smoke-nginx
exit 1
- name: Build and push nginx Docker image
uses: docker/build-push-action@v6
with:
context: .
target: nginx
platforms: linux/amd64,linux/arm64
push: true
tags: |
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-nginx
emersonfelipesp/proxbox-api:latest-nginx
emersonfelipesp/proxbox-api:sha-${{ steps.source.outputs.sha }}-nginx
cache-from: type=gha,scope=nginx
cache-to: type=gha,mode=max,scope=nginx
- name: Smoke-test pushed nginx images
run: |
set -eux
for TAG in "${{ env.IMAGE_TAG }}-nginx" latest-nginx "sha-${{ steps.source.outputs.sha }}-nginx"; do
NAME=$(printf 'proxbox-smoke-%s' "$TAG" | tr '.:' '-')
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" \
-p 127.0.0.1:18443:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:"$TAG"
ok=0
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18443/ >/dev/null 2>&1; then
ok=1
break
fi
sleep 2
done
if [ "$ok" -ne 1 ]; then
docker logs "$NAME"
docker rm -f "$NAME"
exit 1
fi
docker rm -f "$NAME"
done
docker-granian:
if: inputs.mode == 'publish'
name: Docker Hub — granian (granian + mkcert HTTPS)
runs-on: ubuntu-latest
steps:
- name: Resolve checkout ref
id: checkout_source
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.sha }}
run: |
set -euo pipefail
encoded_ref="$(python3 - <<'PY'
import os
from urllib.parse import quote
print(quote(os.environ["SOURCE_REF"], safe=""))
PY
)"
sha="$(gh api "repos/${{ github.repository }}/commits/$encoded_ref" --jq .sha)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.checkout_source.outputs.sha }}
- name: Resolve source revision
id: source
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Resolve image version from pyproject
id: version
env:
GIT_REF: ${{ inputs.source_ref != '' && inputs.source_ref || github.ref_name }}
run: |
python3 <<'PY'
import os
import pathlib
import tomllib
data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8"))
version = data["project"]["version"]
expected_tag = f"v{version}"
git_ref = os.environ["GIT_REF"].strip()
if git_ref.startswith("v") and git_ref != expected_tag:
raise SystemExit(
f"git ref / pyproject mismatch: ref={git_ref!r}, expected {expected_tag!r}"
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
fh.write(f"tag={version}\n")
PY
- name: Keep image tag in env
run: echo "IMAGE_TAG=${{ steps.version.outputs.tag }}" >> $GITHUB_ENV
- name: Build amd64 granian image for pre-release tests
uses: docker/build-push-action@v6
with:
context: .
target: granian
platforms: linux/amd64
push: false
load: true
tags: emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian-pre
cache-from: type=gha,scope=granian
cache-to: type=gha,mode=max,scope=granian
- name: Smoke-test granian image before push
run: |
set -eux
docker rm -f proxbox-smoke-granian 2>/dev/null || true
docker run -d --name proxbox-smoke-granian \
-p 127.0.0.1:18444:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian-pre
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18444/ >/dev/null 2>&1; then
docker rm -f proxbox-smoke-granian
exit 0
fi
sleep 2
done
docker logs proxbox-smoke-granian
docker rm -f proxbox-smoke-granian
exit 1
- name: Build and push granian Docker image
uses: docker/build-push-action@v6
with:
context: .
target: granian
platforms: linux/amd64,linux/arm64
push: true
tags: |
emersonfelipesp/proxbox-api:${{ env.IMAGE_TAG }}-granian
emersonfelipesp/proxbox-api:latest-granian
emersonfelipesp/proxbox-api:sha-${{ steps.source.outputs.sha }}-granian
cache-from: type=gha,scope=granian
cache-to: type=gha,mode=max,scope=granian
- name: Smoke-test pushed granian images
run: |
set -eux
for TAG in "${{ env.IMAGE_TAG }}-granian" latest-granian "sha-${{ steps.source.outputs.sha }}-granian"; do
NAME=$(printf 'proxbox-smoke-%s' "$TAG" | tr '.:' '-')
docker rm -f "$NAME" 2>/dev/null || true
docker run -d --name "$NAME" \
-p 127.0.0.1:18444:8000 \
-e PROXBOX_SKIP_NETBOX_BOOTSTRAP=1 \
-e PROXBOX_ENCRYPTION_KEY \
emersonfelipesp/proxbox-api:"$TAG"
ok=0
for i in $(seq 1 40); do
if curl -kfsS --max-time 5 https://127.0.0.1:18444/ >/dev/null 2>&1; then
ok=1
break
fi
sleep 2
done
if [ "$ok" -ne 1 ]; then
docker logs "$NAME"
docker rm -f "$NAME"
exit 1
fi
docker rm -f "$NAME"
done