---
title: Write a guest Dockerfile
description: Start from the stanza microvm dockerfile prints, add your layers, build it locally under arm64, and avoid the traps that each cost a server-side build cycle.
---

```bash
microvm dockerfile --workdir /workspace > guest.Dockerfile
# add your RUN layers between the chmod line and the ENV lines
docker buildx build --platform linux/arm64 -t guest-check -f guest.Dockerfile .
microvm build --reuse --name my-task-image --dockerfile guest.Dockerfile
```

A server-side build cycle is roughly three minutes plus, on some failures, an image name you cannot reuse. Each item below cost a real cycle to find. At the end of this page you will have a Dockerfile that builds locally under arm64 and passes every check the client runs before it makes a billable call.

## 1. Start from the generated stanza

`microvm dockerfile` prints the Dockerfile that a default build bakes, generated by the same code that builds it, with the platform constraints annotated inline. `--from` sets the image ref for the `FROM` line and defaults to the managed al2023 base's pair, `--port` (default `9000`) is the port agentd listens on inside the guest, and `--workdir` is a working directory to create and set, which the help calls strongly recommended. The output opens with comment lines naming the constraints below; stripped of those, the stanza is this:

```dockerfile
FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal
COPY agentd /agentd
RUN chmod 0755 /agentd
RUN mkdir -p /workspace
WORKDIR /workspace
ENV AGENTD_PORT=9000
ENV AGENTD_LOG=info
EXPOSE 9000
ENTRYPOINT []
CMD ["/agentd"]
```

Append your `RUN` layers after the `chmod` line and pass the result to `--dockerfile`. When you pass `--dockerfile` you are replacing the default, so you carry its daemon lines yourself.

## 2. Build it locally under arm64 first

```bash
docker buildx build --platform linux/arm64 -t guest-check -f guest.Dockerfile .
```

A `dnf` typo or a missing package is free to find here and expensive to find server-side. With `qemu-aarch64` binfmt registered this builds the real target architecture on an x86 host. Two errors in the repository's own example Dockerfile were caught this way in seconds.

## 3. The rules the client checks before any call

**The `FROM` must match the managed base's registry ref.** The build runs your Dockerfile on top of the base the create call names, and a mismatch builds against a base none of the measured platform behavior applies to, so the client refuses it locally. A `@sha256:` digest suffix on that same ref is accepted and is what the shipped examples pin.

**Set a `WORKDIR` explicitly.** `al2023-minimal`, like most public ARM64 bases, leaves `WorkingDir` empty, so an omitted `--cwd` on every later `exec` resolves against `/`. The client requires one when the base declares none. Check that the user your workload runs as can write to it; a root-owned `WORKDIR` under a non-root workload fails at the first write.

**`ENV AGENTD_PORT` must agree with the client's port.** The platform dials its build-time `ready` and `validate` hooks on the port from the create call, so a guest listening elsewhere answers neither and the build fails with `CREATE_FAILED` after a completely clean build log. The daemon's own `agentd listening` line appears, with the wrong address, and no error line follows. The client refuses the disagreement locally, including the case where the Dockerfile names no port while you have moved the client off the default with `--port`: an unset variable leaves the daemon on 9000 rather than on your port.

**Keep `ENTRYPOINT []` and `CMD ["/agentd"]`.** That pair is the trust boundary. It guarantees no workload runs before the platform's run hook lands and the token arrives, and it is what makes an omitted `cwd` inherit the image `WORKDIR`. An `ENTRYPOINT` that swallows `CMD` produces a VM whose daemon never starts, and the symptom is a run-hook timeout that mentions neither. A base image that starts its own background process before bootstrap breaks the invariant too, and enforcing it belongs to whoever builds the image. [Trust](/microvms-agentd/internals/trust/) is the threat model.

**Leave `AGENTD_SSE_KEEPALIVE_SECS` alone** unless you also raise the client's stream idle timeout. The client treats sixty seconds of silence as a dead connection because the daemon's keepalive is fifteen; a longer interval makes healthy streams look dead. This too is refused locally.

## 4. On a -minimal base

Spell weak dependencies off as `--setopt=install_weak_deps=0`. The integer is what works; `False` is not accepted.

`useradd` is not in the minimal base, and pulling `shadow-utils` in for one line is a bigger image for no more correctness. The examples append to `/etc/passwd` directly and hand the working directory to that uid, because `WORKDIR` is where every exec lands and a root-owned one fails a demoted workload at its first write:

```dockerfile
RUN echo "agent:x:1000:1000::/workspace:/bin/bash" >> /etc/passwd \
    && echo "agent:x:1000:" >> /etc/group
RUN mkdir -p /workspace && chown 1000:1000 /workspace
WORKDIR /workspace
```

The daemon itself stays root; demotion is per command, with `exec --user 1000 --group 1000`. [Run coding agents on Bedrock](/microvms-agentd/learn/operations/run-coding-agents-on-bedrock/) shows why that uid is not decoration.

## 5. No secret goes in the image

The image becomes a shared snapshot, so every VM launched from it sees the same bytes. Per-VM credentials travel through the platform's one-shot `runHookPayload` at launch: the agent token always, and anything you add with `run --launch-env KEY=VALUE`, which shares the payload's 4096-byte ceiling. Larger material goes in after bootstrap with `microvm cp --mode 0600` over the authenticated channel.

## 6. The build context holds two members

The artifact `microvm build` uploads carries exactly the Dockerfile and the `agentd` binary, so there is no file beside them for a `COPY` to find. A script the image needs is materialized in the Dockerfile with `printf`; [Prefetch S3 content at image build](/microvms-agentd/learn/operations/prefetch-s3-at-build/) shows a start wrapper written that way. `build --project` is the one widening: it adds the project's manifest and lockfile pair at the archive root, so `COPY pyproject.toml uv.lock ./` finds them.

## 7. The daemon's knobs

Every `AGENTD_*` variable is read at startup, and an unset or unparseable value keeps the default rather than refusing to boot, because a daemon that will not start strands the VM with no way in. Set them as `ENV` lines. The stanza already sets `AGENTD_PORT` and `AGENTD_LOG`; the others bound request bodies, captured output, stream replay, stdin writes, the disk reserve, and the identity repair that gives each VM restored from one snapshot its own machine-id, hostname, and boot\_id. The table with defaults is in [Embedding](/microvms-agentd/internals/embedding/).

## 8. Working examples

Three Dockerfiles in the repository respect all of the above: [coding-agents-on-bedrock](https://github.com/laithalsaadoon/microvms-agentd/tree/main/examples/coding-agents-on-bedrock) adds Node, Python, and two agent CLIs; [code-server-remote-dev](https://github.com/laithalsaadoon/microvms-agentd/tree/main/examples/code-server-remote-dev) installs an IDE from a release RPM; [s3-prefetch-at-build](https://github.com/laithalsaadoon/microvms-agentd/tree/main/examples/s3-prefetch-at-build) replaces `CMD` with a wrapper that fetches before it hands off to the daemon.

When a build fails anyway, [Debug a failed build](/microvms-agentd/learn/operations/debug-a-failed-build/) says where the reason lives. The measurements behind every rule above are in [Platform](/microvms-agentd/internals/platform/).