Skip to main content

Run an Amp agent on Miren

Run Amp, a coding agent, as a headless runner on a Miren cluster. The container dials out to ampcode.com, registers as a Machine, and then waits. You create threads from the ampcode.com web app, pick this runner, and the work executes on Miren — streaming back to your browser.

The end state: a single portless Miren service running amp --no-tui, connected to ampcode.com with an API key, with its working directory on a persistent Miren disk. There is no inbound port, no dashboard, and no HTTP ingress — ampcode.com is the control plane and the runner only makes outbound connections.

Along the way it exercises real Miren behavior — custom Dockerfile builds, a portless background service, 0.0.0.0-free networking (nothing listens), a writable network disk, and single-instance concurrency — so it doubles as a tour of running an outbound-only worker.

This is an application recipe, not a language guide

For getting your own source code onto Miren, start with Deployment and the Language Guides. This page is about running a third-party agent as an outbound-only service.

How it works

  1. The Miren service runs amp --no-tui --runner-id <id> as a long-lived process.
  2. On start it reads AMP_API_KEY and dials ampcode.com, registering as runner <id>.
  3. Its settings.json has amp.remoteThreadCreation.enabled: true, so it accepts threads created remotely.
  4. From the ampcode.com web app you create a thread on that runner. It runs in the runner's working directory (/workspace on the disk) and streams output back to ampcode.com — and to miren logs.

Because a runner is identified by host + working directory, the persistent disk keeps its workspace (cloned repos, build caches) stable across redeploys.

Prerequisites

  • miren CLI installed and authenticated (miren whoami).
  • Access to the target cluster and its org.
  • An Amp API key from ampcode.com/settings.
  • Outbound HTTPS from the cluster to Amp's domains (see Firewall if you restrict egress): ampcode.com (service + installer), auth.ampcode.com (authentication), production.ampworkers.com (the runner's WebSocket connection), and static.ampcode.com (binary downloads for install and amp update). A narrower allowlist can let the build or runner start while authentication or registration silently fails.

Select the target cluster

If the cluster isn't configured locally yet, list what your identity can see and add it (this pins the TLS fingerprint):

# Add a cluster your cloud identity can see (interactive picker; pins the TLS fingerprint)
miren cluster add -i cloud

miren whoami -C amp

The commands below target it explicitly with -C amp. Omit -C to use your default cluster.

The Dockerfile

Amp ships as a CLI, not a container image, so build a thin image that installs it with the official installer (a standalone binary — no Node required). Include the tools the agent will reach for when it runs shell commands (git, ripgrep, curl):

FROM debian:bookworm-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates ripgrep curl \
&& rm -rf /var/lib/apt/lists/*

# Install the Amp CLI to a fixed location and put it on PATH. AMP_VERSION pins the
# release for reproducible builds; drop it to track latest, or bump it deliberately.
ENV AMP_HOME=/opt/amp
RUN curl -fsSL https://ampcode.com/install.sh | AMP_VERSION=0.0.1784319456-g6a2cfc bash \
&& ln -s /opt/amp/bin/amp /usr/local/bin/amp

COPY runner.sh /usr/local/bin/runner.sh
RUN chmod +x /usr/local/bin/runner.sh
Why the extra symlink

The installer drops the binary at $AMP_HOME/bin/amp and tries to symlink it into a per-user PATH directory (~/.local/bin). Building as root, that isn't reliably on PATH, so the recipe symlinks /opt/amp/bin/amp into /usr/local/bin explicitly.

No ENTRYPOINT or CMD needed

Miren runs the service command from app.toml via /bin/sh -c "<cmd>", so this image doesn't need an ENTRYPOINT or CMDrunner.sh is invoked directly (see below).

The runner entrypoint

runner.sh writes Amp's settings on the disk (enabling remote thread creation so ampcode.com can dispatch threads to it — without this the runner connects but silently accepts nothing), clears any stale runner lock left on the disk by a prior deploy, then hands off to the runner with exec so signals reach Amp and Miren can stop it cleanly:

#!/usr/bin/env bash
set -euo pipefail

# Working directory for the runner. Override with AMP_WORKSPACE (in app.toml [[env]]) if you
# change the disk mount_path.
WORKSPACE="${AMP_WORKSPACE:-/workspace}"
mkdir -p "$HOME" "$WORKSPACE" "$(dirname "$AMP_SETTINGS_FILE")"

# Enable remote thread creation so ampcode.com can dispatch threads to this runner. Rewritten
# on every boot (not just when absent) so a stale settings file left on the persistent disk
# can't silently keep the setting off after you change this recipe. Edit the keys to reconfigure.
cat > "$AMP_SETTINGS_FILE" <<'JSON'
{
"amp.remoteThreadCreation.enabled": true,
"amp.notifications.enabled": false
}
JSON

# Amp records a per-directory runner lock (a pidfile under its cache). With HOME on a
# persistent disk that survives redeploys, a stale pidfile makes the new runner refuse to
# start ("Another Amp process is already serving remote threads"). The Miren disk lease
# guarantees only one runner container at a time, so any pidfile here at boot is stale.
rm -f "$HOME/.cache/amp/pids/"*.pid 2>/dev/null || true

# Optional: seed a repo for the agent to work in. Adapt or remove.
# if [ ! -d "$WORKSPACE/repo/.git" ]; then
# git clone https://github.com/you/your-repo "$WORKSPACE/repo"
# fi

cd "$WORKSPACE"
exec amp --no-tui --runner-id "$AMP_RUNNER_ID"

The app.toml

This file lives at .miren/app.toml in your project (not the repo root). If you haven't set the project up yet, miren init creates it for you; otherwise create the .miren/ directory and add the file below. The Dockerfile and runner.sh stay at the project root.

name = "amp-runner"

[build]
dockerfile = "Dockerfile"

# Portless background service: nothing listens, so no `port`/`port_type`.
[services.runner]
command = "runner.sh"

[services.runner.concurrency]
mode = "fixed"
num_instances = 1

[[services.runner.disks]]
name = "workspace"
provider = "miren"
mount_path = "/workspace"
size_gb = 20
filesystem = "ext4"

# A stable, valid hostname, shown as the Machine name on ampcode.com. A runner is identified
# by host + working directory, so keep this fixed to make it easy to pick in the web app.
[[env]]
key = "AMP_RUNNER_ID"
value = "miren-amp"
# Keep Amp's settings, cache, and logs on the writable disk. Amp otherwise reads/writes
# ~/.config/amp, which lands on a read-only image path.
[[env]]
key = "AMP_SETTINGS_FILE"
value = "/workspace/amp/settings.json"
[[env]]
key = "HOME"
value = "/workspace/home"
[[env]]
key = "AMP_LOG_LEVEL"
value = "info"

[[env]]
key = "AMP_API_KEY"
required = true
sensitive = true

amp --no-tui is a long-lived daemon (unlike amp -x, which runs one prompt and exits): Miren restarts it if it exits, and the runner re-registers with ampcode.com after a bounce (it may briefly show as a new Machine connection before the old one drops).

Portless service — name it runner, not web

This service has no port/port_type, so Miren does no HTTP ingress and no port health check. Do not name it web: Miren's ingress routes an app's hostname to the web service, and a portless web service returns error acquiring lease: app/amp-runner (HTTP 500) for every request. Any other name (like runner) is fine.

Constrain the agent — it runs arbitrary shell

A runner executes agent-driven shell commands as the container user with full access to /workspace. Use a dedicated Amp API key (revocable independently), treat the workspace as untrusted, and consider Amp's amp.commands.allowlist / amp.commands.strict settings to limit what it can run unattended.

A Miren disk forces single-instance, churny rollouts

A provider = "miren" (network) disk holds one exclusive lease, so it requires concurrency mode = "fixed" with num_instances = 1. On redeploy the new instance can't mount /workspace until the old one releases the lease, so miren deploy may print did not become healthy while the instance actually comes up a few seconds later — re-check miren app status / miren sandbox list before assuming failure. If you'd rather have snappy rollouts and don't need node-independent storage, use provider = "local" (no lease, no fixed-instance requirement; workspace is node-local).

Redeploys leave a stale runner lock on the disk

Amp writes a per-directory runner lock — a pidfile under $HOME/.cache/amp/pids/. Because HOME is on the persistent disk, that pidfile survives a redeploy, and the fresh runner refuses to start with Another Amp process is already serving remote threads for /workspace. runner.sh deletes stale pidfiles on boot before launching Amp; the disk's single-instance lease guarantees no live runner is ever using them.

Add a .dockerignore so local secrets and Miren state stay out of the build context (secrets are passed with miren deploy -s, never baked into the image):

.env
.miren

Deploy

Non-secret config lives in app.toml. Pass the API key with -s (masked in output, stored server-side); never bake it into the image:

miren deploy -a amp-runner -C amp -f \
-s "AMP_API_KEY=sgamp_..."

Validate the config without building at any time:

miren deploy --analyze -a amp-runner -C amp

Verify

miren app status -a amp-runner -C amp       # Current Version + active
miren sandbox list -C amp # one running sandbox, service "runner"
miren logs -a amp-runner -C amp --since 5m # look for the runner connecting to ampcode.com

Then open ampcode.com: the Machine miren-amp should appear as a connected runner. Create a thread on it, send a prompt, and confirm the work executes — output streams in the ampcode.com web UI and in miren logs.

Using the runner

  • Create work from ampcode.com: start a thread targeting the miren-amp Machine. It runs in /workspace on the cluster.
  • Pre-seed a repo: clone into the workspace once, and it persists on the disk:
miren sandbox list -C amp                   # grab the sandbox id
miren sandbox exec <sandbox-id> -C amp -- \
git clone https://github.com/you/your-repo /workspace/repo
  • Rotate the key: deploy again with a new -s "AMP_API_KEY=..."; the runner reconnects on restart.

Roadblock checklist

  1. Set amp.remoteThreadCreation.enabled: true (in runner.sh), or the runner accepts no threads.
  2. Portless service: omit port/port_type, and name it runner, not web (else error acquiring lease).
  3. Run the daemon amp --no-tui, not amp -x — the service command must not exit.
  4. Give it a stable AMP_RUNNER_ID (valid hostname) and a fixed working directory.
  5. A miren disk means fixed / num_instances = 1; expect did not become healthy noise on redeploy (verify state before retrying). Use local for snappy rollouts.
  6. Point HOME and AMP_SETTINGS_FILE at the writable disk.
  7. Clear Amp's stale runner pidfile on boot (in runner.sh), or redeploys crash-loop with Another Amp process is already serving remote threads.
  8. The agent runs arbitrary shell — use a dedicated key and constrain it.

Next steps