# deploy.me deploy.me is a self-service deployment platform for AI agents and long-running container workloads. You write one TypeScript file that imports the SDK, call `.up()`, and your container is live at a public HTTPS URL within seconds. Per-second billing, hard monthly caps built in, roughly one-third the cost of e2b or Daytona at the same compute size. This file is the structured doc surface for crawlers and LLM agents. It is served as `text/plain` from https://deploy.me/docs/llms.txt and is the canonical machine-readable reference for the public API. ## Quick start Install the SDK (and optionally the CLI): ```bash bun add @deploy-me/sdk bun add -g @deploy-me/cli # provides the `dp` command ``` Get an API token from https://deploy.me/dashboard and export it: ```bash export DEPLOY_ME_TOKEN=dpm_... ``` Write `main.ts`: ```typescript import { client } from "@deploy-me/sdk"; const dm = client({ token: process.env.DEPLOY_ME_TOKEN! }); const m1 = dm.machine({ cpu: 1, ram: 1 }); const hello = await m1 .deploy("hello") .image("traefik/whoami") .up(); console.log(hello.url); ``` Run it: ```bash bun main.ts # SDK directly # or dp up # CLI wrapper — same thing ``` ## SDK reference The package is `@deploy-me/sdk`. Everything is re-exported from the package root. There are no submodule imports. ```typescript import { client, Deploy, Cron, Secret, Currency, DeployMeError, } from "@deploy-me/sdk"; import type { Client, ClientOptions, Machine, Deployment, DeployBuilder, DeployConfig, DeploymentStatus, DeploymentStats, MachineConfig, ScaleConfig, HttpConfig, EnvMap, EnvValue, Target, LogLine, } from "@deploy-me/sdk"; ``` ### client(options): Client Factory that binds an API token to a transport and returns the top-level control surface. ```typescript function client(opts: ClientOptions): Client; interface ClientOptions { token: string; // required — from /dashboard baseUrl?: string; // default "https://api.deploy.me" region?: string; // default "eu-west" fetch?: typeof fetch; // override for tests timeoutMs?: number; // per-request, default 30_000 retries?: number; // 5xx/network retries, default 2 } interface Client { machine(config: MachineConfig): Machine; deploy(name: string): DeployBuilder; list(): Promise; get(name: string): Promise; // null on 404 } ``` Example: ```typescript const dm = client({ token: process.env.DEPLOY_ME_TOKEN! }); const all = await dm.list(); const one = await dm.get("hello"); if (one === null) console.log("hello is gone"); ``` ### Machine A sized compute target. Today a Machine is an in-memory sizing record whose specs ride along on each deploy — `stop()` is a no-op until machines become first-class engine objects. ```typescript type MachineConfig = { cpu: number; // vCPU count ram: number; // RAM in GB storage?: number; // disk in GB region?: string; // override the client's default region }; interface Machine { readonly id: string; readonly cpu: number; readonly ram: number; deploy(name: string): DeployBuilder; // pre-bound to this machine stop(): Promise; } ``` Example: ```typescript const m = dm.machine({ cpu: 8, ram: 32, region: "gra" }); const agent = await m.deploy("coder").image("ghcr.io/me/coder:1").up(); ``` ### DeployBuilder Fluent, immutable builder. Every method returns a fresh builder; chains are safe to fork. Two entrypoints: `client.deploy(name)` (unsized) and `machine.deploy(name)` (sized). ```typescript interface DeployBuilder { name(slug: string): DeployBuilder; image(ref: string): DeployBuilder; // any public Docker image target(t: Target): DeployBuilder; // `ovh:gra`, `aws:...`, `ssh:...` env(map: EnvMap): DeployBuilder; // merges with previous .env() scale(s: ScaleConfig): DeployBuilder; // { min, max, idle? } http(h: HttpConfig): DeployBuilder; // { port, path? } schedule(c: CronExpr): DeployBuilder; timeout(t: string): DeployBuilder; // e.g. "30m", "2h" budget(amount: number, currency?: Currency): DeployBuilder; onMachine(m: MachineConfig): DeployBuilder; toJSON(): DeployConfig; // inspect without deploying up(): Promise; // POST /deploy } ``` The top-level `Deploy` symbol is a transport-less builder useful for composing config; you must move into a client-bound builder before `.up()`. ```typescript const web = await dm .deploy("web") .image("ghcr.io/me/api:1.4") .onMachine({ cpu: 2, ram: 4 }) .http({ port: 8080 }) .env({ NODE_ENV: "production" }) .scale({ min: 1, max: 5, idle: "5m" }) .budget(20, Currency.EUR) .up(); ``` ### Deployment The handle returned by `.up()`, `client.list()`, `client.get()`. ```typescript interface Deployment { readonly id: string; // "dpm-" readonly name: string; readonly url: string; // https://.deploy.me readonly region: string; readonly status: DeploymentStatus; readonly image: string; readonly createdAt: string; // ISO readonly stats?: DeploymentStats; stop(): Promise; // DELETE /deploy/:name restart(): Promise; // POST /deploy/:name/restart refresh(): Promise; // GET /deploy/:name logs(opts?: { follow?: boolean; tail?: number }): AsyncIterable; } type DeploymentStatus = "live" | "deploying" | "stopped" | "failed"; type DeploymentStats = { cpu?: number; ramGB?: number; startedAt?: string; finishedAt?: string; restartCount?: number; exitCode?: number | null; }; type LogLine = { kind: "out" | "err"; text: string; t: number }; ``` Tail the last 50 lines and then stream forever: ```typescript for await (const ln of agent.logs({ tail: 50 })) { process[ln.kind === "err" ? "stderr" : "stdout"].write(ln.text + "\n"); } ``` ### Cron Pure value type. No transport. Build a `CronExpr` and pass it to `builder.schedule(...)`. ```typescript class CronExpr { readonly expr: string; readonly tz?: string } const Cron: { daily(time: string, tz?: string): CronExpr; // "HH:MM" hourly(): CronExpr; // 0 * * * * expression(expr: string, tz?: string): CronExpr; // raw 5-field cron }; ``` ```typescript import { Cron } from "@deploy-me/sdk"; await dm .deploy("nightly-backup") .image("ghcr.io/me/backup:1") .schedule(Cron.daily("03:00", "Europe/Paris")) .up(); ``` ### Secret Reference a value that the engine resolves server-side at deploy time. Raw values are never echoed back into user code. ```typescript class SecretRef { readonly name: string } const Secret: { from(name: string): SecretRef; }; ``` ```typescript import { Secret } from "@deploy-me/sdk"; await dm .deploy("api") .image("ghcr.io/me/api:1") .env({ OPENAI_API_KEY: Secret.from("openai_prod"), STRIPE_KEY: Secret.from("stripe_live"), }) .up(); ``` ### Currency Const-object enum used by `.budget(...)`. ```typescript const Currency = { EUR: "EUR", USD: "USD", GBP: "GBP" } as const; type Currency = (typeof Currency)[keyof typeof Currency]; ``` ### DeployMeError Thrown by all transport calls on non-2xx responses (after retries). ```typescript class DeployMeError extends Error { readonly status: number; // HTTP status, e.g. 401, 404, 500 readonly body: unknown; // parsed JSON body if available // .name === "DeployMeError" } ``` ```typescript try { await dm.deploy("x").image("nope:1").up(); } catch (e) { if (e instanceof DeployMeError && e.status === 404) { console.error("image not found"); } else { throw e; } } ``` ## CLI reference The CLI is `@deploy-me/cli`, exposed as the `dp` binary. It is a thin wrapper over the SDK — anything the CLI does, you can do from `main.ts`. ```bash bun add -g @deploy-me/cli export DEPLOY_ME_TOKEN=dpm_... ``` ### dp up [file] Runs `main.ts` (or the file you pass) with whichever JS runtime is available: `bun` → `node --experimental-strip-types` (22.6+) → `tsx`. The command exits with the script's own exit code. ```bash dp up # ./main.ts dp up scripts/web.ts # any other entrypoint ``` ### dp init [dir] Scaffolds a starter project: `main.ts`, `.gitignore`, `.env.example`. Refuses to overwrite an existing `main.ts`. Exits non-zero if the file already exists. ```bash dp init # current directory dp init my-agent # creates ./my-agent/ ``` ### dp ls List active deploys. Prints a table of name, image, URL, and status. ```bash dp ls ``` ### dp status Detailed view of one deployment: status, URL, image, region, cpu, ram, start time, restart count, exit code. ```bash dp status hello ``` ### dp logs [--no-follow] [--tail N] Tail container logs. Streams forever by default; `Ctrl-C` to stop. Use `--no-follow` to print only the historical tail and exit. `--tail N` sets how many existing lines to include (default 100). ```bash dp logs hello dp logs hello --tail 20 --no-follow ``` ### dp restart Restart the container in place. The URL and name are preserved. ```bash dp restart hello ``` ### dp rm Stop and remove a deploy. The next `dp up` recreates it from `main.ts`. ```bash dp rm hello ``` ### dp open Print the live URL and try to open it in the default browser. Silently no-ops in headless environments. ```bash dp open hello ``` ### Environment variables DEPLOY_ME_TOKEN required. API token from /dashboard. DEPLOY_ME_API_URL optional. Override the control-plane base URL. Defaults to https://api.deploy.me. ### Exit codes 0 success 1 user/config error (missing main.ts, missing token, no JS runtime, target name not found, HTTP 4xx from the control plane) >1 propagated from the spawned script (`dp up`) ## Recipes ### AI agent ```typescript import { client, Secret } from "@deploy-me/sdk"; const dm = client({ token: process.env.DEPLOY_ME_TOKEN! }); const m = dm.machine({ cpu: 8, ram: 32 }); const coder = await m .deploy("coder") .image("ghcr.io/lambda-run/coder-agent:1.2") .env({ ANTHROPIC_API_KEY: Secret.from("anthropic_prod") }) .budget(50) // EUR/month .timeout("6h") .up(); console.log(coder.url); ``` ### HTTP web server, autoscaled ```typescript const web = await dm .deploy("web") .image("ghcr.io/me/api:1.4") .onMachine({ cpu: 2, ram: 4 }) .http({ port: 8080, path: "/healthz" }) .scale({ min: 1, max: 8, idle: "5m" }) .env({ NODE_ENV: "production" }) .up(); ``` ### Scheduled cron job ```typescript import { Cron } from "@deploy-me/sdk"; await dm .deploy("nightly-backup") .image("ghcr.io/me/pg-backup:3") .onMachine({ cpu: 1, ram: 2 }) .schedule(Cron.daily("03:00", "Europe/Paris")) .timeout("30m") .up(); ``` ### Minecraft server ```typescript const m = dm.machine({ cpu: 4, ram: 8 }); const mc = await m .deploy("mc") .image("itzg/minecraft-server") .env({ EULA: "TRUE", MEMORY: "6G" }) .http({ port: 25565 }) .up(); ``` ### Multiple machines, one program ```typescript const small = dm.machine({ cpu: 1, ram: 1 }); const big = dm.machine({ cpu: 16, ram: 64 }); const [api, worker] = await Promise.all([ small.deploy("api").image("ghcr.io/me/api:1").up(), big.deploy("worker").image("ghcr.io/me/worker:1").up(), ]); ``` ## Limits Billing per-second, hard monthly cap from `.budget(...)` or the account default. The engine refuses to start a deploy that would exceed it. Regions v1 ships EU only — `eu-west` (default), `gra`, `sbg`. More to follow. Image registry v1 accepts public Docker images only. Private registries land with the secrets API. TLS HTTPS auto-provisioned by Caddy on https://.deploy.me. Sandbox TTL free-tier sandboxes are reaped after 30 minutes of idle. Paid deploys have no TTL — they live until stopped, removed, or out-of-budget. Concurrency default: 10 concurrent deploys per account. Raised on request. ## Errors All SDK methods that hit the control plane throw `DeployMeError` on non-2xx responses. The transport retries 5xx and network errors up to `retries` (default 2) with exponential backoff (150ms, 300ms, capped at 2s). 4xx responses are not retried. Common statuses: 400 malformed request — usually a missing `name` or `image`, or an invalid cron expression. `e.body` carries the engine's reason. 401 missing or expired token. Re-export `DEPLOY_ME_TOKEN`. 402 budget exceeded. Raise `.budget(...)` or wait for the next billing window. 404 no such deploy. `client.get()` returns `null` instead of throwing; other methods throw. 409 name collision. A deploy with that name already exists — `rm` it or pick a different slug. 429 rate limited. Back off and retry; the transport already retries once on 5xx but not 429. 500/502/503 transient engine error. The transport retries automatically; if it still surfaces, it is a real outage. ## API endpoints The control plane lives at `https://api.deploy.me` (override with `baseUrl` / `DEPLOY_ME_API_URL`). All requests carry `Authorization: Bearer ` and return JSON. The SDK is the supported interface; this section documents the wire format for parity. POST /deploy create or update a deploy body: { name, image, port?, cpu?, ramGB?, env? } returns: { name, image, url, container? } GET /list array of all deploys for the token returns: EngineDeployRow[] GET /deploy/:name one deploy returns: EngineDeployRow DELETE /deploy/:name stop and remove POST /deploy/:name/restart in-place restart GET /deploy/:name/logs SSE stream of `LogLine` JSON frames query: ?follow=0&tail=N GET /compute-rows public — no auth — the compute snapshot that powers /#pricing on the marketing site Wire shapes: ```typescript type EngineDeployRow = { name: string; image: string; state?: string; // running | created | restarting | exited | dead url: string; cpu?: number; ramGB?: number; startedAt?: string; // ISO finishedAt?: string; restartCount?: number; exitCode?: number | null; }; type LogLine = { kind: "out" | "err"; text: string; t: number }; ``` ## Architecture engine → sdk → (website, cli). The SDK is the single source of truth for the public API contract. The CLI is a thin wrapper. The website's homepage hero and this docs page are both backed by the same SDK exports. ## Links Site https://deploy.me Dashboard https://deploy.me/dashboard Docs (HTML) https://deploy.me/docs Docs (this) https://deploy.me/docs/llms.txt GitHub https://github.com/lambda-run/deploy-me-packages Security mailto:security@deploy.me