---
name: ci-cd-on-bootload
description: Set up a CI/CD pipeline that builds a container image and rolls it onto a bootload service on every push (GitHub Actions, GitLab CI, or any runner). Use when the user wants automated build→push→deploy, continuous deployment, a release pipeline, or "deploy on merge" for an app hosted on bootload. Covers scoped CI tokens, image registries, zero-downtime rolls, health-gated verification, and rollback. For a first manual deploy, use the deploy-on-bootload skill first.
---

# CI/CD on bootload

bootload runs each app in its own Firecracker microVM. A pipeline is three moves:

1. **Once:** deploy the service (so CI only ever *rolls* it) and mint a scoped CI token.
2. **Every push:** build an image → push it to a registry → `bootload restart <svc> --image <ref>`.
3. **Guardrail:** the roll is health-gated; if the new image never goes healthy, `bootload rollback <svc>`.

`restart --image` is the whole trick: it creates a new deployment that rolls out one replica at a time. Old replicas keep serving until the new image passes its health check, so a bad build doesn't take the site down — it just doesn't take over.

Every command below is non-interactive once `BOOTLOAD_TOKEN` is set. Add `--plain` for stable, parseable output (it's automatic when stdout isn't a terminal, e.g. in CI).

> New to bootload? Do one manual deploy first with the **deploy-on-bootload** skill,
> then automate it with this one.

---

## 1. One-time setup (run locally, not in CI)

### a. Deploy the service once

CI *rolls* an existing service — it never creates it. Create it once so its name, route, and sizing are fixed:

```bash
bootload deploy --project <project> --name web \
  --image ghcr.io/acme/web:bootstrap --port 8080:http
# → assigns a public HTTPS URL (web-xxxxxx.fr1.apps.bootload.io), printed as a `route` line
```

`--port` must match what the image listens on (nginx → `80:http`, a typical Node app → `3000:http`). A wrong port surfaces later as "health check never passed".

### b. Mint a scoped CI token

Give CI its own key — never a personal login token. **Tested minimum scopes** for a build-and-roll pipeline:

```bash
bootload token create ci-web \
  --scope projects:read \
  --scope services:read --scope services:write \
  --scope deployments:read --scope deployments:write \
  --scope registry:push --scope registry:pull \
  --project <project-id> \
  --resource service:<service-id>      # optional: lock the key to this one service
# the secret (blt_...) is printed ONCE — copy it now
```

- `projects:read` is required even though you're only touching a service — the CLI resolves the project first. Omit it and every command 403s with `token lacks scope projects:read`.
- `--project` / `--resource` shrink the blast radius: a leaked key can only touch what you named.
- `registry:push`/`registry:pull` are only needed if you use **bootload's built-in registry** (Option A below). Drop them for an external registry.

Store the secret as a CI secret named `BOOTLOAD_TOKEN`. Revoke it any time with `bootload token revoke <id>` (`bootload token list` shows the id in the first column).

### c. Wallet must have balance

Deploys and rolls draw from the project's prepaid wallet. An empty wallet fails the roll with **HTTP 402 `wallet balance is zero`**. Keep a balance, or set auto-top-up in the portal, before wiring CI.

---

## 2. Pick a registry (both tested)

### Option A — bootload's built-in registry (self-contained)

No external accounts, no pull-credential management. Log in, push into your org namespace, roll:

```bash
bootload image login --org <org>                 # docker login registry.bootload.io with a scoped token
bootload image push web:$TAG --org <org>          # → registry.bootload.io/<org>/web:$TAG
bootload restart web --project <project> \
  --image registry.bootload.io/<org>/web:$TAG
```

`bootload image push` prints the exact `--image` reference to deploy. On production the host is `registry.bootload.io` (a test fleet uses `registry.test.bootload.io`).

### Option B — your own registry (ghcr, Docker Hub, ECR, …)

Build and push with your registry's normal tooling, then point bootload at the ref. For a **private** image, store pull credentials once so bootload can pull it:

```bash
bootload registry add ghcr --project <project> \
  --url ghcr.io --username <gh-user> --token <read:packages PAT>
# public images need no credentials
```

Then every roll is just:

```bash
bootload restart web --project <project> --image ghcr.io/acme/web:$TAG
```

---

## 3. GitHub Actions — full pipeline (Option B, ghcr)

This is the common shape: build+push to GHCR with the built-in `GITHUB_TOKEN`, then roll bootload. GitHub's `ubuntu-latest` runner uses a plaintext Docker config (no credential helper), so registry logins just work.

```yaml
# .github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]

concurrency:            # never let two rolls race
  group: deploy-web
  cancel-in-progress: false

jobs:
  ship:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write     # push to GHCR
    env:
      IMAGE: ghcr.io/${{ github.repository }}/web
      BOOTLOAD_TOKEN: ${{ secrets.BOOTLOAD_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - name: Build and push image
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          TAG=${{ github.sha }}
          docker build -t "$IMAGE:$TAG" -t "$IMAGE:latest" .
          docker push "$IMAGE:$TAG"
          docker push "$IMAGE:latest"
          echo "REF=$IMAGE:$TAG" >> "$GITHUB_ENV"

      - name: Install the bootload CLI
        run: curl -fsSL https://bootload.io/v1/cli/install.sh | sh

      - name: Roll the new image and verify
        env:
          PROJECT: acme-prod
          SERVICE: web
        run: |
          bootload restart "$SERVICE" --project "$PROJECT" --image "$REF"
          # Poll the newest deployment until it settles; roll back on failure.
          for i in $(seq 1 60); do
            line=$(bootload deployments "$SERVICE" --project "$PROJECT" --plain | head -1)
            case "$line" in
              *succeeded*) echo "✅ rolled out: $line"; exit 0 ;;
              *failed*|*error*) echo "❌ rollout failed: $line"
                                bootload rollback "$SERVICE" --project "$PROJECT"; exit 1 ;;
            esac
            echo "… $line"; sleep 5
          done
          echo "⏱ timed out waiting for health"; bootload rollback "$SERVICE" --project "$PROJECT"; exit 1
```

For a **private** repo image, run `bootload registry add ghcr …` once (§2, Option B) so bootload can pull it — the workflow itself doesn't change.

---

## 4. GitLab CI — full pipeline (Option A, bootload's registry)

Uses bootload's built-in registry, so there's no second registry to manage. Docker-in-Docker builds the image; `bootload image` pushes and rolls.

```yaml
# .gitlab-ci.yml
stages: [deploy]

deploy:
  stage: deploy
  image: docker:27
  services: [docker:27-dind]
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
    ORG: acme
    PROJECT: acme-prod
    SERVICE: web
    # BOOTLOAD_TOKEN is a masked, protected CI/CD variable
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  resource_group: deploy-web         # serialize rolls
  script:
    - apk add --no-cache curl
    - curl -fsSL https://bootload.io/v1/cli/install.sh | sh
    - TAG=$CI_COMMIT_SHORT_SHA
    - docker build -t web:$TAG .
    - bootload image login --org "$ORG"
    - bootload image push web:$TAG --org "$ORG"
    - REF=registry.bootload.io/$ORG/web:$TAG
    - bootload restart "$SERVICE" --project "$PROJECT" --image "$REF"
    - |
      for i in $(seq 1 60); do
        line=$(bootload deployments "$SERVICE" --project "$PROJECT" --plain | head -1)
        case "$line" in
          *succeeded*) echo "rolled out: $line"; exit 0 ;;
          *failed*|*error*) bootload rollback "$SERVICE" --project "$PROJECT"; exit 1 ;;
        esac
        sleep 5
      done
      bootload rollback "$SERVICE" --project "$PROJECT"; exit 1
```

---

## 5. Verifying a rollout (the important part)

`bootload restart --image` returns as soon as the deployment is *accepted*, not when it's live. A real pipeline must wait for the outcome:

- `bootload deployments <svc> --plain` lists deployments newest-first: `<date>  <id>  <status>  <image>`.
- The new roll is the top row. Poll it until `succeeded` (pass) or `failed`/`error` (fail), as in the examples above.
- Deployment states you'll see: `pending` → `rolling_out` → `succeeded`, or `failed`.
- On failure, `bootload rollback <svc>` re-deploys the previous succeeded image; `bootload rollback <svc> --to <deployment-id>` targets a specific one.

Skipping this check means a broken image silently never takes over while CI reports green.

---

## 6. Tested gotchas

- **`projects:read` scope is required** on the CI token, even for service-only work.
- **Always pass `--project` (and `--org` where a command takes it) in CI** — there's no interactive "current project" to fall back on; without it, ambiguous accounts fail `multiple projects — pass --project`.
- **Empty wallet → HTTP 402.** Keep a balance / auto-top-up before automating.
- **Docker credential helpers bite in CI.** `bootload image login` runs `docker login`; if the runner has a helper configured (`pass`, `osxkeychain`) that isn't initialized, login fails with `error storing credentials`. GitHub/GitLab default runners use a plaintext config and are fine; on a custom runner, point `DOCKER_CONFIG` at a dir whose `config.json` has no `credsStore`.
- **`--plain` / piped output** is the parseable format; there is no `--json`.
- **`bootload destroy <svc>` needs confirmation** — type or pipe the service name (`echo web | bootload destroy web`). Don't put destroy in a normal pipeline.
- **You can't delete an image a service still deploys** (`HTTP 409`) — roll the service to another tag first.
- **Serialize rolls** (`concurrency` / `resource_group`) so two pushes don't fight over the same service.

---

## 7. Reference

- First deploy / app shapes (volumes, domains, databases, secrets): the **deploy-on-bootload** skill.
- Full CLI reference: https://bootload.io/docs/cli/
- Machine-readable platform docs: https://bootload.io/llms-full.txt
- Rate card (know the cost before automating spend): `bootload pricing`
