Deployment
Run the engine yourself with Docker, deploy it from a git push to Render or Railway, or provision a starter stack on AWS. The agents run in Daytona either way, so you deploy only the small engine. Once it's hosted, watch runs from your dev machine with an authenticated dashboard.
Run the stack yourself with Docker.
The self-managed path has two parts. The engine (loopy run) is one long-lived
process you run yourself with Docker, on your own machine for development or a VM you control in
production: it loads the manifest, hosts your sensor webhooks,
and drives runs as events arrive. The agents run somewhere else: each step runs in a
Daytona sandbox, a container cloud you reach with an API key, so you don't manage that compute.
You compile, copy the manifest and project to the host, set a few credentials, and start
the stack.
1. Compile the graph
loopy compile turns the project into one manifest.json: it builds the
step graph and checks every event name and {{ }} reference. A green compile is the
deploy gate, so run it in CI. The deploy artifact is the manifest plus the project root, which is
still read at runtime for your sensor code and env_file paths.
# exits 0 only if every reference resolves and the graph is acyclic
loopy compile . # writes manifest.json (use --check to validate only)
2. Set credentials on the host
On the VM, put the infrastructure credentials the engine needs in loopy.env at the
project root. The key one for the cloud is DAYTONA_API_KEY: that's the sandbox cloud
your agents run in. Run loopy auth github once to register a GitHub App so agents can
clone, push, and open PRs, then loopy webhooks github to register the GitHub webhooks
against LOOPY_PUBLIC_URL (it writes GITHUB_WEBHOOK_SECRET here for you).
The agents' model keys (like ANTHROPIC_API_KEY) go in the
sandbox env_file, not here. See Secrets.
DAYTONA_API_KEY=dt-... # the sandbox cloud agents run in
LOOPY_PUBLIC_URL=https://loopy.example.com # base URL sources deliver webhooks to
GITHUB_WEBHOOK_SECRET=... # verifies inbound deliveries; loopy webhooks github writes it
3. Start the engine
From the project directory, loopy run brings up the stack in Docker: a
redis container (the event bus) and the loopy container (sensor webhooks plus the
runtime). Run history is written to a named volume, and redis persists its queue, so both survive
a container restart. The webhooks bind to port 8000.
cd my-project
loopy run --detach # brings up redis + loopy; webhooks on :8000
Deploy from a git push (Render, Railway).
Any platform that builds a container from a git push runs the engine the same way, so the
steps below are identical on Render, Railway, or Fly. Two commands generate everything the
platform needs: loopy dockerfile writes the image definition, and
loopy env prints the environment to paste into the platform's settings. The
engine leans only on what every host exposes: a process environment for its secrets, a TCP
port it binds, an HTTPS ingress that terminates TLS, and a disk for run history. Agents
still run in Daytona, so the platform hosts only the small engine, not the agent compute.
1. Generate the Dockerfile
loopy dockerfile writes a version-pinned Dockerfile and
.dockerignore to the project root, so you never hand-write either. The image
installs the loopy release the command was generated from, copies the project in, and runs
loopy compile . during the build, so a red compile fails the deploy. It starts
one in-process engine and passes no --bus flag: the engine uses Redis when
REDIS_URL is set and the in-process bus otherwise. Run history is written inside
the container at .loopy/state.db; for durability across redeploys, mount a disk
and add --state-path /state/state.db to the command. The .dockerignore
keeps loopy.env and every dotenv file out of the image. Both files are pinned to
your CLI, so regenerate them after upgrading loopy, then commit both and connect the repo.
# Generated by `loopy dockerfile`; pinned to your installed loopy, regenerate after an upgrade
FROM python:3.12-slim
RUN pip install --no-cache-dir "loopy-computer[redis]==0.1.0"
WORKDIR /project
COPY . /project
RUN loopy compile . # build gate: a red compile fails the deploy; writes manifest.json
ENTRYPOINT ["loopy"]
CMD ["run", "manifest.json", "--in-process", "--root", ".", "--host", "0.0.0.0", "--port", "8000"]
2. Print and paste the environment
loopy env reads your local loopy.env and prints the engine's
control-plane block for a one-shot paste into the platform's env settings (Render environment
variables, Railway variables). It emits the infrastructure creds the engine needs
(DAYTONA_API_KEY, the GitHub App keys, GITHUB_WEBHOOK_SECRET,
LOOPY_ADMIN_TOKEN) plus a REDIS_URL placeholder for you to fill in.
It omits LOOPY_PUBLIC_URL (that one is dev-side, for webhook registration and
loopy admin) and never prints the sandbox model keys. It writes secrets to
stdout on purpose, for that paste: do not commit the output. See
Secrets.
# --- Control plane (engine): infra creds; set these on the platform ---
DAYTONA_API_KEY=dt-...
GITHUB_APP_ID=...
GITHUB_APP_PRIVATE_KEY=...
GITHUB_WEBHOOK_SECRET=...
LOOPY_ADMIN_TOKEN=loopy_sk_...
REDIS_URL=redis://… # managed Redis connection string; remove this line to use the in-process bus
# LOOPY_PUBLIC_URL is dev-side (webhook registration, loopy admin); keep it in your local loopy.env, not here
3. Managed Redis and a persistent disk
Add the platform's managed Redis (Render Key Value, Railway's Redis plugin) and point
REDIS_URL at it, so the event queue survives a restart. Leave it out and the
engine falls back to the in-process bus: fine for a single node, but the queue is lost when
the container recycles. Give the engine a persistent disk (a Render disk, a Railway volume)
at the mount path you passed to --state-path, so run history survives a
redeploy. Keep the engine at one instance: SQLite is a single writer, and the disk pins the
service to one instance anyway. A run that is mid-flight when the engine redeploys is still
lost; in-flight durability needs the durable runtime, a future feature.
4. The sandbox key stays a file
The agents' model key is not in the block above. It rides each sandbox's
env_file (for example secrets/base.env), which loopy env
never prints and .dockerignore keeps out of the image, so it is in neither your
repo nor the build. Provide that file on the platform at the same project-relative path the
env_file: in registry.yml names: on Render, add it as a Secret File;
on a platform without secret files, put it on a mounted volume at that path. The engine reads
it at runtime and injects it into the Daytona sandbox exactly as it would on a VM. See
Secrets for which credential lives where.
5. Point your sources at the public URL
The platform gives the service an HTTPS hostname and terminates TLS for you. Set
LOOPY_PUBLIC_URL to that hostname in your local loopy.env, then
loopy webhooks github registers the GitHub webhooks for you: one per repo in
registry.yml, pointing at /hooks/github under that host, via the
GitHub App. For other sources, loopy webhooks list prints each sensor's full
delivery URL to paste into that service's settings (loopy run prints them at
startup too). For the platform's health check, point it at /healthz: an open
liveness route that reports nothing else.
https://loopy-engine.onrender.com/hooks/github # one path per source; check the startup log
Host the engine on AWS.
AWS maps onto the same single-node setup as the VM path, provisioned for you from your own
account, with no domain to bring. One small EC2 instance runs two containers, a
redis and the loopy engine (the same single-node topology as the
bundled Docker stack), behind a CloudFront distribution that serves a valid,
auto-renewing certificate on its own *.cloudfront.net name, so GitHub and Sentry
can deliver webhooks over HTTPS without you owning a domain or touching DNS. An EBS volume
keeps run history and the Redis queue across a reboot. Agents still run in Daytona, so AWS
hosts only the engine, not the agent compute. Keep it at one instance for the same reason
Render does: SQLite is a single writer, and the data volume pins the engine to one host. This
is the bootstrap deploy target: loopy provisions the host for you, and the
target is named for what it is (the batteries-included starter path) rather than the cloud
it runs on. The mechanics are a CloudFormation template and the
loopy deploy bootstrap command that drives it.
1. What gets provisioned
loopy deploy bootstrap stands up one CloudFormation stack in your account: an EC2
instance in the default VPC, an Elastic IP so its address survives a stop or restart, an EBS
data volume mounted at /state, an IAM instance role scoped to read this stack's
secrets, and a CloudFront distribution in front. The distribution is the public HTTPS
endpoint: it terminates TLS with an AWS-managed certificate on its *.cloudfront.net
name, forwards every method and header to the engine so webhook POSTs and their signatures
arrive intact, and caches nothing. The instance's security group only accepts traffic from
CloudFront's managed prefix list, so the Elastic IP is a locked-down origin, not a second
open door. There is no load balancer, no managed Redis, and no certificate for you to manage.
loopy deploy bootstrap --region us-east-1
# creates the stack, prints the https://<id>.cloudfront.net URL when it's live
2. Provide the AWS keys
The command reads your AWS credentials the same way the AWS SDK does: a named
--profile, or the standard environment variables. That is the only input. It
calls create-stack under the hood, so a re-run is an update-stack
against the same stack name and a bad template rolls back on its own. There is no DNS step
and no domain to register: the CloudFront name exists the moment the distribution deploys,
and the certificate comes with it.
3. Where the secrets live
The split is the same one Render's Secret File handles. The engine's infrastructure creds
(DAYTONA_API_KEY, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY,
GITHUB_WEBHOOK_SECRET, LOOPY_ADMIN_TOKEN,
LOOPY_PUBLIC_URL) are stored as SSM Parameter Store SecureStrings and rendered
into loopy.env on the instance at boot, read through the instance role, never
baked into the template or an AMI. The agents' model key is different:
ANTHROPIC_API_KEY rides the sandbox env_file (for example
secrets/base.env), stored the same way and written to that project-relative path
on the instance, where the engine resolves it at runtime to inject into the Daytona sandbox
exactly as it would on a VM. See Secrets for
which credential lives where.
4. Point your sources at the public URL
LOOPY_PUBLIC_URL is the https://<id>.cloudfront.net name the
deploy resolves once the distribution is live. This is the parameter that does not exist
until you deploy, so loopy init never asks for it in this mode: the deploy
command writes it into your local loopy.env for you (and pushes it to SSM for
the instance), so registration works with no hand-copy. loopy deploy bootstrap does
not register webhooks itself. That stays one explicit step: run
loopy webhooks github after the deploy and it registers the GitHub
webhooks against that host just as it does anywhere else, and loopy webhooks list
prints each other source's full delivery URL to paste into its settings. Signature
verification is unaffected: GitHub's HMAC is over the request body, which CloudFront forwards
untouched. If you later want a name of your own, attach a domain to the distribution (an ACM
certificate plus a CNAME); it is never required to receive webhooks.
The dashboard is the one thing not served on that public URL here. CloudFront reaches the
instance over plain HTTP, so the dashboard's bearer token would cross an unencrypted hop,
and /admin is refused at the edge on purpose. Reach it over an SSM tunnel
instead: aws ssm start-session port-forwards the engine port to your dev machine,
and loopy admin recognizes the CloudFront URL, prints that exact tunnel
command (from the instance id the deploy records), then runs the UI locally against it
with the token kept in that tunnel. Webhooks are
unaffected (they keep hitting the public URL); this is the tradeoff of bringing no domain.
https://d111abcdef8.cloudfront.net/hooks/github # one path per source; check the startup log
5. Tear it down
Removing the deploy is one delete-stack: it takes the instance, the Elastic IP,
the CloudFront distribution, the security group, the role, and the parameters with it. The
EBS data volume holds your run history, so it goes too unless you snapshot it first. Take a
snapshot before you destroy if that history matters. The one thing kept is the small bucket
that carried the project tarball (a few kilobytes); the next deploy reuses it.
loopy deploy bootstrap --destroy --region us-east-1 # delete-stack; snapshot /state first if you need the history
Watch runs from your dev machine.
The dashboard is read-only, but run and
step outputs carry diffs, links, and root-cause text, so a hosted control plane serves
them only behind auth. The engine's one public URL is path-routed: webhook deliveries
arrive at $LOOPY_PUBLIC_URL/hooks/* and the dashboard is mounted at
$LOOPY_PUBLIC_URL/admin, so there is no second service to deploy. The
mechanism is one shared bearer token: set it once in the platform's environment and once
on your dev machine, and the dashboard UI runs locally while the data comes over HTTPS. The
contract is the same on every host (Render, Fly, Kubernetes, a VM): the token arrives as
a process env var and the platform ingress terminates TLS. Loopy never branches on the
provider.
1. Reuse the token loopy init minted
You do not generate this by hand. loopy init already minted a
LOOPY_ADMIN_TOKEN into loopy.env on your dev machine (a 256-bit
random string, loopy_sk_ prefixed so secret scanners can match a leaked one).
Copy that same value into LOOPY_ADMIN_TOKEN in the control plane's
environment (on Render, a service env var with sync: false, like
DAYTONA_API_KEY above). The two sides never share a file:
loopy.env is gitignored and is not part of any deploy artifact.
LOOPY_ADMIN_TOKEN=loopy_sk_... # minted by loopy init; copy this value into the platform env
2. Set the token on the control plane
With LOOPY_ADMIN_TOKEN in the service environment, loopy run
mounts the dashboard at /admin on the same server that receives webhooks,
and requires the token on every /admin/api route (checked in constant time,
before any handler). Without the token, a non-local bind serves webhooks but no
/admin at all, so a misconfigured deploy withholds run data instead of
serving it openly. GET /admin/healthz stays open for platform probes: it
reports liveness and nothing else.
loopy run ... # webhooks at /hooks/*, dashboard at /admin (bearer required)
3. Run the local client
On your dev machine, loopy admin starts a small local proxy: it serves
the dashboard at 127.0.0.1:9000 and forwards /api requests to
$LOOPY_PUBLIC_URL/admin with Authorization: Bearer attached,
both read from loopy.env. It routes itself from that URL, so you never name
a target: a host you provide (Render, a VM) is proxied at /admin, while the
provisioned stack's CloudFront URL is reached over its SSM tunnel instead (pass
--tunnel to force it). The token stays in
that process. Your browser
never holds it, it never appears in a URL, and the client refuses to send it over plain
HTTP to a non-local host. To point somewhere else, pass --url:
loopy admin --url https://….
loopy admin # UI local, data from $LOOPY_PUBLIC_URL/admin (CloudFront → SSM tunnel)