Production Deployment Tutorial
Learn how to deploy AgentForge applications to production with Docker, Kubernetes, and cloud platforms.
Overview
This tutorial covers:
- Environment Setup - Configuration and secrets management
- Docker Deployment - Containerization and Docker Compose
- Kubernetes Deployment - Production-ready K8s manifests
- Cloud Platforms - AWS, GCP, and Azure deployment
- Monitoring & Observability - Health checks and metrics
- CI/CD - Automated deployment pipelines
Prerequisites
- Completed Your First Agent
- Basic Docker knowledge
- Access to a cloud platform (optional)
- Node.js 20+ installed
Step 1: Prepare Your Application
Project Structure
Ensure your project follows this structure:
my-agent/
├── src/
│ ├── index.ts # Application entry point
│ ├── agent.ts # Agent definition
│ └── tools/ # Custom tools
├── package.json
├── tsconfig.json
├── Dockerfile
└── .dockerignoreEnvironment Configuration
Create environment-specific configuration:
typescript
// src/config.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.string().default('3000'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
// LLM Configuration
OPENAI_API_KEY: z.string(),
OPENAI_MODEL: z.string().default('gpt-4'),
// Database (optional)
DATABASE_URL: z.string().optional(),
// Redis (optional)
REDIS_URL: z.string().optional(),
// Monitoring
LANGSMITH_API_KEY: z.string().optional(),
LANGSMITH_PROJECT: z.string().optional(),
});
export const config = envSchema.parse(process.env);Production-Ready Agent
Configure your agent for production:
typescript
// src/agent.ts
import { createReActAgent } from '@agentforge/patterns';
import { ChatOpenAI } from '@langchain/openai';
import { productionPreset } from '@agentforge/core/middleware';
import { config } from './config';
// Create LLM with production settings
const llm = new ChatOpenAI({
modelName: config.OPENAI_MODEL,
temperature: 0,
maxRetries: 3,
timeout: 30000,
});
// Create agent
const baseAgent = createReActAgent({
llm,
tools: [...yourTools],
maxIterations: 10,
});
// Apply production middleware
export const agent = productionPreset(baseAgent, {
nodeName: 'main-agent',
cache: {
ttl: 3600000, // 1 hour
maxSize: 1000,
},
rateLimit: {
maxRequests: 100,
windowMs: 60000, // 1 minute
},
retry: {
maxAttempts: 3,
backoff: 'exponential',
},
timeout: {
timeout: 30000, // 30 seconds
},
logging: {
level: config.LOG_LEVEL,
},
metrics: {
enabled: true,
},
});Health Check Endpoint
Add health checks for monitoring:
typescript
// src/health.ts
import { createHealthChecker } from '@agentforge/core/monitoring';
export const healthChecker = createHealthChecker({
name: 'agent-service',
checks: [
{
name: 'llm',
check: async () => {
// Test LLM connection
await llm.invoke('test');
return { status: 'healthy' };
},
},
{
name: 'database',
check: async () => {
// Test database connection (if applicable)
return { status: 'healthy' };
},
},
],
});
// Express.js example
app.get('/health/live', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/health/ready', async (req, res) => {
const health = await healthChecker.check();
res.status(health.healthy ? 200 : 503).json(health);
});Application Entry Point
typescript
// src/index.ts
import express from 'express';
import { agent } from './agent';
import { healthChecker } from './health';
import { config } from './config';
const app = express();
app.use(express.json());
// Health endpoints
app.get('/health/live', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/health/ready', async (req, res) => {
const health = await healthChecker.check();
res.status(health.healthy ? 200 : 503).json(health);
});
// Agent endpoint
app.post('/agent/invoke', async (req, res) => {
try {
const { input } = req.body;
const result = await agent.invoke({ messages: [{ role: 'user', content: input }] });
res.json({ result });
} catch (error) {
console.error('Agent error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
const port = parseInt(config.PORT);
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});Step 2: Docker Deployment
Create Dockerfile
Create a multi-stage Dockerfile for optimal image size:
dockerfile
# Dockerfile
# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Stage 2: Production
FROM node:20-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Set ownership
RUN chown -R nodejs:nodejs /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --production --ignore-scripts && \
npm cache clean --force
# Copy built application from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:${PORT:-3000}/health/live', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Start the application
CMD ["node", "dist/index.js"]Create .dockerignore
# .dockerignore
node_modules
dist
.git
.env
.env.*
*.log
coverage
.vscode
.idea
README.mdDocker Compose for Local Testing
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PORT=3000
- OPENAI_API_KEY=${OPENAI_API_KEY}
- DATABASE_URL=postgresql://user:password@postgres:5432/agentforge
- REDIS_URL=redis://redis:6379
- LOG_LEVEL=info
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health/live', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=agentforge
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
redis-data:Build and Run
bash
# Build the image
docker build -t my-agent:latest .
# Run with Docker Compose
docker-compose up -d
# View logs
docker-compose logs -f app
# Test the endpoint
curl -X POST http://localhost:3000/agent/invoke \
-H "Content-Type: application/json" \
-d '{"input": "Hello, agent!"}'
# Stop services
docker-compose downStep 3: Kubernetes Deployment
Create Kubernetes Manifests
Create a k8s/ directory with the following files:
Deployment
yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-app
labels:
app: agent
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: agent
template:
metadata:
labels:
app: agent
spec:
containers:
- name: app
image: my-agent:latest
ports:
- name: http
containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: agent-secrets
key: openai-api-key
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 10Service
yaml
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: agent-service
spec:
type: LoadBalancer
selector:
app: agent
ports:
- port: 80
targetPort: httpConfigMap & Secret
yaml
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: agent-config
data:
log-level: "info"yaml
# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: agent-secrets
type: Opaque
stringData:
openai-api-key: "your-api-key-here"Deploy to Kubernetes
bash
# Apply manifests
kubectl apply -f k8s/
# Verify deployment
kubectl get pods
kubectl get svc
# View logs
kubectl logs -f deployment/agent-app
# Test the service
kubectl port-forward svc/agent-service 8080:80
curl -X POST http://localhost:8080/agent/invoke \
-H "Content-Type: application/json" \
-d '{"input": "Hello!"}'Step 4: CI/CD Pipeline
GitHub Actions
Create .github/workflows/deploy.yml:
yaml
name: Deploy
on:
push:
branches: [main]
release:
types: [published]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/agent-app \
app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
kubectl rollout status deployment/agent-appStep 5: Cloud Platform Deployment
AWS ECS
bash
# Create ECR repository
aws ecr create-repository --repository-name my-agent
# Build and push image
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
docker build -t my-agent .
docker tag my-agent:latest ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-agent:latest
docker push ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/my-agent:latest
# Create ECS task definition and service (use AWS Console or CLI)Google Cloud Run
bash
# Build and push to GCR
gcloud builds submit --tag gcr.io/PROJECT_ID/my-agent
# Deploy to Cloud Run
gcloud run deploy my-agent \
--image gcr.io/PROJECT_ID/my-agent \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars NODE_ENV=production \
--set-secrets OPENAI_API_KEY=openai-key:latestAzure Container Instances
bash
# Create container registry
az acr create --resource-group myResourceGroup --name myregistry --sku Basic
# Build and push
az acr build --registry myregistry --image my-agent:latest .
# Deploy
az container create \
--resource-group myResourceGroup \
--name my-agent \
--image myregistry.azurecr.io/my-agent:latest \
--cpu 1 --memory 1 \
--registry-login-server myregistry.azurecr.io \
--registry-username <username> \
--registry-password <password> \
--dns-name-label my-agent \
--ports 3000 \
--environment-variables NODE_ENV=production \
--secure-environment-variables OPENAI_API_KEY=<your-key>Step 6: Monitoring & Observability
Add Prometheus Metrics
typescript
// src/metrics.ts
import { createMetricsCollector } from '@agentforge/core/monitoring';
export const metrics = createMetricsCollector({
prefix: 'agent_',
labels: ['environment', 'version'],
});
// Track agent invocations
metrics.counter('invocations_total', 'Total agent invocations');
metrics.histogram('duration_seconds', 'Agent execution duration');
metrics.gauge('active_requests', 'Active requests');
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', metrics.contentType);
res.end(await metrics.metrics());
});LangSmith Integration
typescript
// src/config.ts
export const config = {
// ... other config
langsmith: {
apiKey: process.env.LANGSMITH_API_KEY,
project: process.env.LANGSMITH_PROJECT || 'production',
tracingEnabled: process.env.NODE_ENV === 'production',
},
};
// src/agent.ts
import { LangSmithTracer } from '@langchain/core/tracers/langsmith';
const tracer = new LangSmithTracer({
projectName: config.langsmith.project,
apiKey: config.langsmith.apiKey,
});
// Use with agent
const result = await agent.invoke(
{ messages: [...] },
{ callbacks: [tracer] }
);Best Practices
1. Environment Variables
typescript
// ✅ Good - use environment variables
const apiKey = process.env.OPENAI_API_KEY;
// ❌ Bad - hardcode secrets
const apiKey = 'sk-...';2. Graceful Shutdown
typescript
// Handle shutdown signals
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
// Close server
server.close(() => {
console.log('Server closed');
});
// Close database connections
await db.close();
// Exit
process.exit(0);
});3. Resource Limits
yaml
# Always set resource limits
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"4. Health Checks
typescript
// Implement both liveness and readiness probes
app.get('/health/live', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/health/ready', async (req, res) => {
const checks = await runHealthChecks();
res.status(checks.healthy ? 200 : 503).json(checks);
});Troubleshooting
Common Issues
Issue: Container crashes on startup
bash
# Check logs
kubectl logs deployment/agent-app
docker logs <container-id>
# Common causes:
# - Missing environment variables
# - Invalid configuration
# - Port already in useIssue: High memory usage
typescript
// Monitor memory
const used = process.memoryUsage();
console.log('Memory usage:', {
rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
});
// Increase memory limits if neededIssue: Slow response times
typescript
// Add request timing
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${duration}ms`);
});
next();
});Next Steps
- Monitoring Guide - Advanced monitoring setup
- Testing Strategies - Test your deployment
- Advanced Patterns - Optimize your agents
- Middleware Guide - Production middleware