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.
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
- The Miren service runs
amp --no-tui --runner-id <id>as a long-lived process. - On start it reads
AMP_API_KEYand dials ampcode.com, registering as runner<id>. - Its
settings.jsonhasamp.remoteThreadCreation.enabled: true, so it accepts threads created remotely. - From the ampcode.com web app you create a thread on that runner. It runs in the runner's
working directory (
/workspaceon the disk) and streams output back to ampcode.com — and tomiren logs.
Because a runner is identified by host + working directory, the persistent disk keeps its workspace (cloned repos, build caches) stable across redeploys.
Prerequisites
mirenCLI 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), andstatic.ampcode.com(binary downloads for install andamp 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
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.
Miren runs the service command from app.toml via /bin/sh -c "<cmd>", so this image
doesn't need an ENTRYPOINT or CMD — runner.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).
runner, not webThis 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.
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 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).
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-ampMachine. It runs in/workspaceon 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
- Set
amp.remoteThreadCreation.enabled: true(inrunner.sh), or the runner accepts no threads. - Portless service: omit
port/port_type, and name itrunner, notweb(elseerror acquiring lease). - Run the daemon
amp --no-tui, notamp -x— the service command must not exit. - Give it a stable
AMP_RUNNER_ID(valid hostname) and a fixed working directory. - A
mirendisk meansfixed/num_instances = 1; expectdid not become healthynoise on redeploy (verify state before retrying). Uselocalfor snappy rollouts. - Point
HOMEandAMP_SETTINGS_FILEat the writable disk. - Clear Amp's stale runner pidfile on boot (in
runner.sh), or redeploys crash-loop withAnother Amp process is already serving remote threads. - The agent runs arbitrary shell — use a dedicated key and constrain it.
Next steps
- App Configuration — the full
app.tomlreference in context - Persistent Storage — Miren disks vs. local disks
- Deployment — deploying your own source to Miren
- Firewall — controlling egress if you restrict outbound traffic
- Using Dockerfile.miren — building from your own Dockerfile