Deploy from GitHub via OIDC
Configure trust in the portal, copy the generated GitHub Actions workflow, and push. It builds an image, pushes it to your registry, and rolls it out to a container with no long-lived secrets.
How a deploy works
Every push to your trusted branch runs one workflow that does three things, all authenticated with short-lived GitHub OIDC tokens, with no static passwords or stored API keys anywhere in the flow:
- Registry push. GitHub Actions logs in to
registry.alawadi.cloudwithdocker login -u oidc(a token minted at run time), then builds and pushes your image tagged with the commit SHA. - Deploy. The same run exchanges another OIDC token for a short-lived, single-use deploy token, then rolls that exact image out to one container.
Two separate trusts gate this: a registry push trust (which repo may push images) and a container deploy trust (which repo may deploy to a specific container). Pushing an image alone does not deploy: you need both. The portal sets up both with forms and hands you a ready-to-paste workflow, so you do not hand-write YAML or look up IDs.
Set it up in the portal
The portal generates the entire workflow for you, pre-filled with your registry address, audiences, container ID, and deploy audience. You copy it and adjust only your build specifics.
Open Registries, create or select your registry, and open its detail page. The Push setup (GitHub Actions OIDC) card holds the registry trust form.
In Push setup, fill GitHub repository (owner/repo), Branch ref
(pre-filled refs/heads/main), and optionally Environment, then click
Configure trust. The card shows the read-only OIDC Audience the
registry assigned. You never type it.
Scroll to the CI workflow templates card and pick your runtime tab (Node ·
Express, Next.js · React, Python · FastAPI, Python · Django, Go, PHP · Laravel,
Java · Spring Boot, Rust, or Static · Vite). The card shows the App port
(always 8080), the Health endpoint, and the Env vars that stack
expects.
In the Deploy target dropdown, select the container this workflow deploys to. The list is filtered to the registry's project. If the Deploy trust required badge shows, click Configure deploy trust. One click reuses the registry's repo / branch / environment, so you don't retype anything. The badge flips to Deploy trust configured.
Click Copy workflow to copy the generated .github/workflows/deploy.yml.
It already contains your real IMAGE, PUSH_AUDIENCE, CONTAINER_ID, and
DEPLOY_AUDIENCE. Paste it into your repo, adjust only the per-stack build
variables shown as inline comments (and DOCKERFILE if it isn't at the repo
root). If your repo has no Dockerfile, expand Supporting Dockerfile and copy
the production-ready one for your stack.
Commit and push to your trusted branch. Watch the rollout on the container detail page (timeline and logs). On the registry detail page, pushed tags appear in the Images card.
The workflow is generated, not hand-written
Once you pick a deploy target, CONTAINER_ID, DEPLOY_AUDIENCE, IMAGE, and
PUSH_AUDIENCE are all filled into the copied file. You do not look up IDs or
build the audience string by hand. The only edits are the stack-specific build
variables the file documents inline.
What's in the generated workflow
The copied .github/workflows/deploy.yml is complete. The values it pre-fills:
| Variable | Value the portal injects |
|---|---|
IMAGE | Your registry's image repository, e.g. registry.alawadi.cloud/<tenant>/<slug>. |
PUSH_AUDIENCE | The registry's OIDC audience, shown read-only in Push setup. |
CONTAINER_ID | The selected deploy target's ID. |
DEPLOY_AUDIENCE | alawadi-deploy:container:<CONTAINER_ID>, computed for you (also shown with a copy button). |
DOCKERFILE | Dockerfile by default. Change only if yours lives elsewhere. |
The workflow logs in with OIDC (docker login registry.alawadi.cloud -u oidc --password-stdin), sets BUILDX_NO_DEFAULT_ATTESTATIONS=1 (the registry rejects
the default build-attestation manifest), and requests permissions: id-token: write. Every stack listens on port 8080 as a non-root user, ready for the
platform's restricted security policy.
If your registry trust has an Environment set, the generated workflow also
adds environment: <name> to the job so the OIDC claim matches.
Advanced: configure deploy trust on the container page
Each container has its own Continuous deployment (GitHub Actions) card on the
container detail page (/dashboard/projects/<project-id>/containers/<container-id>).
It is the same trust the Configure deploy trust button sets, with a form you
can edit directly:
- GitHub repository (
owner/repo, required) - Branch ref (pre-filled
refs/heads/main, required, must start withrefs/) - Environment (optional GitHub environment claim)
Click Configure trust and the card displays the Deploy audience
(alawadi-deploy:container:<container-id>) plus a ready deploy snippet. The
Recent deployments list on the same card auto-updates as each push rolls out
(queued → rolling_out → succeeded). A deploy that does not reach succeeded
ends in one of failed, rolled_back, superseded, or stranded.
Polling deploy status from your own CI
The deploy request returns a status_url; poll it and key your loop on these
exact values. The in-flight status is rolling_out — running is the legacy
name older deploys emitted and new ones no longer use. Treat succeeded as
success, and failed, rolled_back, superseded, and stranded as terminal,
non-success states (superseded isn't an error — a newer push replaced this
deploy — but you should still stop polling). Anything else means keep polling.
The deploy snippet the portal generates already follows this contract, so
prefer copying it over hand-writing the loop.
Advanced: drive it from CI without the portal
If you automate trust setup itself (for example in your own provisioning scripts), you can call the API directly. This is the secondary path: the portal forms above are the primary, recommended flow.
Configure container deploy trust with your portal bearer token (the JWT from Google sign-in, not a GitHub OIDC token):
# PROJECT_ID and CONTAINER_ID come from the container detail page.
curl -sS -X PUT \
"https://api.alawadi.cloud/v1/projects/$PROJECT_ID/containers/$CONTAINER_ID/deploy/github-oidc-trust" \
-H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repository":"owner/repo","ref":"refs/heads/main","environment":"production"}'Inspect or remove it:
curl -sS \
"https://api.alawadi.cloud/v1/projects/$PROJECT_ID/containers/$CONTAINER_ID/deploy/github-oidc-trust" \
-H "Authorization: Bearer $PORTAL_ACCESS_TOKEN"
curl -sS -X DELETE \
"https://api.alawadi.cloud/v1/projects/$PROJECT_ID/containers/$CONTAINER_ID/deploy/github-oidc-trust" \
-H "Authorization: Bearer $PORTAL_ACCESS_TOKEN"The deploy itself (what the generated workflow runs on every push) is a two-call exchange. First swap the GitHub OIDC token (requested with the deploy audience) for a short-lived, single-use deploy token, then post the image:
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to container (OIDC -> short-lived deploy token)
env:
IMAGE: registry.alawadi.cloud/acme/web
CONTAINER_ID: 8f31c2a0-1b2c-4d5e-8f9a-0123456789ab
DEPLOY_AUDIENCE: alawadi-deploy:container:8f31c2a0-1b2c-4d5e-8f9a-0123456789ab
run: |
OIDC="$(curl -sSf \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$DEPLOY_AUDIENCE" | jq -r .value)"
DEPLOY_TOKEN="$(curl -sSf -X POST \
-H "Authorization: Bearer $OIDC" \
"https://api.alawadi.cloud/v1/containers/$CONTAINER_ID/deploy-token" | jq -r .token)"
curl -sSf -X POST \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"image\":\"$IMAGE:${{ github.sha }}\"}" \
"https://api.alawadi.cloud/v1/containers/$CONTAINER_ID/deploy"# 1. Exchange the GitHub OIDC token for a short-lived deploy token.
DEPLOY_TOKEN=$(curl -sSf -X POST \
-H "Authorization: Bearer $OIDC_TOKEN" \
"https://api.alawadi.cloud/v1/containers/$CONTAINER_ID/deploy-token" | jq -r .token)
# 2. Roll out the image (immutable tag, never :latest).
curl -sSf -X POST \
"https://api.alawadi.cloud/v1/containers/$CONTAINER_ID/deploy" \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"image":"registry.alawadi.cloud/acme/web:sha-8f31c2a"}'{
"image": "registry.alawadi.cloud/acme/web:sha-8f31c2a"
}No long-lived secrets
Deploy and registry tokens are short-lived and scoped. The deploy token is
single-use and tied to one container; you never store a permanent API key in
GitHub. Deployed images must use an immutable tag (such as sha-8f31c2a), not
:latest, and must come from a registry you own.
Related: Connect a container registry for push trust and image naming.
Security and isolation
How alawadi.cloud isolates every project and runs every app unprivileged, enforced at admission, so a root or privileged image cannot run.
Connect a container registry
Create a private registry on alawadi.cloud, push images from GitHub Actions over OIDC, and deploy them to a container.