Building Core Agents (Docker)
Core agents run as Docker containers — they are suited for workloads that need persistent processes, filesystem access, heavy computation, or native libraries that don't fit in a Cloudflare Worker.
When to choose core over edge
| Need | Edge (Worker) | Core (Docker) |
|---|---|---|
| Fast cold starts (under 50ms) | Yes | No |
| Global low latency | Yes | Regional |
| Long-running processes (over 30s CPU) | No | Yes |
| Filesystem access | No | Yes |
| Heavy computation / ML inference | No | Yes |
| Persistent storage | KV / R2 / D1 | Local disk + volumes |
| Multiple processes | No | Yes |
| Native binaries (puppeteer, ffmpeg) | Limited | Yes |
If your agent needs any of the "Core only" capabilities, build a core agent.
How core agents work
When a buyer deploys a core agent, the platform:
- Reads your Dockerfile from the connected GitHub repository
- Builds the image and pushes it to the platform's registry
- Deploys to Fly.io as a container with resource allocations based on the buyer's plan
- Binds buyer secrets as environment variables
- Returns a URL that routes traffic to the container
What the container receives
Your container runs with these environment variables injected at runtime:
| Variable | Description |
|---|---|
AD_DEPLOYMENT_ID | Unique ID for this deployment |
AD_USER_ID | The buyer's user ID |
AD_TEMPLATE_ID | Your template ID |
AD_MODEL | LLM model the buyer selected (e.g. openai/gpt-4o-mini) |
AD_REGION | Deployment region |
AD_GATEWAY_BASE_URL | Platform gateway URL for LLM calls |
PORT | The port your container must listen on |
Plus any buyer-configured secrets from your manifest (e.g. LLM_API_KEY, DATABASE_URL).
Repository structure
Your GitHub repo needs these files at minimum:
my-core-agent/
├── Dockerfile # Required — defines the container
├── app.py # Your agent code (any language)
├── requirements.txt # Dependencies (Python example)
└── .env.example # Optional — auto-generates secret manifest
Full example: Python RAG agent
Dockerfile
FROM python:3.12-slim
WORKDIR /app
# Install dependencies first (better layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy agent code
COPY app.py .
# The platform injects PORT at runtime
ENV PORT=8080
EXPOSE 8080
# Start the agent
CMD ["python", "app.py"]
app.py
import os
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.request
AD_DEPLOYMENT_ID = os.environ.get("AD_DEPLOYMENT_ID", "")
AD_MODEL = os.environ.get("AD_MODEL", "openai/gpt-4o-mini")
AD_GATEWAY_BASE_URL = os.environ.get("AD_GATEWAY_BASE_URL", "")
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
PORT = int(os.environ.get("PORT", 8080))
class AgentHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
self._json({"ok": True, "agent": "rag-agent"})
return
if self.path == "/_provision":
required = ["LLM_API_KEY"]
configured = [k for k in required if os.environ.get(k)]
self._json({
"status": "ok",
"provisioned": len(configured) == len(required),
"secrets": {
"configured": len(configured),
"expected": len(required),
"required": len(required),
"missing": [k for k in required if not os.environ.get(k)],
},
})
return
self._text("RAG Agent Active. Use POST /ask to interact.")
def do_POST(self):
if self.path == "/ask":
content_length = int(self.headers["Content-Length"])
body = json.loads(self.rfile.read(content_length))
question = body.get("question", "")
answer = self._call_llm(question)
self._json({"answer": answer, "model": AD_MODEL})
return
self._json({"error": "Not found"}, 404)
def _call_llm(self, question):
"""Route LLM calls through the AgentDeploy gateway."""
url = f"{AD_GATEWAY_BASE_URL}/v1/chat/completions"
payload = json.dumps({
"model": AD_MODEL,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": question},
],
}).encode()
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Authorization", f"Bearer {LLM_API_KEY}")
req.add_header("Content-Type", "application/json")
req.add_header("X-AD-Deployment", AD_DEPLOYMENT_ID)
with urllib.request.urlopen(req) as response:
data = json.loads(response.read())
return data["choices"][0]["message"]["content"]
def _json(self, obj, status=200):
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(obj).encode())
def _text(self, msg, status=200):
self.send_response(status)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(msg.encode())
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), AgentHandler)
print(f"Core agent listening on port {PORT}")
server.serve_forever()
requirements.txt
# No external deps needed — uses stdlib only
# Add your own (langchain, chromadb, etc.) as needed
Required endpoints
Just like edge agents, core agents must implement these HTTP endpoints:
GET /health
Return a 200 with a JSON body. The platform uses this to check if the container is alive.
GET /_provision
Report which secrets are configured. The platform uses this to determine deployment readiness.
{
"status": "ok",
"provisioned": true,
"secrets": {
"configured": 1,
"expected": 1,
"required": 1,
"missing": []
}
}
If provisioned is false, the deployment stays in provisioning status until the buyer configures all required secrets.
LLM gateway usage
Core agents use the same gateway as edge agents. Route LLM calls through the gateway for token metering and billing:
# Python example
import urllib.request
import json
def call_gateway(messages, deployment_id, api_key):
url = f"{AD_GATEWAY_BASE_URL}/v1/chat/completions"
payload = json.dumps({
"model": AD_MODEL,
"messages": messages,
}).encode()
req = urllib.request.Request(url, data=payload, method="POST")
req.add_header("Authorization", f"Bearer {api_key}")
req.add_header("Content-Type", "application/json")
req.add_header("X-AD-Deployment", deployment_id)
with urllib.request.urlopen(req) as response:
return json.loads(response.read())
// Node.js example
const response = await fetch(`${process.env.AD_GATEWAY_BASE_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LLM_API_KEY}`,
'Content-Type': 'application/json',
'X-AD-Deployment': process.env.AD_DEPLOYMENT_ID,
},
body: JSON.stringify({
model: process.env.AD_MODEL,
messages: [{ role: 'user', content: 'Hello' }],
}),
});
const data = await response.json();
The gateway is OpenAI-compatible — the model string determines which backend provider handles the request (e.g. openai/gpt-4o-mini, anthropic/claude-3-sonnet).
Secret manifest
Declare the same secrets your container reads from environment variables:
| Key | Display Name | Type | Required |
|---|---|---|---|
LLM_API_KEY | LLM API Key | api_key | true |
DATABASE_URL | Database URL | db_connection | false |
The platform injects these as environment variables into the container at runtime.
Container resource limits
Resource allocation depends on the buyer's plan:
| Plan | CPU | Memory | Disk |
|---|---|---|---|
| Starter | 1 vCPU | 512 MB | 1 GB |
| Pro | 2 vCPU | 1 GB | 5 GB |
| Business | 4 vCPU | 4 GB | 20 GB |
Your container should handle graceful degradation when resources are constrained.
Language examples
Core agents can be written in any language that runs in Docker. Common choices:
Node.js
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
// server.js
const http = require('http');
const PORT = process.env.PORT || 8080;
const server = http.createServer(async (req, res) => {
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
if (req.url === '/_provision') {
const required = ['LLM_API_KEY'];
const configured = required.filter(k => process.env[k]);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
provisioned: configured.length === required.length,
secrets: {
configured: configured.length,
expected: required.length,
required: required.length,
missing: required.filter(k => !process.env[k]),
},
}));
return;
}
// Your agent logic here
res.writeHead(200);
res.end('Core Agent Active');
});
server.listen(PORT, () => console.log(`Listening on ${PORT}`));
Go
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o agent .
FROM alpine:3.20
WORKDIR /app
COPY /app/agent .
EXPOSE 8080
CMD ["./agent"]
Template configuration
When creating a core template:
| Field | Value |
|---|---|
| Deploy Runtime | core |
| Hosting Compatible | true |
Secret: LLM_API_KEY | Type: api_key, Required: true |
If your agent supports both edge and core runtimes, set Deploy Runtime to both and publish separate edge and core versions. Buyers choose at deploy time.
Security scanning
Core agents go through the same 5-scanner security audit as edge agents. The scanners analyze your Dockerfile, source code, and dependency manifests:
- gitleaks scans Dockerfiles and source for hardcoded secrets
- semgrep runs SAST on your source code
- osv-scanner checks dependencies in
requirements.txt,package.json,go.mod, etc. - LLM scanner checks prompts for injection attacks
- Permission analyzer reviews network patterns and API scopes
You need a C rating (50+) or higher to publish. See Scanning Process for details.
Testing locally
# Build the image
docker build -t my-core-agent .
# Run with test secrets
docker run -p 8080:8080 \
-e LLM_API_KEY=your-test-key \
-e AD_GATEWAY_BASE_URL=https://gateway.agentdeploy.io \
-e AD_DEPLOYMENT_ID=test-deployment \
-e AD_MODEL=openai/gpt-4o-mini \
my-core-agent
# Test endpoints
curl http://localhost:8080/health
curl http://localhost:8080/_provision
curl -X POST http://localhost:8080/ask \
-H "Content-Type: application/json" \
-d '{"question": "Hello!"}'