CI on Rackspace Spot: an Argo Workflows alternative to GitHub Actions for about $0.01/hr

kubernetesrackspace-spotargo-workflowsci-cd

Last verified:

On this page
  1. What you’ll need#
  2. Part 1 — Install Argo Workflows and Argo Events#
  3. Part 2 — The WorkflowTemplate is the CI job#
  4. Part 3 — Wire up the webhook with Argo Events#
  5. Part 4 — Preemption: retry on Spot, fall back to on-demand#
  6. Cost, roughly#
  7. Caveats worth knowing#
For terminals & agents: curl -s https://jedarden.com/guides/ci-on-rackspace-spot.md

GitHub Actions went down for about five hours on August 6, 2026 — degraded availability, delayed and failed workflow runs, errors on the Actions API. It wasn’t an outlier: GitHub logged 26 incidents in April, 23 in May, 23 in June, and six more in the first six days of August alone. Self-hosted runners don’t save you here, either — the orchestration layer that queues and dispatches runs is still GitHub’s, so a control-plane incident stalls self-hosted runners exactly like it stalls hosted ones.

None of that makes GitHub Actions bad software. It makes it a single point of failure you don’t control, for a job — running your tests and builds — that has no hard dependency on any one vendor. This guide is the pattern I actually run: Argo Workflows for the CI jobs themselves, Argo Events to receive the webhooks, on a Rackspace Spot Kubernetes cluster priced by open auction — cents per hour, not fractions of a cent per minute. The build that turns this article into the page you’re reading runs through the same kind of pipeline.

This isn’t a drop-in replacement. You lose the Marketplace Actions ecosystem and GitHub’s hosted UI; you gain a system nothing outside your own cluster can take down, at a small fraction of the per-minute cost. Read the caveats at the bottom before you commit to it.

What you’ll need#

  • A working Rackspace Spot cloudspace with a long-lived kubeconfig. If you don’t have one yet, the previous guide covers account setup, region/bid selection, and the two first-boot fixes every new cloudspace needs (Calico’s node-IP autodetection, and the Docker Hub anonymous-pull throttle) — both bite CI nodes just as hard as dev-environment nodes.
  • kubectl installed locally
  • A git host that can send webhooks on push (GitHub, GitLab, Forgejo/Gitea all work identically here — this guide uses GitHub’s payload shape, but Argo Events’ generic webhook source doesn’t care which one you point at it)
  • A container registry to push images to (Docker Hub, GHCR, your own — any registry your bid class can reach)

Part 1 — Install Argo Workflows and Argo Events#

Both are plain Kubernetes controllers — no dependency on a specific cloud, which is the point.

kubectl create namespace argo-workflows
kubectl apply -n argo-workflows -f \
  https://github.com/argoproj/argo-workflows/releases/latest/download/install.yaml

kubectl create namespace argo-events
kubectl apply -n argo-events -f \
  https://raw.githubusercontent.com/argoproj/argo-events/stable/manifests/install.yaml
kubectl apply -n argo-events -f \
  https://raw.githubusercontent.com/argoproj/argo-events/stable/manifests/native-event-bus-install.yaml

Confirm both sets of pods reach Running before continuing:

kubectl get pods -n argo-workflows
kubectl get pods -n argo-events

If a pod sits ImagePullBackOff on a fresh node, that’s the Docker Hub anonymous-pull throttle from the previous guide — it hits system images on every newly created Spot node, not just your own. Fix the registry secret before debugging anything else.

Part 2 — The WorkflowTemplate is the CI job#

A WorkflowTemplate is the reusable definition; the git-triggered Workflow that runs on every push just references it. The whole chain, end to end, is six pieces:

Flat illustration in the same style as the site logo: warm cream background, bold black outlines, limited red and orange palette. A horizontal flow diagram of six identical boxes connected by arrows in sequence, labeled in order PUSH, WEBHOOK, SENSOR, TEMPLATE, NODE, ARTIFACT. The fourth box, TEMPLATE, is filled solid crimson red as the single accent step in an otherwise outline-only sequence.

Rootless image builds inside Kubernetes normally mean Kaniko or Buildkit rather than Docker-in-Docker — Kaniko needs no privileged pod, which matters on a shared cluster:

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: container-build
  namespace: argo-workflows
spec:
  entrypoint: build
  arguments:
    parameters:
      - name: repo-url
      - name: revision
      - name: image
  templates:
    - name: build
      inputs:
        parameters:
          - name: repo-url
          - name: revision
          - name: image
      initContainers:
        - name: clone
          image: alpine/git:2.45.2
          command: [sh, -c]
          args:
            - "git clone {{inputs.parameters.repo-url}} /work && \
               git -C /work checkout {{inputs.parameters.revision}}"
          volumeMounts:
            - name: workdir
              mountPath: /work
      container:
        image: gcr.io/kaniko-project/executor:latest
        args:
          - --context=/work
          - --destination={{inputs.parameters.image}}:{{inputs.parameters.revision}}
        volumeMounts:
          - name: workdir
            mountPath: /work
          - name: docker-config
            mountPath: /kaniko/.docker
      volumes:
        - name: workdir
          emptyDir: {}
        - name: docker-config
          secret:
            secretName: registry-credentials

Two things that aren’t obvious until they cost you an afternoon:

The controller’s default podGC is OnPodCompletion — the pod disappears the instant the job finishes, success or failure. If you’re debugging a failing build, stream its logs while it’s still running (kubectl logs -f <pod> -n argo-workflows) or submit a one-off debug Workflow with podGC: {strategy: OnWorkflowCompletion} to keep evidence around.

A generic multi-repo template beats one template per repo. Once you have more than two or three repos building the same way, parameterize repo-url/image (as above) and pass different values per Workflow rather than copy-pasting the template. Detecting which subdirectory changed in a monorepo is a git diff --name-only in an init container, not a special Argo feature.

Part 3 — Wire up the webhook with Argo Events#

An EventSource exposes an HTTP endpoint and validates the webhook; a Sensor watches for events on it and submits the Workflow.

apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: github
  namespace: argo-events
spec:
  service:
    ports:
      - port: 12000
        targetPort: 12000
  webhook:
    push:
      port: "12000"
      endpoint: /push
      method: POST
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: github-push
  namespace: argo-events
spec:
  dependencies:
    - name: push-dep
      eventSourceName: github
      eventName: push
  triggers:
    - template:
        name: submit-build
        argoWorkflow:
          operation: submit
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: container-build-
                namespace: argo-workflows
              spec:
                workflowTemplateRef:
                  name: container-build
                arguments:
                  parameters:
                    - name: repo-url
                      value: "https://github.com/your-org/your-repo"
                    - name: revision
                      value: "main"
                    - name: image
                      value: "your-registry/your-repo"

Expose the EventSource’s Service through whatever ingress you already run — don’t hand it a public IP with nothing in front of it. At minimum, put your git host’s webhook secret in Argo Events’ HMAC validation (spec.webhook.push.filter / the source’s secureChannel config in newer versions) so the endpoint rejects anything that isn’t actually signed by your git host.

Part 4 — Preemption: retry on Spot, fall back to on-demand#

This is spot capacity: when the auction price rises above your bid, the node — and any pod on it — is gone with no warning. For a CI job, that’s a build failure that had nothing to do with your code.

Flat illustration in the same style as the site logo: warm cream background, bold black outlines, limited red and orange palette. Two square panels divided by a vertical black line. The left panel shows a server node icon with a bold crimson red X stamped over it beneath a jagged rising price line, captioned PRICE RISES. The right panel shows an identical node icon, calm and outlined in black with a small checkmark, with a curved arrow looping in from the left panel, captioned REQUEUED.

Bid at the p80 percentile, not the current market price. The live pricing table shows p20/p50/p80 for every server class: a standing bid at p80 would have kept a node through roughly 80% of its recent sampling window. Bidding exactly today’s market price wins the node right now and gets preempted at the next uptick.

As of July 2026, new bids floor at $0.01/hr, rounded to fixed steps — $0.01 increments up to $0.04/hr, $0.02 up to $0.10/hr, $0.03 up to $0.20/hr, $0.05 above that (Rackspace’s changelog has the exact table). That’s a floor on what you can newly bid, not on what you pay: a bid placed before the change that sits below $0.01/hr keeps running unchanged, so the market-clearing price on any given class can still land below a cent — you just can’t be the one placing a new bid down there anymore. If your target percentile comes back under $0.01/hr, round up to $0.01/hr or the nearest step above it.

Let Argo retry on spot first — it’s usually enough:

spec:
  retryStrategy:
    limit: "2"
    retryPolicy: OnError
  nodeSelector:
    pool: spot
  tolerations:
    - key: pool
      operator: Equal
      value: spot
      effect: NoSchedule

For anything that can’t tolerate a retry-and-wait — a release build with a deadline — add a second on-demand node pool and a second WorkflowTemplate (or a priorityClassName) that targets it instead of spot. Rackspace Spot’s on-demand tier prices well above the auction floor, which is exactly why it works as a fallback: it’s expensive enough that almost nothing runs on it, so it’s rarely the thing being preempted, and cheap enough relative to what a missed release costs that eating the difference on the rare critical build is a non-decision. Point your release pipeline’s Workflow at the on-demand pool by nodeSelector the same way the spot pool is targeted above.

Cost, roughly#

GitHub-hosted Linux 2-core runners bill overage minutes at $0.006/min (~$0.36/hr) as of GitHub’s January 2026 pricing update. Rackspace Spot’s comparable class — a 2 vCPU / 3.75 GB general-purpose node — has a $0.01/hr minimum new bid as of Rackspace’s July 2026 pricing change (see the caveat below), still roughly 36× cheaper than the GitHub rate. The pricing table will show you today’s actual market-clearing price for your region, which can sit below that minimum on capacity held by bids placed before the change.

Flat illustration in the same style as the site logo: warm cream background, bold black outlines, limited red and orange palette. A large black balance scale. The left pan hangs low under one heavy solid black block, captioned MINUTES BILLED. The right pan floats high holding a single tiny crimson red feather, captioned DOLLAR PER HOUR. A worker figure with short black hair, crimson red polo, warm orange skin, and closed serene eyes stands beside the scale gesturing at it.

That’s not an apples-to-apples comparison and I won’t pretend it is: GitHub’s minutes buy you zero ops, a UI, log retention, and the Marketplace Actions ecosystem. What you’re paying for on the Spot side is the infrastructure this guide just walked through, and you maintain it. The comparison is only fair once you count your own time — but if you’re already running a Spot cluster for other reasons (as in the DevPod guide), the marginal cost of routing CI through the same cluster is close to free.

Caveats worth knowing#

  • This is not a GitHub Actions clone. There’s no Marketplace, no reusable Actions ecosystem, no hosted log UI out of the box (Argo Workflows ships its own UI, which is not the same thing). You’re writing containers, not YAML uses: steps.
  • Rackspace doesn’t publish how deep the spot pool is at any price. The auction price is your only signal — there’s no API or console view that shows “N nodes available.” Bid at a percentile you’re comfortable with and let preemption handling (Part 4) absorb the rest, rather than trying to reason about capacity you can’t observe.
  • The $0.01/hr minimum bid is new capacity only. Rackspace changed this in July 2026 (see the changelog); existing bids below $0.01/hr keep running untouched, so don’t be surprised if the pricing table shows a market-clearing price under a cent even though you can’t place a new bid there. As of this writing, Rackspace’s own /pricing marketing page still advertises “bid from $0.001/hr” and hasn’t caught up to its own changelog — trust the changelog, not the marketing copy.
  • The 429 Docker Hub trap recurs on every fresh CI node, exactly as it does for dev environments — a node that Kaniko or your init container can’t pull system images onto is almost always this, not a cluster problem.
  • Verify webhook signatures. An EventSource endpoint with no secret validation will happily submit Workflows for anyone who finds the URL.
  • Prices move. Everything above reflects Rackspace’s published feed and GitHub’s published pricing at the time this was written — confirm both before committing a budget to either.