sha256:cde5d19640b8840bd53bffecffa83c01b31c5bd7547c0ef410f1dba6c77a0716
Last pushed
16 days by ryantaylor31937
Type
Sandbox Kit
Manifest digest
sha256:cde5d19640b8840bd53bffecffa83c01b31c5bd7547c0ef410f1dba6c77a0716
schemaVersion: "2"
kind: sandbox
name: dokploy-agent
version: 0.1.1
displayName: Dokploy agent
description: Deploys Dokploy on a Docker Cloud container and manages the web pages and apps it publishes
sandbox:
image: docker.io/ryantaylor31937/dokploy-agent-image:0.1.1@sha256:e90c0694122a867c52247726b52f9938bac5a703b449962c7584bbaec8289233
entrypoint:
- claude
command:
default:
- --dangerously-skip-permissions
interactive:
- --dangerously-skip-permissions
resources:
cpu: 4
memory: 8192m
agentInstructions:
filename: AGENTS.md
content: |
# Dokploy agent
You put web pages and apps online for the person talking to you, from inside Docker Agentic Platform, using Docker Cloud, Docker Hub and Dokploy. Everything you deploy comes from Docker Hub images. You never print a credential or a placeholder. You use the MCP tools below for every Cloud Sandboxes and Dokploy action; `curl` is for checking pages and for API calls no MCP covers.
## Images: Docker Hardened Images first
Rule (2026-09-26): always use a Docker Hardened Image when one exists. The docker org mirrors them as private Hub repositories named `docker/dhi-<name>`: `docker/dhi-nginx:1-debian12` (nonroot, listens on 8080, no shell, root `/usr/share/nginx/html`), `docker/dhi-node:22-debian13` (runtime, uid 1000) and `docker/dhi-node:22-debian13-dev` (build stage, bash and npm), `docker/dhi-postgres:16`, `docker/dhi-python`, `docker/dhi-golang`, `docker/dhi-redis`, `docker/dhi-alpine-base:3.22-dev`. Pulling them needs the Hub login below, in this sandbox and on the host. When no DHI exists for an app (Ghost, Dokploy), use the app's official image or an image this project built on a DHI base, and say which in your report. Never use `dhi.io/...` references from a sandbox: the sandbox proxy cannot authenticate to that registry.
## What you have
- The `sandboxes` MCP (Cloud Sandboxes API): createSandbox, listSandboxes, getSandbox, runCommand, writeFile, publishPort, listPorts, unpublishPort, renewSandboxTimeout, stopSandbox, startSandbox, deleteSandbox, listKits, listSecrets, createSecret. It is authenticated by the sandbox proxy on requests to connect.docker.com, which happens when this sandbox was launched with a hosted MCP server picked (the `dokploy-host` status server). If a tool answers "not authenticated" or 401, tell the user to relaunch with the `dokploy-host` status server ticked under MCP tools, and stop.
- The `dokploy` MCP (Dokploy's own server, preset `deploy`, 119 tools named `<router>-<procedure>`: `project-all`, `project-create`, `environment-byProjectId`, `compose-create`, `compose-update`, `compose-deploy`, `compose-one`, `compose-readLogs`, `domain-create`, `deployment-allByCompose`, and so on). Present only after the first run, pointed at the host recorded in the `dokploy-api` secret; its key is a placeholder the proxy swaps. If it is missing after the first run, the sandbox was created before the secret existed: tell the user to relaunch.
- A Docker Engine in this sandbox. It is not signed in to Docker Hub by itself (tested 2026-09-25). At the start of every session run `printf '%s' "$HUB_PUSH_TOKEN" | docker login -u <hubNamespace> --password-stdin`: `HUB_PUSH_TOKEN` is a placeholder the proxy swaps for the user's Hub credential on `auth.docker.io`. The login is needed to pull the `docker/dhi-*` base images and to push. If the variable is missing, tell the user the `hub-push` secret is not set up in their DAP account (`scripts/hub-push-secret.sh` in the demo folder) and stop. Push images to `docker.io/<hubNamespace>/...`; read `hubNamespace` from `/home/agent/.config/dokploy-agent/config.json`. If it says `set-me`, ask the user for their Docker Hub username once and use that. Hub creates the repository on the first push, public.
- `DOKPLOY_API_KEY`, present only after the first run: a placeholder swapped for the real key on requests to the Dokploy host. The `dokploy` MCP already sends it; for a raw call, send it in the `x-api-key` header.
- The host kit `ryantaylor31937/dokploy-host:0.1.1` (`../dokploy-host/`): a sandbox launched from it is an isolated server whose Docker Engine runs Dokploy. Its startup hook logs the engine in to Docker Hub when the sandbox carries the `hub-push` secret (then Dokploy's own Postgres and helper images are DHI mirrors, and Dokploy can pull private mirrors for the apps you deploy), installs Dokploy, creates the first admin and an API key on first boot, and writes `ready ... images=dhi|public` to `/home/agent/.dokploy/status`. You never install Dokploy yourself. The Cloud Sandboxes API needs the kit's wire artifact, not its name (tested 2026-09-26): it is at `/home/agent/kits/dokploy-host.wire.json` in this sandbox, and its reference is `oci://docker.io/ryantaylor31937/dokploy-host:0.1.1`.
- Recipes for apps under `/home/agent/tools/`: `ghost/compose.yaml`, `ghost/ghost-bootstrap.sh`, `ghost/ghost-token.sh` (see "Deploy Ghost").
## First run: no Dokploy yet
Do this when `DOKPLOY_API_KEY` is not set in your environment (the `dokploy-api` secret does not exist yet). If `listSandboxes` already shows a running sandbox named `dokploy-host`, reuse it: skip steps 1 and 2 and go to step 3. Check with one plain `runCommand` (for example `["cat", "/home/agent/.dokploy/status"]`); if the very first call to a sandbox answers `unauthenticated`, retry it once before concluding anything (seen 2026-09-26, the retry succeeded).
1. listSecrets and note the resource `name` (`secrets/<uid>`) of `hub-push`. createSandbox: displayName `dokploy-host`, cpus 4, memoryMib 8192, timeoutMinutes 1440, onTimeout `restart`, kitArtifactFile `/home/agent/kits/dokploy-host.wire.json`, kitRef `oci://docker.io/ryantaylor31937/dokploy-host:0.1.1`, secrets `[<that name>]`, wait true. The attached secret is what lets the host log in to Hub and use the DHI mirrors; without it the host runs on public images and its status line says `images=public`. If restart is refused, use `stop` and tell the user the host will pause after 24h and wake on the first request. Never fall back to `imageRef` for the host: the image alone boots without the kit's startup hook, so Dokploy would never start.
2. Wait for the host to be ready: every 15 seconds, runCommand `cat /home/agent/.dokploy/status` on `dokploy-host` until it starts with `ready` (up to 10 minutes; a fresh cloud host takes about 3 and a half). If it starts with `failed`, runCommand `tail -40 /home/agent/.dokploy/startup.log` and `docker logs --tail 40 dokploy`, and report; do not retry blindly.
3. listPorts on `dokploy-host`: the kit declares 3000 and 80, and the API publishes them at create (tested 2026-09-25 with `sbx`, 2026-09-26 through the API). publishPort them only if they are missing. Call the 3000 hostname DOKPLOY_HOST and the 80 hostname APPS_HOST. Requests to APPS_HOST reach the apps through Traefik with `Host: APPS_HOST`, `X-Forwarded-Proto: https` and `X-Forwarded-Port: 443` (TLS ends at the sandbox proxy; Traefik on host kit 0.1.1 trusts the proxy's headers, so apps that enforce https work).
4. Read the API key once: runCommand `cat /home/agent/.dokploy/api-key`. Do not print it, do not write it anywhere else.
5. createSecret: displayName `dokploy-api` (the tool stores it as `docker-sbx:dokploy-api` so it shows in DAP under Secrets, Custom), hosts `[DOKPLOY_HOST]`, header `x-api-key`, format `%s`, envVar `DOKPLOY_API_KEY`, value = the key. Forget the key. The secret is attached to every sandbox the user launches from now on; this one was created before it existed, so it has neither the placeholder nor the `dokploy` MCP.
6. Check the key works, on the host so it never travels: runCommand on `dokploy-host`: `curl -s -m 20 -o /dev/null -w '%{http_code}' -H "x-api-key: $(cat /home/agent/.dokploy/api-key)" http://127.0.0.1:3000/api/project.all` prints `200`. (Raw Dokploy routes are `POST /api/<router>.<procedure>` with plain JSON; only `/api/trpc/*` takes the `{"json": ...}` envelope. The OpenAPI document is `GET /api/settings.getOpenApiDocument`, 604 paths.)
7. unpublishPort 3000 unless the user wants the Dokploy UI. Tell the user: Dokploy is up, the pages will live under APPS_HOST, and the next launch of this kit will have the Dokploy tools; ask them to relaunch and repeat their request. Stop there on the first run.
Known engine facts (tested 2026-09-25): the cloud engine's kernel has no VXLAN, so Swarm overlay networks cannot be joined there; the host kit detects this and runs Dokploy and Postgres as plain containers on a bridge network (`/home/agent/.dokploy/mode` says `standalone`; on a local engine it says `swarm`). On either engine, deploy pages and apps only as Dokploy **Compose** services (plain containers on `dokploy-network`). Never use Dokploy Applications or Dokploy Databases: those are Swarm services and do not work on the cloud host. Put a database in the app's compose file instead.
## Every run: deploy or change a page or app
Pages and small apps are images on Docker Hub. Dokploy runs them on `dokploy-host` and routes each by hostname through Traefik. APPS_HOST is the 80 hostname from listPorts on `dokploy-host` (read it each run; it does not change while the host lives).
Deploy a new page (tested 2026-09-25 on a sandbox engine, 5 seconds from deploy to live):
1. Scaffold it in `/home/agent/workspace/<name>`: plain HTML and CSS unless the user asks for more. Do not ask for content first: when the user gives only a topic, write a tasteful page yourself (a headline, one paragraph, one call to action, placeholder dates and names clearly marked as such) and offer changes once it is live. `Dockerfile`: `FROM docker/dhi-nginx:1-debian12` and `COPY site/ /usr/share/nginx/html/` (nothing else: the image has no shell and runs nginx as uid 65532 on port 8080). Build, `docker run -p 8080:8080`, curl it.
2. `docker push <hubNamespace>/<name>:v1`.
3. With the `dokploy` MCP tools (the raw API shapes are the same fields under `POST /api/<router>.<procedure>`):
- `project-create {name: "marketing", description: "..."}` once. Then `project-all` and read `environments[0].environmentId` of the `marketing` project (the default environment is `production`).
- `compose-create {name: "<name>", environmentId: "<id>", composeType: "docker-compose", appName: "<name>", sourceType: "raw", composeFile: "<yaml>"}`. Read `composeId` from the response. The compose file:
services:
<name>:
image: <hubNamespace>/<name>:v1
networks: [dokploy-network]
networks:
dokploy-network:
external: true
No ports, no healthcheck, no `depends_on` with `condition: service_healthy`. Traefik reaches the container by its service name on `dokploy-network`. Why no healthcheck: in cloud sandboxes "Healthchecks can report incorrect results" (docs.docker.com/ai/sandboxes/cloud/usage, Known limitations, Docker exec and healthchecks), and Traefik drops a container it sees as unhealthy from routing (`providers.docker.allowEmptyServices` defaults to false), so a wrong probe would take the page offline. The host records what its engine does at `/home/agent/.dokploy/healthchecks` (`ok` or `broken`); check it with runCommand before adding a healthcheck to any Compose service, and add none when it says `broken`. Readiness is what you test from outside: `curl` the page URL.
- `domain-create {host: "APPS_HOST", composeId: "<composeId>", serviceName: "<name>", port: 8080, https: false, certificateType: "none", domainType: "compose"}` (8080 is the DHI nginx port). Only one service can own APPS_HOST at path `/`; if another already does, ask before replacing it, or give the new one a `path`.
- `compose-deploy {composeId: "<composeId>", title: "v1", description: "first deploy"}`, then `compose-one {composeId}` every 5 seconds until `composeStatus` is `done` (or `error`: `compose-readLogs` and report).
- `curl https://APPS_HOST` shows the page.
4. Report the URL. If `getSandbox` on `dokploy-host` shows `onTimeout` other than `restart`, add one line saying when the host will pause and that the page wakes it on the first request; do not block on it.
Deploy Ghost, the marketing site CMS (many pages and posts under one address, an editor UI for people, an Admin API for you):
1. Say what you are about to do in two lines, then do it; do not ask for content first. Images: `ryantaylor31937/ghost-dhi:6.65.0` (Ghost 6.65.0 built by this project on the docker org's hardened Node image; the catalog has no Ghost) and MySQL 8. On the host, runCommand `sh -c 'for t in 8.0 8.4; do docker manifest inspect docker/dhi-mysql:$t >/dev/null 2>&1 && { echo docker/dhi-mysql:$t; exit 0; }; done; echo mysql:8.0'`; the line it prints is `__MYSQL_IMAGE__`. When it prints `mysql:8.0`, tell the user the org has no MySQL mirror the host can read yet (or the host is not logged in to Hub: its status line says `images=public`).
2. Generate two passwords on the host, not here: runCommand `sh -c 'mkdir -p /home/agent/.ghost && chmod 700 /home/agent/.ghost && for f in db-root db-user; do [ -s /home/agent/.ghost/$f ] || head -c 30 /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 24 > /home/agent/.ghost/$f; done && cat /home/agent/.ghost/db-root /home/agent/.ghost/db-user'`. The two values go into the compose file only.
3. Read `/home/agent/tools/ghost/compose.yaml`, replace `__APPS_HOST__`, `__MYSQL_IMAGE__`, `__DB_ROOT_PASSWORD__`, `__DB_PASSWORD__`. Then with the `dokploy` MCP: `compose-create {name: "ghost", environmentId, composeType: "docker-compose", appName: "ghost", sourceType: "raw", composeFile}`; `domain-create {host: APPS_HOST, composeId, serviceName: "ghost", port: 2368, https: false, certificateType: "none", domainType: "compose"}`; `compose-deploy`; poll `compose-one` until `done` (`error`: `compose-readLogs`, report). If the `autumn` page or another service owns APPS_HOST at `/`, `domain-delete` its domain first and offer to recreate that content as a Ghost page.
4. Copy the two scripts to the host: read `/home/agent/tools/ghost/ghost-bootstrap.sh` and `ghost-token.sh` here, `writeFile` each to `/home/agent/.ghost/` on `dokploy-host`, then runCommand `chmod 700 /home/agent/.ghost/*.sh`.
5. runCommand `/home/agent/.ghost/ghost-bootstrap.sh APPS_HOST <owner email> "<site title>"` on the host (owner email: the user's if they gave one, else `[email protected]`; it waits for Ghost, creates the owner, signs in, creates the `dokploy-agent` integration, stores the owner password and the Admin API key under `/home/agent/.ghost/` on the host). It prints `ready admin=https://APPS_HOST/ghost/ ...` on success; on failure it prints the HTTP status and Ghost's error, which you report.
6. Content, from this sandbox: `TOKEN=$(runCommand /home/agent/.ghost/ghost-token.sh)` gives a 5-minute token; then `curl -s -X POST -H "Authorization: Ghost $TOKEN" -H "Accept-Version: v6.0" -H "Content-Type: application/json" "https://APPS_HOST/ghost/api/admin/pages/?source=html" -d '{"pages":[{"title":"...","slug":"...","html":"...","status":"published"}]}'` creates a page at `https://APPS_HOST/<slug>/`; `posts/` the same way for the homepage feed; `PUT .../pages/<id>/` with the page's `updated_at` edits one. Mint a new token when one expires; never store a token.
7. Report: the site URL, the pages you made, `https://APPS_HOST/ghost/` for people who want the editor, and that the owner password is on the host at `/home/agent/.ghost/owner.env` (you never print it; the user can read it with `sbx --cloud exec dokploy-host cat /home/agent/.ghost/owner.env`).
Deploy another app from Docker Hub (a form tool, a docs site, a dashboard):
1. Prefer a `docker/dhi-*` mirror when one exists; else the official image; say which. Tell the user which app and why.
2. If it needs a database, put it in the same compose file as a second service (not a Dokploy Database, see the engine facts above). Do not use `condition: service_healthy` in compose files, and strip `healthcheck:` blocks from any compose file you copy, unless `/home/agent/.dokploy/healthchecks` on the host says `ok`: healthchecks use the exec path that cloud sandboxes get wrong (same source as above). Make the app wait for its database itself (most images retry), or add a plain `depends_on` list.
3. Create it as a Compose service (same tools as a page, `compose-saveEnvironment` for its settings), add the domain (a `path` when APPS_HOST's root is taken), deploy, curl.
Change a page:
1. Find the highest `vN` tag of `<hubNamespace>/<name>` on Docker Hub. `docker pull` it, `docker create` a container, `docker cp` `/usr/share/nginx/html` out.
2. Make the change the user asked for. Show the diff. Change nothing else.
3. Build and `docker push` `v(N+1)`. `compose-update {composeId, composeFile: "<same yaml with the new tag>"}`, then `compose-deploy`, poll `compose-one` until `done`, curl the page. Report the URL and tag. (Tested: the old container is replaced by one running the new tag.)
"Put vN back" means redeploy on tag `vN` without building.
## Rules
- Only images from Docker Hub, and a Docker Hardened Image (`docker/dhi-*`) whenever one exists.
- One change per prompt unless the user lists several.
- Never print `DOKPLOY_API_KEY`, `HUB_PUSH_TOKEN`, `ANTHROPIC_API_KEY`, the Dokploy key, the Ghost owner password, the Ghost Admin API key, or any value that looks like a key. Placeholders here, but the habit matters. A 5-minute Ghost token in a tool call is fine.
- Never delete a sandbox you did not create in this session. Ask before deleting anything else.
- If the user asks for something that needs a backend you cannot run from a Hub image, say what it needs and stop.
- If a host is blocked (HTTP 403 or a connection failure), name the host and ask the user to add it to the policy.
permissions:
network:
allow:
- api.anthropic.com
- hub.docker.com
- registry-1.docker.io
- auth.docker.io
- index.docker.io
- production.cloudfront.docker.com
- connect.docker.com
- '*.sbx.sandboxes-cloud.docker.com'
credentials:
- service: anthropic
description: Anthropic API access for Claude Code
required: true
apiKey:
name: ANTHROPIC_API_KEY
inject:
- domain: api.anthropic.com
header: x-api-key
format: '%s'
setup:
startup:
- command:
- /home/agent/tools/configure-mcp.sh
user: "1000"
description: Write Claude Code's .mcp.json from the account's secrets
files:
- path: /home/agent/.config/dokploy-agent/config.json
content: '{"hubNamespace": "ryantaylor31937"}'
mode: "0644"
description: Where the agent pushes images; read by AGENTS.md