Deploying an Agent as a Web service means facing the real Internet — HTTPS encryption, cross-origin requests, WebSocket long connections, CDN acceleration, DDoS protection, and more. Unlike pure API deployment, Web UI deployment also needs to consider front-end asset hosting, SEO, and first-screen loading speed.
This article dives into the complete production-grade Web deployment stack, from reverse proxy configuration to high-availability architecture, performance optimization to security protection.
Launch Web Service: Separate API and WebUI
ADK Go supports deploying API services and WebUI separately, which is the best practice in production:
// Recommended for production: separate API and WebUI
func main() {
ctx := context.Background()
// create Agent
agent, err := createAgent(ctx)
if err != nil {
log.Fatal(err)
}
// API service (for programmatic calls)
apiRuntime, err := agentruntime.New(ctx,
agentruntime.WithAgent(agent),
agentruntime.WithPort(8080),
agentruntime.WithCORS(agentruntime.CORSConfig{
AllowedOrigins: []string{"https://my-agent.example.com"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization", "X-Request-ID"},
AllowCredentials: true,
MaxAge: 86400,
}),
)
if err != nil {
log.Fatal(err)
}
// WebUI service (for browser users)
webUIRuntime, err := agentruntime.New(ctx,
agentruntime.WithAgent(agent),
agentruntime.WithPort(8081),
agentruntime.WithWebUI(agentruntime.WebUIConfig{
StaticPath: "./webui/dist", // front-end build artifacts
IndexPath: "index.html",
APIProxyTarget: "http://localhost:8080", // proxy to API service
}),
)
if err != nil {
log.Fatal(err)
}
// start both services
errCh := make(chan error, 2)
go func() { errCh <- apiRuntime.Serve() }()
go func() { errCh <- webUIRuntime.Serve() }()
if err := <-errCh; err != nil {
log.Fatal(err)
}
}
Advantages of separate deployment:
- Independent scaling: API and WebUI have different load patterns and can be scaled separately.
- Independent maintenance: Front-end updates do not require restarting the API service.
- Security isolation: WebUI can be placed behind a CDN, while the API can be restricted to internal access.
- Technology stack independence: The front-end can be built with Vite/Webpack, decoupled from the back-end.
HTTPS Configuration: The Security Baseline for Production
Use Nginx Reverse Proxy (Recommended)
Nginx as a reverse proxy is the production standard, providing SSL termination, load balancing, static file caching, and more:
# /etc/nginx/sites-available/my-agent
# HTTP redirect to HTTPS
server {
listen 80;
server_name my-agent.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS service
server {
listen 443 ssl http2;
server_name my-agent.example.com;
# SSL certificate configuration
ssl_certificate /etc/ssl/certs/my-agent.example.com.crt;
ssl_certificate_key /etc/ssl/private/my-agent.example.com.key;
# SSL hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS (force HTTPS)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# security response headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# logging
access_log /var/log/nginx/my-agent.access.log;
error_log /var/log/nginx/my-agent.error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
# API proxy (proxy to backend)
location /api/ {
proxy_pass http://localhost:8080/;
proxy_http_version 1.1;
# forward request headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
# timeout config (Agent responses may be slow)
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 300s; # 5 minutes for long inference
# SSE (Server-Sent Events) support
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400;
# error handling
proxy_intercept_errors on;
error_page 502 503 504 /50x.html;
}
# WebSocket support (if real-time communication is needed)
location /ws/ {
proxy_pass http://localhost:8080/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
# WebUI static files
location / {
root /opt/my-agent/webui/dist;
try_files $uri $uri/ /index.html;
# static file cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# do not cache HTML (support front-end routing)
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# error pages
location = /50x.html {
root /usr/share/nginx/html;
internal;
}
# limit request body size (prevent memory issues from large uploads)
client_max_body_size 10m;
# rate limiting (DDoS protection)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
# ... other config
}
}
Use Let’s Encrypt Free Certificates
# install certbot
sudo apt-get install certbot python3-certbot-nginx
# automatically obtain and configure certificate
sudo certbot --nginx -d my-agent.example.com
# test automatic renewal
sudo certbot renew --dry-run
# view certificate info
sudo certbot certificates
Automatic renewal configuration:
Let’s Encrypt certificates are valid for 90 days and need automatic renewal:
# add crontab task
sudo crontab -e
# add this line (renew daily at 3 AM)
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
Use Cloudflare Proxy (Enhanced Security)
If the domain uses Cloudflare DNS, enable its proxy service for extra protection:
- SSL/TLS mode: Set to “Full (strict)”.
- Always Use HTTPS: Enable.
- Security Level: Set as needed.
- Bot Fight Mode: Enable to block malicious crawlers.
- Rate Limiting Rules: Configure API rate limiting.
High Availability: From Single Point to Cluster
Multi-instance + Load Balancing Architecture
┌─────────────┐
│ CDN / │
│ Cloudflare │
└──────┬──────┘
│
┌──────▼──────┐
│ Nginx │
│ (load │
│ balancing) │
│ + SSL term │
└──────┬──────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Runtime │ │ Runtime │ │ Runtime │
│instance 1│ │instance 2│ │instance 3│
│ :8080 │ │ :8081 │ │ :8082 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└────────────────┼────────────────┘
▼
┌─────────────┐
│ Redis │
│ (Session │
│ shared │
│ storage) │
└─────────────┘
Nginx Load Balancing Configuration
upstream runtime_backend {
# weighted round-robin (adjust weights by instance performance)
server 127.0.0.1:8080 weight=3 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8081 weight=3 max_fails=3 fail_timeout=30s;
server 127.0.0.1:8082 weight=3 max_fails=3 fail_timeout=30s;
# health check (requires nginx-plus or third-party module)
# health_check interval=5s fails=3 passes=2;
# connection pool config
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 1000;
}
server {
location /api/ {
proxy_pass http://runtime_backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
# enable connection reuse
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# timeout config
proxy_connect_timeout 30s;
proxy_send_timeout 60s;
proxy_read_timeout 300s;
}
}
Session Sharing: Redis Cluster Configuration
Multi-instance deployment must solve Session sharing; Redis is the most common solution:
import (
"github.com/redis/go-redis/v9"
)
func createRedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // password is required in production
DB: 0,
// connection pool config
PoolSize: 100, // connection pool size
MinIdleConns: 10, // min idle connections
MaxRetries: 3, // max retries
DialTimeout: 5 * time.Second, // connection timeout
ReadTimeout: 3 * time.Second, // read timeout
WriteTimeout: 3 * time.Second, // write timeout
PoolTimeout: 4 * time.Second, // timeout acquiring connection from pool
// health check
ConnMaxIdleTime: 30 * time.Minute,
ConnMaxLifetime: 1 * time.Hour,
})
}
// Redis Cluster for production
func createRedisCluster() *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
"redis-node-1:6379",
"redis-node-2:6379",
"redis-node-3:6379",
},
Password: "your-strong-password",
// cluster config
PoolSize: 100,
MinIdleConns: 10,
MaxRetries: 3,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
// routing config
RouteRandomly: true, // route reads randomly to replicas
RouteByLatency: false,
})
}
// Runtime config with Redis Session store
runtime, err := agentruntime.New(ctx,
agentruntime.WithAgent(agent),
agentruntime.WithRedisStore(redisClient),
agentruntime.WithSessionSerializer(agentruntime.JSONSerializer), // JSON serialization
)
Session serialization performance comparison:
| Serialization | Speed | Size | Readability | Recommendation |
|---|---|---|---|---|
| JSON | Medium | Large | Good | General, easy to debug |
| MessagePack | Fast | Small | Poor | High-performance scenarios |
| Protobuf | Fastest | Smallest | Poor | Extreme performance, but adds complexity |
Operations and Monitoring: The Three Pillars of Observability
Health Check Endpoint
// layered health check
func healthHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
health := HealthStatus{
Status: "healthy",
Timestamp: time.Now().UTC(),
Version: version,
Uptime: time.Since(startTime).String(),
}
// dependency checks
checks := make(map[string]DependencyStatus)
// Redis check
if err := redisClient.Ping(ctx).Err(); err != nil {
checks["redis"] = DependencyStatus{Status: "down", Error: err.Error()}
health.Status = "unhealthy"
} else {
checks["redis"] = DependencyStatus{Status: "up", Latency: "2ms"}
}
// LLM API check (lightweight)
if err := checkLLMHealth(ctx); err != nil {
checks["llm"] = DependencyStatus{Status: "degraded", Error: err.Error()}
if health.Status == "healthy" {
health.Status = "degraded"
}
} else {
checks["llm"] = DependencyStatus{Status: "up", Latency: "150ms"}
}
health.Checks = checks
// set HTTP status code based on status
switch health.Status {
case "healthy":
w.WriteHeader(http.StatusOK)
case "degraded":
w.WriteHeader(http.StatusOK) // degraded still returns 200, but marks status
case "unhealthy":
w.WriteHeader(http.StatusServiceUnavailable)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(health)
}
Prometheus Metrics Integration
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// request counter
requestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "agent_requests_total",
Help: "Total number of requests",
},
[]string{"method", "endpoint", "status"},
)
// request latency histogram
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "agent_request_duration_seconds",
Help: "Request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint"},
)
// active session count
activeSessions = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "agent_active_sessions",
Help: "Number of active sessions",
},
)
// LLM API call metrics
llmCalls = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "agent_llm_calls_total",
Help: "Total number of LLM API calls",
},
[]string{"model", "status"},
)
llmLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "agent_llm_latency_seconds",
Help: "LLM API latency in seconds",
Buckets: []float64{0.5, 1, 2, 5, 10, 30, 60},
},
[]string{"model"},
)
)
func init() {
prometheus.MustRegister(requestCounter, requestDuration, activeSessions, llmCalls, llmLatency)
}
// start metrics server in main
func startMetricsServer(port int) {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: mux,
}
go func() {
log.Printf("Metrics server starting on :%d", port)
if err := server.ListenAndServe(); err != nil {
log.Printf("Metrics server error: %v", err)
}
}()
}
Grafana Monitoring Dashboard
Key monitoring metrics:
| Metric | Alert Threshold | Description |
|---|---|---|
| Request QPS | - | Understand load trends |
| P99 latency | > 10s | Key user experience metric |
| Error rate | > 1% | Service health |
| Active sessions | > MaxSessions * 0.8 | Memory pressure warning |
| LLM API error rate | > 5% | Dependency anomaly |
| Goroutine count | > 10000 | Possible leak |
| Memory usage | > 80% | OOM risk |
| CPU usage | > 80% | Performance bottleneck |
In-Depth Troubleshooting
Q: WebSocket Doesn’t Work Behind Nginx Proxy
Root cause: Nginx does not forward WebSocket Upgrade headers by default.
Complete solution:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
# WebSocket required headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# other headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# long connection timeout
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
Verification method:
# test WebSocket connection
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Host: my-agent.example.com" \
-H "Origin: https://my-agent.example.com" \
https://my-agent.example.com/ws/
Q: Sessions Are Inconsistent Across Multiple Instances
Root cause: By default Sessions are stored in memory and are not shared across instances.
Solution:
- Use Redis shared Session (recommended).
- Use Sticky Session (session affinity).
# Sticky Session config (IP hash)
upstream runtime_backend {
ip_hash; # same IP always routed to same instance
server 127.0.0.1:8080;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
Note: Sticky Session is a temporary solution; sessions are lost when instances restart or scale. Production must use Redis.
Q: CORS Issues
Root cause: Browser same-origin policy restrictions.
Correct configuration:
// backend config
runtime, err := agentruntime.New(ctx,
agentruntime.WithCORS(agentruntime.CORSConfig{
// do not use "*" in production
AllowedOrigins: []string{
"https://my-agent.example.com",
"https://app.example.com",
},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{
"Content-Type",
"Authorization",
"X-Request-ID",
"X-CSRF-Token",
},
ExposedHeaders: []string{"X-Request-ID"},
AllowCredentials: true, // allow cookies
MaxAge: 86400,
}),
)
# CORS can also be configured at Nginx (fallback)
location /api/ {
# CORS preflight request
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://my-agent.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' '86400' always;
add_header 'Content-Length' '0';
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://my-agent.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend;
}
Performance Optimization Checklist
- Enable HTTP/2: Nginx config
listen 443 ssl http2 - Enable Gzip: Compress text responses, reducing transfer by 70%+.
- Static file caching: Set 1-year cache for JS/CSS/images.
- CDN acceleration: Put static assets on a CDN.
- Connection reuse: Configure Nginx
keepalive. - Connection pools: Redis connection pool, HTTP Client connection pool.
- Response compression: Enable compression for large JSON responses.
- Request batching: Front-end batch requests to reduce connection count.
- Lazy loading: Load WebUI components on demand.
- Preconnect: Use
<link rel="preconnect">to accelerate critical resources.
Next Steps
Web deployment is done; next, let’s look at Docker containerized deployment — build once, run anywhere.
← CLI Deployment | Docker Containerized Deployment →
Want to learn more Go ADK hands-on tips? Follow the “Full Stack Summit - Mengshou Programming” WeChat official account for weekly Go / AI programming updates.
