alawadi.cloudDocs
Guides

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:

  1. Registry push. GitHub Actions logs in to registry.alawadi.cloud with docker login -u oidc (a token minted at run time), then builds and pushes your image tagged with the commit SHA.
  2. 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:

VariableValue the portal injects
IMAGEYour registry's image repository, e.g. registry.alawadi.cloud/<tenant>/<slug>.
PUSH_AUDIENCEThe registry's OIDC audience, shown read-only in Push setup.
CONTAINER_IDThe selected deploy target's ID.
DEPLOY_AUDIENCEalawadi-deploy:container:<CONTAINER_ID>, computed for you (also shown with a copy button).
DOCKERFILEDockerfile 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 with refs/)
  • 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 (queuedrolling_outsucceeded). 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_outrunning 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.

On this page