梦兽编程
AI_SUITE

Web Interface Deployment and Maintenance: Let Users Access via Browser

A detailed guide to deploying an Agent in production via the ADK Go Web interface, including HTTPS configuration, reverse proxy, and high-availability solutions.

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:

  1. Independent scaling: API and WebUI have different load patterns and can be scaled separately.
  2. Independent maintenance: Front-end updates do not require restarting the API service.
  3. Security isolation: WebUI can be placed behind a CDN, while the API can be restricted to internal access.
  4. Technology stack independence: The front-end can be built with Vite/Webpack, decoupled from the back-end.

HTTPS Configuration: The Security Baseline for Production

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:

  1. SSL/TLS mode: Set to “Full (strict)”.
  2. Always Use HTTPS: Enable.
  3. Security Level: Set as needed.
  4. Bot Fight Mode: Enable to block malicious crawlers.
  5. 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:

SerializationSpeedSizeReadabilityRecommendation
JSONMediumLargeGoodGeneral, easy to debug
MessagePackFastSmallPoorHigh-performance scenarios
ProtobufFastestSmallestPoorExtreme 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:

MetricAlert ThresholdDescription
Request QPS-Understand load trends
P99 latency> 10sKey user experience metric
Error rate> 1%Service health
Active sessions> MaxSessions * 0.8Memory pressure warning
LLM API error rate> 5%Dependency anomaly
Goroutine count> 10000Possible 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:

  1. Use Redis shared Session (recommended).
  2. 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.

Frequently Asked Questions

Why is it recommended to deploy API and WebUI separately in production?

After separation they can be scaled, maintained, and security-isolated independently, and front-end and back-end technology stacks remain decoupled.

How is Web deployment different from API-only deployment?

It also requires HTTPS, CORS, WebSocket, CDN, SEO, and first-screen performance considerations.

How should API and WebUI be deployed in production?

Deploy them separately for independent scaling, maintenance, and security isolation.

What's required for production Web deployment?

A reverse proxy, SSL/TLS termination, security headers, and performance optimization.

Can WebUI static assets be served separately?

Yes, via CDN or a dedicated static path while proxying API requests to the backend.