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.
Use it when your program needs a toolchain Aetherfy does not ship — Go, Rust, Java, a system library, a compiled binary. You own what is inside the container; Aetherfy still runs it the way it runs every other agent.
What changes when an Aetherfy agent uses the dockerfile runtime
| Standard runtime | runtime: dockerfile | |
|---|---|---|
| Who writes the Dockerfile | Aetherfy, from a runtime template | You |
| Aetherfy supervisor | In front of your app | In front of the command your image declares — your image is not modified |
GET /health endpoint | Provided for you | Provided for you — you do not write one |
| Your server’s port | Chosen by Aetherfy | 8080, which is also in PORT |
| Logs | Your stdout and stderr | Your stdout and stderr |
| Signal on shutdown | SIGTERM to your server and every process it started | The same — draining on SIGTERM is your server’s code |
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 |
Aetherfy puts one small static binary, its supervisor, in front of your
container’s own command. Your Dockerfile is built exactly as you wrote it,
from exactly the files you uploaded — Aetherfy adds nothing to your build
context and changes nothing in it, your .dockerignore included; then Aetherfy reads the ENTRYPOINT/CMD your built image declares and adds a
layer in which the supervisor starts that command. Your image is not modified
and your command is not replaced. What the supervisor does with the command
depends on the agent’s type: a service runs it once, as the server the
supervisor sends requests to; a job runs it once per run request.
Your image must declare an ENTRYPOINT or a CMD, inherited from your base
image or written in your Dockerfile. An image that declares neither has no
command to run, and the deploy is refused with a message saying so.
Your image’s ENTRYPOINT does not run as PID 1. It runs as a child of
Aetherfy’s supervisor, for a service and a job alike, so an init that
insists on being PID 1 will not start. s6-overlay’s /init — the entrypoint of
linuxserver.io-style images — exits with s6-overlay-suexec: fatal: can only run as pid 1, and the deploy fails with that line as its load error. tini does
start: it logs a warning that it is not running as PID 1 and is not registered
as a child subreaper, so it will not reap zombie processes, and then runs your
command as usual.
How Aetherfy runs a custom service container
Your server listens on port 8080. Hardcode it, or read it from PORT:
Aetherfy sets PORT to 8080 for your server, replacing any value your image
sets. The supervisor listens on port 8079 inside your container and is what the
platform routes requests to, so nothing in your container may bind port 8079.
The supervisor holds that port before your server starts: a server that tries
to bind it gets its own “address already in use” error, and if that makes it
exit before it listens, the deploy fails with that error as the reason.
Aetherfy answers GET /health for you. The supervisor answers it: 503
while your server is starting, 200 once your server accepts connections on
port 8080, and 503 for good if your server exits before it listens. Your server
does not need a /health route, and a /health route it has is never reached —
the supervisor answers that path itself. If your server must finish warming up
before it takes traffic, bind port 8080 only once it has.
Aetherfy routes on that answer: a machine is sent requests only while its
GET /health answers 2xx. A new release takes no traffic until it does, and a
running machine that stops answering leaves rotation until it recovers.
A server that exits before it listens fails the deploy at once, with what it printed to stderr as the reason, in the deployment’s error and in your agent’s logs. A server that exits after it started serving stops the machine, as it would on any runtime.
The rest works as it does on every runtime. Whatever your server writes to
stdout and stderr becomes the agent’s logs. A request that arrives while your
server is starting is held until it listens, and
idle_timeout_minutes suspends an idle machine; both
work exactly as described there. WebSocket connections are passed through to
your server and keep the machine awake while they are open. Aetherfy cannot see
your routes inside a custom container, so it does not report whether your agent
serves WebSockets.
On shutdown your server and every process it started receive SIGTERM,
and the machine has 120 seconds before SIGKILL. Your server gets the same
share of that window as a server on any other runtime — see
How Aetherfy shuts a service down.
The supervisor delivers the signal; finishing in-flight requests when it
arrives is your server’s code, because nothing Aetherfy generated is running
inside it.
How Aetherfy runs a task in a custom container
A task in a custom container works like any task. A type: job agent on
Aetherfy is driven by the supervisor, which receives each run, hands your code
its payload and reports what it returned. The command your image declares is
what it runs once per run request. Everything on
The task contract applies — the payload file, the
result file, exit codes, logs, stopping and the one-run-at-a-time rule.
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 likeA complete aetherfy.yaml:
name: my-go-agent
runtime: dockerfile
type: service
memory_mb: 512
regions:
- us-east-1A complete Dockerfile — a Go multi-stage build that satisfies everything
Aetherfy checks for:
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. It needs no /health route: Aetherfy
answers that one for you.
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 |
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
| Plan | 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.
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_MEMORY_MB, AETHERFY_VCPUS, AETHERFY_API_KEY, AETHERFY_API_URL,
AETHERFY_VECTORS_URL, AETHERFY_SPAWN_URL, AETHERFY_DEPLOYMENT_ID,
AETHERFY_SPAWN_ID on ephemeral runs, and AETHERFY_WORKSPACE when the agent
declares a workspace — plus every secret you have set. A service also finds
PORT set to 8080, the port its server listens on.
Logging is unchanged too: whatever your server writes to stdout and stderr becomes the agent’s logs, exactly as on a standard runtime. See Runs and logs.
Installing the Aetherfy SDK in a custom container
The standard runtimes preinstall it; a custom container does not. Aetherfy
writes the dependency steps for a standard image and can put the SDK in one. It
does not write yours, so if your code imports the agent helper — payload,
machine, fan_out, spawn, write_result, result, wait — install it in
your own Dockerfile, like any other dependency:
RUN pip install aetherfy-vectorsRUN npm install aetherfy-vectorsNothing on the task contract requires the SDK. Every call it makes is an environment variable, a file, or a documented REST endpoint, and a container that reads those directly needs no package at all.