Skip to main content

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​

NeedEdge (Worker)Core (Docker)
Fast cold starts (under 50ms)YesNo
Global low latencyYesRegional
Long-running processes (over 30s CPU)NoYes
Filesystem accessNoYes
Heavy computation / ML inferenceNoYes
Persistent storageKV / R2 / D1Local disk + volumes
Multiple processesNoYes
Native binaries (puppeteer, ffmpeg)LimitedYes

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:

  1. Reads your Dockerfile from the connected GitHub repository
  2. Builds the image and pushes it to the platform's registry
  3. Deploys to Fly.io as a container with resource allocations based on the buyer's plan
  4. Binds buyer secrets as environment variables
  5. Returns a URL that routes traffic to the container

What the container receives​

Your container runs with these environment variables injected at runtime:

VariableDescription
AD_DEPLOYMENT_IDUnique ID for this deployment
AD_USER_IDThe buyer's user ID
AD_TEMPLATE_IDYour template ID
AD_MODELLLM model the buyer selected (e.g. openai/gpt-4o-mini)
AD_REGIONDeployment region
AD_GATEWAY_BASE_URLPlatform gateway URL for LLM calls
PORTThe 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:

KeyDisplay NameTypeRequired
LLM_API_KEYLLM API Keyapi_keytrue
DATABASE_URLDatabase URLdb_connectionfalse

The platform injects these as environment variables into the container at runtime.


Container resource limits​

Resource allocation depends on the buyer's plan:

PlanCPUMemoryDisk
Starter1 vCPU512 MB1 GB
Pro2 vCPU1 GB5 GB
Business4 vCPU4 GB20 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 --from=builder /app/agent .
EXPOSE 8080
CMD ["./agent"]

Template configuration​

When creating a core template:

FieldValue
Deploy Runtimecore
Hosting Compatibletrue
Secret: LLM_API_KEYType: 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!"}'