---
slug: agents/dockerfile
title: Custom Dockerfile runtime
kind: reference
surface: agents
summary: How runtime dockerfile works on Aetherfy — you supply the whole container, what the archive must contain, the health endpoint you now own, what validation blocks versus warns, and the plan tiers that allow it.
sources:
  - aetherfy-control-plane:orchestrator/fly_builder.py
  - aetherfy-control-plane:orchestrator/image_generator.py
  - aetherfy-control-plane:orchestrator/fly_manager.py
  - aetherfy-control-plane:shared/plan_validator.py
  - aetherfy-control-plane:shared/config_parser.py
---

# Custom Dockerfile runtime

Every other Aetherfy runtime — `python3.11`, `node22`, `bun` and the rest — has
Aetherfy generate the container for you. `runtime: dockerfile` is the opposite
arrangement: you supply the entire `Dockerfile` and Aetherfy builds it as-is.

The trade is total control for total responsibility. Use it when your program
needs a toolchain Aetherfy does not ship — Go, Rust, Java, a system library, a
compiled binary.

## What changes when an Aetherfy agent uses the dockerfile runtime

| | Standard runtime | `runtime: dockerfile` |
|---|---|---|
| Who writes the Dockerfile | Aetherfy, from a runtime template | You |
| Platform runner injected | Yes | **No** |
| `GET /health` endpoint | Provided for you | **You must implement it** |
| Signal handling on shutdown | Handled for you | Yours |
| `entrypoint:` in `aetherfy.yaml` | Selects the file to run | Ignored — your `CMD`/`ENTRYPOINT` decides |
| Lockfile requirements | Enforced for Node and Bun | **Skipped** — you pin dependencies yourself |

The consequential row is the health endpoint. Aetherfy health-checks every
`service` agent at `GET /health` on port 8080. A standard runtime gets that for
free from the injected runner; with a custom Dockerfile nothing is injected, so
an agent that does not answer `/health` will never be considered healthy.

## What an Aetherfy dockerfile archive must contain

A `Dockerfile` at the **root** of the uploaded archive — the same directory as
`aetherfy.yaml`, not in a subfolder.

```
aetherfy.yaml          # runtime: dockerfile
Dockerfile             # required, at the root
src/
  main.go              # or any language you like
```

A complete `aetherfy.yaml`:

```yaml
name: my-go-agent
runtime: dockerfile
type: service
memory_mb: 512
regions:
  - us-east-1
```

A complete `Dockerfile` — a Go multi-stage build that satisfies everything
Aetherfy checks for:

```dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o server .

FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
```

Your program must listen on port 8080 and serve `GET /health` with HTTP 200.

## What Aetherfy validates in a custom Dockerfile

Validation runs in two tiers. These block the build and put the deployment into
`failed`:

| Condition | Result |
|---|---|
| No `Dockerfile` at the archive root | Build fails: "runtime 'dockerfile' is set but no Dockerfile was found in the archive. Add a Dockerfile to your project root." |
| No `CMD` and no `ENTRYPOINT` instruction | Build fails — nothing would start |

These are logged as warnings and do **not** block the build:

| Condition | Why it is flagged |
|---|---|
| No `EXPOSE` instruction | The port is likely unreachable |
| No `/health` path detected in the image | Health checks will not pass |
| `chmod 777` or world-writable files | Overly permissive permissions |
| Runs as root with no `USER` instruction | Container runs privileged |

Aetherfy's validation is deliberately non-exhaustive — it catches the common
mistakes rather than linting arbitrary Dockerfiles. A build that passes
validation is not thereby certified correct.

## Which Aetherfy plans allow the dockerfile runtime

| Tier | Custom Dockerfile |
|---|---|
| Free | Not available |
| Starter | Available |
| Performance | Available |
| Enterprise | Available |

Creating an agent with `runtime: dockerfile` on Free is rejected with HTTP 400
and a message naming the restriction. The gate also applies in reverse: an
account with a `dockerfile` agent cannot downgrade to Free until that agent moves
to a standard runtime, and the downgrade check names the offending agent.

Because a runtime is immutable once an agent exists, switching an existing agent
to or from `dockerfile` is not possible — a changed runtime is rejected with
`RUNTIME_IMMUTABLE`. Delete the agent and recreate it, or create a new one
alongside. See [the aetherfy.yaml reference](/agents/aetherfy-yaml).

## Aetherfy environment variables in a custom container

A custom Dockerfile changes nothing about the environment your agent receives.
Aetherfy injects the same variables it injects for a standard runtime —
`AETHERFY_AGENT_ID`, `AETHERFY_AGENT_NAME`, `AETHERFY_REGION`,
`AETHERFY_WORKSPACE`, `AETHERFY_API_KEY`, `AETHERFY_API_URL`,
`AETHERFY_VECTORS_URL`, `AETHERFY_SPAWN_URL`, `AETHERFY_DEPLOYMENT_ID`, and
`AETHERFY_SPAWN_ID` on ephemeral runs — plus every secret you have set.

Logging is unchanged too: whatever your container writes to stdout and stderr
becomes the agent's logs. See [Runs and logs](/agents/runs-and-logs).
