---
title: "Run Buildkite agents on exe.dev, a fresh VM per job"
description: "Self-hosted Buildkite agents on exe.dev: each job gets its own sandbox, copied from a warm base VM and deleted when the job ends."
url: https://exe-staging.dev/deploy/buildkite
category: "Developer tools"
updated: 2026-11-05
---

# Run Buildkite agents on exe.dev, a fresh VM per job

Every Buildkite job on a fresh copy of a warm VM, billed by the second.

- **Software:** Buildkite (https://buildkite.com)
- **Source:** https://github.com/buildkite/agent
- **License:** MIT (the agent)
- **Time:** 20 minutes
- **Machine-readable:** https://exe-staging.dev/deploy/buildkite/manifest.json
- **File:** https://exe-staging.dev/deploy/buildkite/base-setup.sh
- **File:** https://exe-staging.dev/deploy/buildkite/bk-dispatcher.py

## What you get

- A base VM with buildkite-agent, ready to be copied
- The bk-dispatcher service, which gives each job in the exe and exe-large queues a fresh sandbox and deletes it afterwards
- The Buildkite agent token and the exe.dev API key held by integrations, on no VM
- A pipeline whose steps run on `queue: exe`, with `started` and `deleted` lines in `journalctl -u bk-dispatcher`

Running on the VM:

- **Dispatcher**: bk-dispatcher.py (Python 3, standard library), a custom stack on Buildkite's Stacks API, the bk-dispatcher systemd service on my-buildkite, data in `~/bk/env`
- **Base VM**: my-buildkite-base, a regular VM with buildkite-agent, your toolchains, and warm caches, data in `~/.buildkite-agent, ~/.cache, ~/go`
- **Job sandboxes**: one cp --sandbox copy of the base per job, sized by queue (2 or 4 vCPU), deleted when the job ends

How it is reached:

- ssh my-buildkite-base.exe.xyz (private): the base VM, to install toolchains and warm caches
- ssh my-buildkite.exe.xyz (private): the dispatcher; it only makes outbound requests, so nothing is public

Buildkite runs the pipeline; your agents run the jobs. This page puts
those agents on exe.dev without keeping a fleet of them warm. You prepare
a **base VM** once, with `buildkite-agent`, your toolchains, and warm
caches. For every job, a small **dispatcher** copies the base into a new
[sandbox](/deploy/sandbox), starts an agent on it that may run that one
job and nothing else, and deletes the VM when the job is done. Copying
the base and starting the agent took about 3.5 seconds in our tests.

The dispatcher is a custom stack on Buildkite's
[Stacks API](https://buildkite.com/docs/apis/agent-api/stacks), the API
Buildkite's own [Kubernetes stack](https://buildkite.com/docs/agent/self-hosted/agent-stack-k8s)
is built on. Each job VM gets a
[job acquisition token](https://buildkite.com/docs/agent/self-hosted/job-acquisition-tokens)
for its job, never your agent token. What changes in a pipeline is the
queue, `agents: { queue: exe }`. What you get is a whole VM per job (its
own kernel, `sudo`, Docker, real vCPUs) that starts with everything
installed and is billed by the second.

The same approach for GitHub Actions is on
[GitHub Actions on exe.dev](/deploy/github-actions).

## How it works

- **my-buildkite-base** is an ordinary VM. It never runs jobs; it's what
  each job's VM is copied from.
- **my-buildkite** runs [bk-dispatcher.py](https://exe-staging.dev/deploy/buildkite/bk-dispatcher.py),
  a short Python program. Every 2 seconds it asks Buildkite which jobs
  are waiting in its queues, and for each one:
  1. reserves the job, so nothing else takes it;
  2. `cp my-buildkite-base my-buildkite-job-<random> --sandbox`, sized
     for the job's queue;
  3. asks Buildkite for a job acquisition token, which can only register
     an agent for that job and expires in 10 minutes;
  4. starts `buildkite-agent` on the copy with that token and
     `BUILDKITE_AGENT_ACQUIRE_JOB`, so it runs the job and exits.

  When the job is finished, cancelled, or failed, the dispatcher stops
  the agent and deletes the VM. If a VM can't be made, it fails the job
  with the reason, rather than leaving it waiting.
- Nothing calls the dispatcher. It only makes outbound requests, so
  `my-buildkite` stays private: no webhook, no public port.
- The dispatcher holds no Buildkite or exe.dev key. It reaches both
  through two [integrations](/docs/integrations) attached to it alone,
  so neither is on any VM, the job VMs included.

## Before you start

- A [Buildkite](https://buildkite.com/signup) organization. The Free plan
  is enough. You need to be an administrator of the organization, or a
  maintainer of its cluster.
- An exe.dev account that can create sandboxes. Personal accounts can. On
  a Work team, an admin turns them on with
  `ssh exe.dev team settings standalone all-users`.
- About 20 minutes.

## Prepare the base VM

<details class="dp-details" id="create-the-vm" data-manual-setup>
<summary>Create the base and install the agent</summary>

On your computer, create the base. The `buildkite` tag matters: the
dispatcher's exe.dev key will only be able to touch VMs with it.

```bash
ssh exe.dev new --name=my-buildkite-base --tag=buildkite
```

VM names are global, so pick any free name, and use it for `BASE_VM`
later. Install the agent with
[base-setup.sh](https://exe-staging.dev/deploy/buildkite/base-setup.sh). It adds
Buildkite's signed apt repository, installs the current `buildkite-agent`
(v4), and turns off the package's always-on service. In its place it
installs `buildkite-job.service`, which runs one agent, as `exedev`, when
the dispatcher starts it on a copy. It also enables Docker at boot. It
holds no token.

```bash
ssh my-buildkite-base.exe.xyz 'curl -fsSL https://exe-staging.dev/deploy/buildkite/base-setup.sh | bash'
```

It ends with `buildkite-agent version 4.… installed`.

</details>

The base already has what the default exe.dev image ships: Docker, Go,
Python with `uv`, git, and build tools. Install whatever else your
pipelines need, the way you would on any Ubuntu 24.04 machine:

```bash
ssh my-buildkite-base.exe.xyz 'sudo apt-get install -y postgresql-client libvips-dev'
```

Then warm the caches by building your project there once. Jobs run as
the same `exedev` user, so they find `~/go/pkg/mod`, `~/.cache/go-build`,
`~/.npm`, `~/.cache/pip`, or `~/.cargo` already full. From a checkout on
your computer:

```bash
rsync -a --exclude=.git ./ my-buildkite-base.exe.xyz:warm/
ssh my-buildkite-base.exe.xyz 'cd warm && go build ./... && go test ./... ; cd && rm -rf warm && sync'
```

(`npm ci && npm test`, `uv sync && uv run pytest`, `cargo test`: whatever
your steps run.) Docker images work the same way: `docker pull` them on
the base, and every job has them. See [Caches](#caches) for git.

Don't put secrets on the base. Everything on it is on every job's VM.

## Create the queues and the agent token

In Buildkite, open **Agents** and your cluster (a new organization has
one, **Default cluster**).

1. Under **Queues**, select **New Queue**, choose **Self hosted**, and
   give it the key `exe`. Make a second one, `exe-large`, for bigger
   jobs. Each queue is one size of job VM; you set the sizes in
   [Start the dispatcher](#start-the-dispatcher).
2. Under **Agent Tokens**, select **New Token**, describe it as
   `exe.dev dispatcher`, and copy the token (it starts with `bkct_`).

The dispatcher uses that token to find, reserve, and start jobs in the
cluster. It never leaves exe.dev's integration: the job VMs get a
one-job token instead.

## Create the dispatcher VM and its keys

On your computer:

```bash
ssh exe.dev new --name=my-buildkite
```

**Buildkite.** Put the agent token in an
[HTTP proxy integration](/docs/integrations-http-proxy) that only
`my-buildkite` can use. This reads it without echoing it or saving it in
your shell history:

```bash
printf 'Agent token: '; read -rs T; echo
ssh exe.dev "integrations add http-proxy --name bk-agent --target https://agent.buildkite.com --header 'Authorization:Token $T' --attach vm:my-buildkite"
unset T
```

The quotes matter: Buildkite wants `Authorization: Token …`, and the
space in it would otherwise split the header in two.

**exe.dev.** Make an API key that can copy, list, reach, comment on, and
delete VMs tagged `buildkite`, and nothing else, and hand it to a second
integration:

```bash
ssh exe.dev ssh-key generate-api-key --label=buildkite-dispatcher --tag=buildkite \
    --cmds=cp,ls,rm,ssh,comment --exp=1y --json | jq -r .token \
  | ssh exe.dev integrations add http-proxy --name bk-exe \
    --target https://exe.dev --bearer - --attach vm:my-buildkite
```

The copies the key makes are tagged `buildkite` too, so it can delete
them. `ssh exe.dev ssh-key remove buildkite-dispatcher` revokes it.

Check both from the dispatcher VM:

```bash
ssh my-buildkite.exe.xyz
curl -s -X POST https://bk-exe.int.exe.xyz/exec -d ls | jq -r '.vms[].vm_name'
curl -s -X POST https://bk-agent.int.exe.xyz/v3/stacks/register -H 'Content-Type: application/json' -d '{}'
```

The first prints `my-buildkite-base`, and nothing that isn't tagged
`buildkite`. The second prints `{"message":"Cluster queue not found for
key: "}`: Buildkite accepted the token and refused the empty request.
`401` means the token is wrong; an `invalid token` error from exe.dev
means the second integration got something other than the key.

> [!WARNING]
> Attach these two integrations to `my-buildkite` only
> (`--attach vm:my-buildkite`), never by the `buildkite` tag: every job
> VM carries that tag, and any job could then create VMs or take other
> jobs.

## Start the dispatcher

On the VM (`ssh my-buildkite.exe.xyz`), fetch the dispatcher and write
its settings:

```bash
mkdir -p ~/bk
curl -fsSL -o ~/bk/bk-dispatcher.py https://exe-staging.dev/deploy/buildkite/bk-dispatcher.py
(umask 077; cat > ~/bk/env <<'EOF'
AGENT_API=https://bk-agent.int.exe.xyz
EXE_API=https://bk-exe.int.exe.xyz
BASE_VM=my-buildkite-base
QUEUES="exe=2 exe-large=4"
MAX_AGENTS=5
EOF
)
```

- `QUEUES`: the queue keys this dispatcher serves, each with the vCPUs of
  its job VMs. Every vCPU comes with 2 GiB of memory, the most a sandbox
  has, so `exe` jobs get 2 vCPU / 4 GiB and `exe-large` jobs 4 vCPU /
  8 GiB.
- `MAX_AGENTS`: at most this many job VMs at once, all queues together;
  further jobs wait in their queue. Keep it at 10 or under on
  Buildkite's Free plan, and under your exe.dev plan's VM limit (50 on
  [Personal](/docs/billing/personal)) minus the VMs you already have.
- Optional: `POLL_SECONDS` (2) between checks, `START_SECONDS` (300) for
  an agent to pick up its job before the dispatcher fails it,
  `MAX_HOURS` (6) before any job VM is deleted, and `STACK_KEY` (the
  VM's name), which identifies the dispatcher to Buildkite.

Run it as a service, so it comes back after a crash or a reboot:

```bash
sudo tee /etc/systemd/system/bk-dispatcher.service >/dev/null <<'EOF'
[Unit]
Description=Buildkite jobs on exe.dev sandboxes
After=network-online.target
Wants=network-online.target

[Service]
User=exedev
EnvironmentFile=/home/exedev/bk/env
ExecStart=/usr/bin/python3 /home/exedev/bk/bk-dispatcher.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now bk-dispatcher
journalctl -u bk-dispatcher -n 5 --no-pager -o cat
```

The log says `stack my-buildkite: queues exe (2 vCPU) exe-large (4 vCPU),
copies of my-buildkite-base, up to 5 at once`, with no `register:` or
`reconcile:` errors after it. In Buildkite, the `exe` queue shows as
**Connected** within a few seconds.

## Run a pipeline on it

In a pipeline's steps, send the jobs you want on exe.dev to a queue:

```yaml
steps:
  - label: ":go: test"
    command: go test ./...
    agents:
      queue: exe

  - label: ":package: release build"
    command: make release
    agents:
      queue: exe-large
```

Start a build, and watch the dispatcher on the VM:

```bash
journalctl -u bk-dispatcher -f -o cat
```

```
22:59:54 started my-buildkite-job-16952394 for exe job 01a1adf0-9d47-430b-bdf6-6f6cc3451c15 in 3.4s
22:59:54 started my-buildkite-job-56a17dd1 for exe-large job 01a1adf0-9d4c-46ed-97fe-492fbae01cee in 3.3s
23:00:02 deleted my-buildkite-job-56a17dd1 (job finished)
23:00:10 deleted my-buildkite-job-16952394 (job finished)
```

In Buildkite, each job names `my-buildkite-job-…` as its agent. A step
with `parallelism: 8` gets eight VMs at once, or `MAX_AGENTS` of them
with the rest started as the first ones finish. Cancel a build and its
VMs are deleted within seconds.

Moving one step at a time is fine: steps on other queues (Buildkite's
hosted agents, your other agents) keep running where they did.

## Caches

The base holds whatever was on its disk when a job's VM was copied:
toolchains, dependency caches, build caches, Docker images. Nothing a job
writes comes back to it. It costs no time per job, but it's only as
fresh as your last warm-up, so re-run the warm-up when dependencies
change a lot, or weekly.

In our test, building a Go project of about 150 modules took 44 seconds
on a fresh base. After one `go build` on the base, it took 4.7 seconds,
and the whole job, checkout included, went from 48 seconds to 12.

**Git.** base-setup.sh turns on the agent's
[git mirrors](https://buildkite.com/docs/agent/self-hosted/configure#git-mirrors-path),
so a mirror on the base makes every checkout a short fetch. Name it after
the repository URL exactly as your pipeline has it, with every character
that isn't a letter or digit replaced by `-`. On your computer, for a
public repository:

```bash
repo=https://github.com/you/app.git
ssh my-buildkite-base.exe.xyz "git clone --mirror $repo ~/.buildkite-agent/git-mirrors/$(printf %s "$repo" | sed 's/[^[:alnum:]]/-/g') && sync"
```

For a private one, clone it with a credential that isn't saved in the
mirror, such as `git -c http.extraHeader="Authorization: Basic …" clone
--mirror …`, and let jobs fetch with their own (see
[Checkout and secrets](#checkout-and-secrets)).

To update the base, change it (`ssh my-buildkite-base.exe.xyz`) and end
with `sync`: new job VMs are copies of its disk, and anything not yet
written to disk isn't in them. Jobs already running keep their copy.

## Checkout and secrets

Job VMs hold no credentials, so give jobs theirs from Buildkite:

- **Private GitHub repositories:** on each queue's **Settings** tab,
  turn on
  [Use GitHub App token for checkout](https://buildkite.com/docs/agent/self-hosted/code-access#using-a-buildkite-issued-github-app-token).
  Each job then clones with a short-lived, read-only token. It needs
  Buildkite's full-access GitHub App on your GitHub account.
- **Other secrets,** and SSH keys for other Git hosts:
  [Buildkite secrets](https://buildkite.com/docs/pipelines/security/secrets/buildkite-secrets),
  which the agent fetches for the job that asks.

exe.dev can keep some keys off job VMs altogether. An integration
attached by the `buildkite` tag is usable from every job VM, without a
key on it: an [HTTP proxy](/docs/integrations-http-proxy) to a private
package registry or API, or the [LLM gateway](/docs/integrations-llm)
for tests that call a model. A step calls `https://<name>.int.exe.xyz/`
and exe.dev adds the credential. Remember that any job can use it.

## Just one VM

For a small project whose builds all come from you, one VM with
always-on agents may be all you need: no base, no dispatcher, no
integrations, and nothing to pay beyond your plan. Its agents run job
after job on the same VM, so each job sees what earlier ones left
behind, and any job can read the agent token in the agent's
configuration. Keep it to your own code.

<details class="dp-details" id="one-vm-setup">
<summary>Install agents on one VM and keep them running</summary>

Make the `exe` queue and an agent token as in
[Create the queues and the agent token](#create-the-queues-and-the-agent-token).
On your computer, create a VM (pick any free name) and log in:

```bash
ssh exe.dev new --name=my-buildkite-agent
ssh my-buildkite-agent.exe.xyz
```

On the VM, install the agent from Buildkite's apt repository, give it the
token, and start two agents (`spawn=2`) that take jobs from the `exe`
queue:

```bash
curl -fsSL https://keys.openpgp.org/vks/v1/by-fingerprint/32A37959C2FA5C3C99EFBC32A79206696452D198 \
  | sudo gpg --dearmor -o /usr/share/keyrings/buildkite-agent-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/buildkite-agent-archive-keyring.gpg] https://apt.buildkite.com/buildkite-agent stable main" \
  | sudo tee /etc/apt/sources.list.d/buildkite-agent.list
sudo apt-get update && sudo apt-get install -y buildkite-agent
printf 'Agent token: '; read -rs T; echo
sudo sed -i "s/^token=.*/token=\"$T\"/" /etc/buildkite-agent/buildkite-agent.cfg; unset T
printf 'tags="queue=exe"\nspawn=2\n' | sudo tee -a /etc/buildkite-agent/buildkite-agent.cfg
sudo usermod -aG docker buildkite-agent
sudo systemctl enable --now docker buildkite-agent
sudo journalctl -u buildkite-agent -n 5 --no-pager
```

The log ends with `Waiting for instructions...` for each agent, and both
show as connected on the `exe` queue in Buildkite. Jobs run as the
`buildkite-agent` user, with its caches in `/var/lib/buildkite-agent`.
Size the VM for `spawn` jobs at once (`ssh exe.dev resize`), and don't
run this and the dispatcher on the same queue.

</details>

If the VM restarts without shutting down cleanly, Buildkite can give a
job to an agent that no longer exists. The job waits until Buildkite
marks that agent lost, about four minutes, and then runs again.

## What it costs

Buildkite charges for its plan the same way whoever runs the agents:
Free for up to 5 users, Pro $30 per active user a month. On self-hosted
agents, Free runs up to 10 jobs at once; Pro includes 10 connected
agents, then $3.50 a month for each. Every job VM is a connected agent
while it runs, so `MAX_AGENTS` is also your agent count.

exe.dev bills the [sandboxes](/deploy/sandbox) by the second, from the
copy to the delete: $0.105 an hour per 2 vCPU / 4 GiB, so $0.00175 a
minute for 2 vCPU, $0.0035 for 4, $0.007 for 8. Each job VM lives about
10 to 15 seconds longer than its job. `my-buildkite` and
`my-buildkite-base` are ordinary VMs that share your plan's capacity with
your other VMs and cost nothing extra (on Personal, plans start at $15 a
month).

Buildkite's [hosted agents](https://buildkite.com/pricing/) are $0.004
per vCPU-minute on Linux, also by the second: $0.008 a minute for Small
(2 vCPU, 4 GB), $0.016 for Medium (4 vCPU, 16 GB), $0.032 for Large
(8 vCPU, 32 GB). Free includes 2,000 vCPU-minutes a month, Pro 4,000.

Some months, on 30 days:

- **A small team.** 100 jobs a day, 5 minutes each, on 2 vCPU: 15,000
  minutes. Buildkite Small: $120, or $112 after Free's 2,000 vCPU-minutes.
  exe.dev: 3,000 VMs × 5¼ minutes ≈ 263 hours × $0.105 ≈ $27.60. If the
  warm base brings the jobs to 3 minutes, ≈ $17.10.
- **Big builds.** 2,000 builds a month, 12 minutes each, on 8 vCPU.
  Buildkite Large: 24,000 minutes × $0.032 = $768. exe.dev, 8 vCPU /
  16 GiB (4 units, $0.42 an hour): ≈ 408 hours ≈ $171.50.

Mind the memory: Buildkite's Medium and Large have 4 GB per vCPU, and a
sandbox at most 2 GiB, so a job that needs 16 GB takes 8 vCPU here.
Buildkite's hosted agents stay the better deal for builds that fit in
the included minutes, and for macOS, Windows, and arm64, which exe.dev
doesn't run.

## Update

- **The agent.** Buildkite releases one every week or two. Re-run the
  install on the base: it upgrades the package and keeps your
  configuration.
  `ssh my-buildkite-base.exe.xyz 'curl -fsSL https://exe-staging.dev/deploy/buildkite/base-setup.sh | bash'`.
- **The dispatcher.** Fetch `bk-dispatcher.py` again as in
  [Start the dispatcher](#start-the-dispatcher), then
  `sudo systemctl restart bk-dispatcher`. Running jobs are unaffected.
- **The keys.** The exe.dev key above expires in a year; Buildkite agent
  tokens made in its interface don't expire. To replace either, make a
  new one, `ssh exe.dev integrations remove bk-agent` (or `bk-exe`), and
  add it again as above.

## Troubleshooting

Start with the dispatcher's log on `my-buildkite`:
`journalctl -u bk-dispatcher -n 50 --no-pager -o cat`.

- **A job waits and the log shows nothing.** Its queue isn't in
  `QUEUES`, or `MAX_AGENTS` job VMs are already running, or the queue is
  paused in Buildkite (the dispatcher respects that).
- **`register:` or `reconcile:` with `HTTP Error 401`.** The agent token
  in `bk-agent` is wrong or revoked. `404 Cluster queue not found` means
  the first key in `QUEUES` isn't a queue in the token's cluster.
- **`launch … failed: HTTP Error 422`** and the job fails with
  `exe.dev: could not start …`. exe.dev refused the copy, and the log
  says why: `vm "…" not found` is a `BASE_VM` that doesn't exist or isn't
  tagged `buildkite`; otherwise sandboxes aren't enabled for your team,
  or the VM limit is reached.
- **A job fails with `the agent on … did not pick up the job`.** The
  agent on the copy didn't start. Install the base again, then look at a
  job VM while it's up: `ssh my-buildkite-job-… journalctl -u buildkite-job`.
- **A job can't find a tool or a cached package.** It wasn't on the base's
  disk when the job's VM was copied. Install it, `sync`, and re-run.
- **Leftover job VMs.** The dispatcher deletes them while it runs. To see
  them yourself:
  `ssh exe.dev ls --json | jq -r '.vms[].vm_name | select(startswith("my-buildkite-job-"))'`.

## Frequently asked questions

### How much does it cost compared with Buildkite's hosted agents?

A 2 vCPU / 4 GiB sandbox is $0.105 an hour, billed by the second: $0.00175 a minute, against $0.008 for Buildkite's Small Linux hosted agent of the same size. 8 vCPU is $0.007 a minute here and $0.032 (with 32 GB) there. Buildkite's plan fee is the same either way. See [What it costs](#what-it-costs).

### Does this work on Buildkite's free plan?

Yes. The Stacks API and job acquisition tokens are available on every plan, and we tested this page on a Free organization. Free allows up to 10 jobs at once on self-hosted agents, so keep `MAX_AGENTS` at 10 or under. Pro includes 10 connected agents and charges $3.50 a month for each one beyond that.

### Is it faster?

Usually, because the base already has your toolchain and caches. In our test a Go project's `go build ./...` took 44 seconds on a fresh base and 4.7 seconds on a warm one, and the whole job went from 48 seconds to 12. A job starts 5 to 7 seconds after it's scheduled: the dispatcher checks every 2 seconds, and copying the base and starting the agent take about 3.5.

### Why not the Elastic CI Stack or the Kubernetes agent stack?

They run agents on AWS or on a Kubernetes cluster. This page is the same idea on exe.dev, with a whole VM per job instead of an EC2 instance or a pod: a short dispatcher on Buildkite's Stacks API, the API Buildkite built its Kubernetes stack on.

### Do plugins, artifacts, meta-data, and secrets still work?

Yes. Each job VM runs the ordinary `buildkite-agent`, so plugins, `buildkite-agent artifact upload`, `meta-data`, annotations, and [Buildkite secrets](https://buildkite.com/docs/pipelines/security/secrets/buildkite-secrets) work as on any self-hosted agent. Only the agent's tags are different: the dispatcher picks the job, so `agents:` other than `queue` don't select anything.

### What happens when the dispatcher is down?

Jobs wait in their queue, and start when it's back: it finds its running job VMs again by their exe.dev comments, and reserved jobs that it didn't start return to the queue. Buildkite shows the queue as disconnected after about five minutes without the dispatcher.

### Do I need all this for a small project?

No. One always-on agent on one VM is a five-minute job; see [Just one VM](#just-one-vm). Its jobs share the VM and see what earlier jobs left behind, which is fine for your own code.


## Related

- [Run GitHub Actions jobs on exe.dev, a fresh VM per job](https://exe-staging.dev/deploy/github-actions.md): Every job on a fresh copy of a warm VM, billed by the second.
- [Sandboxes on exe.dev](https://exe-staging.dev/deploy/sandbox.md): A VM with its own CPU and memory, billed by the second while it runs.
- [Use exe.dev as a remote Docker host](https://exe-staging.dev/deploy/docker-host.md): Keep the docker CLI. Send the builds, the containers, and the fan noise to a VM.
- [Dev, Test, Prod: Choose One, Two, or Three](https://exe-staging.dev/deploy/dev-test-prod.md): You can build, test, and deploy on exe.dev.
- [Forgejo](https://exe-staging.dev/deploy/forgejo.md): Self-hosted Git forge with exe.dev SSO
- [Gitea](https://exe-staging.dev/deploy/gitea.md): Self-hosted Git forge with exe.dev SSO
