Compiling the Agent into a static binary and deploying it via CLI is Go’s most advantageous production delivery method. Compared to languages like Python or Node.js that require a runtime environment, Go’s static compilation makes deployment extremely simple — a single binary plus a config file is enough to run. But there is a big gap between “it runs” and “it runs stably”.
This article walks through CLI deployment best practices systematically, from build optimization, process management, logging, monitoring integration to automated operations.
Build Optimization: From go build to a Production Binary
Basic Build
# Development build (with debug info)
go build -o my-agent .
# Production build (optimized)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=$(git describe --tags) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-trimpath \
-o my-agent .
Parameter reference:
| Parameter | Purpose | Production Recommendation |
|---|---|---|
CGO_ENABLED=0 | Disable CGO and link statically | Required. Avoids glibc version dependency issues |
GOOS=linux | Target operating system | Choose based on the deployment environment |
GOARCH=amd64 | Target architecture | Mainstream cloud server architecture |
-ldflags='-s -w' | Strip symbol table and debug info | Required. Reduces binary size by ~30% |
-ldflags="-X main.version=..." | Inject version info | Recommended. Facilitates issue tracking |
-trimpath | Strip build path info | Recommended. Improves security |
Multi-architecture Build (Cloud Native Essential)
Modern cloud environments often require supporting multiple CPU architectures (x86_64, ARM64):
#!/bin/bash
# build.sh - multi-architecture build script
VERSION=$(git describe --tags --always)
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"
# Define target platforms
PLATFORMS=(
"linux/amd64"
"linux/arm64"
"darwin/amd64"
"darwin/arm64"
)
for PLATFORM in "${PLATFORMS[@]}"; do
GOOS=${PLATFORM%/*}
GOARCH=${PLATFORM#*/}
OUTPUT="dist/my-agent-${GOOS}-${GOARCH}"
echo "Building for ${PLATFORM}..."
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} \
go build -ldflags="${LDFLAGS}" -trimpath -o "${OUTPUT}" .
# Compute and output file hash (for verification)
sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"
done
echo "Build complete. Artifacts in dist/"
Binary Size Optimization
Smaller production binaries mean faster deployment transfers and shorter cold-start times:
# Compress with upx (optional, but adds startup decompression time)
upx --best dist/my-agent-linux-amd64
# Verify compression result
ls -lh dist/
Size comparison:
| Build Method | Typical Size | Use Case |
|---|---|---|
| Default build | ~50MB | Development / debugging |
-ldflags='-s -w' | ~35MB | Production |
| + upx compression | ~12MB | Bandwidth-constrained environments |
Running and Configuration Management
CLI Argument Design
Production-grade CLI should support multiple configuration sources (priority from high to low):
// main.go - configuration parsing
package main
import (
"flag"
"os"
"github.com/spf13/viper"
)
type Config struct {
Port int `mapstructure:"port"`
LogLevel string `mapstructure:"log_level"`
LogFormat string `mapstructure:"log_format"`
MaxConcurrent int `mapstructure:"max_concurrent"`
MaxSessions int `mapstructure:"max_sessions"`
SessionTTL time.Duration `mapstructure:"session_ttl"`
RedisURL string `mapstructure:"redis_url"`
MetricsPort int `mapstructure:"metrics_port"`
EnablePprof bool `mapstructure:"enable_pprof"`
}
func loadConfig() (*Config, error) {
viper.SetEnvPrefix("AGENT") // env var prefix: AGENT_PORT
viper.AutomaticEnv()
// defaults
viper.SetDefault("port", 8080)
viper.SetDefault("log_level", "info")
viper.SetDefault("log_format", "json")
viper.SetDefault("max_concurrent", 100)
viper.SetDefault("max_sessions", 10000)
viper.SetDefault("session_ttl", "24h")
viper.SetDefault("metrics_port", 9090)
// config file
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/my-agent/")
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, err
}
}
// CLI flags (highest priority)
flag.Int("port", viper.GetInt("port"), "HTTP server port")
flag.String("log-level", viper.GetString("log_level"), "Log level")
flag.Parse()
// bind CLI flags to viper
viper.BindPFlags(flag.CommandLine)
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
Configuration File Example
# config.yaml - production config
port: 8080
log_level: info
log_format: json # JSON format is easy for log collection systems to parse
# concurrency control
max_concurrent: 200
max_sessions: 50000
session_ttl: 24h
# Redis persistence
redis_url: "redis://localhost:6379/0"
# monitoring
metrics_port: 9090
enable_pprof: false # disable in production; enable temporarily for troubleshooting
# TLS (required in production)
tls:
enabled: true
cert_file: "/etc/my-agent/server.crt"
key_file: "/etc/my-agent/server.key"
Process Management: Deep systemd Configuration
systemd is the standard process manager on modern Linux systems; correct configuration greatly improves service stability.
Basic Service Configuration
# /etc/systemd/system/my-agent.service
[Unit]
Description=ADK Go Agent Runtime
Documentation=https://docs.example.com/my-agent
After=network-online.target redis.service
Wants=network-online.target
Requires=redis.service # if Redis is required
[Service]
Type=notify
User=agent
Group=agent
# working directory
WorkingDirectory=/opt/my-agent
# executable command
ExecStart=/opt/my-agent/my-agent --config /etc/my-agent/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
# restart policy
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=3
# resource limits
# memory: adjust based on Agent complexity; leave 50% headroom
MemoryMax=2G
MemorySwapMax=0 # disable swap to avoid performance jitter
# CPU: limit to 2 cores
CPUQuota=200%
# file descriptors: increase for high concurrency
LimitNOFILE=65535
# process limit
LimitNPROC=10000
# log output to journal
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-agent
# graceful shutdown
TimeoutStopSec=30
KillSignal=SIGTERM
KillMode=mixed
# security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/my-agent/data
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Enable and Operate
# reload systemd configuration
sudo systemctl daemon-reload
# enable auto-start on boot
sudo systemctl enable my-agent
# start service
sudo systemctl start my-agent
# view status
sudo systemctl status my-agent
# view detailed logs
sudo journalctl -u my-agent -f
# graceful reload (if app supports HUP config reload)
sudo systemctl reload my-agent
# view resource usage
systemctl show my-agent --property=MemoryCurrent,CPUUsageNSec
Advanced: Notification Mode (Type=notify)
Go 1.16+ supports systemd’s notify protocol, letting the service notify systemd after it has actually started:
import (
"github.com/coreos/go-systemd/v22/daemon"
)
func main() {
// ... initialization code ...
// notify systemd: service is ready
if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
log.Printf("failed to notify systemd: %v", err)
}
// ... start server ...
}
With Type=notify configured, systemd waits until the service is truly ready before marking it active, avoiding the race condition where “the service has started but is not yet ready to receive requests”.
Logging: From Printing to Observability
Structured Logging Configuration
Production environments must use structured logs (JSON) so log collection systems (e.g., ELK, Loki, Fluentd) can parse them:
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
"time"
)
func setupLogger(level, format string) {
// set log level
switch level {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
}
if format == "json" {
// JSON format (production)
log.Logger = zerolog.New(os.Stdout).
With().
Timestamp().
Caller().
Str("service", "my-agent").
Str("version", version).
Logger()
} else {
// console format (development)
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).
With().
Timestamp().
Caller().
Logger()
}
}
// usage example
func handleRequest(ctx context.Context, req *Request) {
logger := log.Ctx(ctx).With().
Str("request_id", req.ID).
Str("user_id", req.UserID).
Logger()
logger.Info().Str("input", req.Input).Msg("processing request")
// ... processing logic ...
if err != nil {
logger.Error().Err(err).Int("latency_ms", latency).Msg("request failed")
return
}
logger.Info().Int("latency_ms", latency).Msg("request completed")
}
journalctl Log Queries
# follow logs in real time
sudo journalctl -u my-agent -f
# logs from the last hour
sudo journalctl -u my-agent --since "1 hour ago"
# query by time range
sudo journalctl -u my-agent --since "2026-05-29 10:00:00" --until "2026-05-29 12:00:00"
# view error-level logs
sudo journalctl -u my-agent -p err
# export logs to file
sudo journalctl -u my-agent --since "today" > /tmp/my-agent.log
Log Rotation Configuration
Although systemd journal has built-in log management, if the app writes log files directly, configure logrotate:
# /etc/logrotate.d/my-agent
/var/log/my-agent/*.log {
daily
rotate 30 # keep 30 days
compress # compress old logs
delaycompress # delay compression by one day
missingok # do not error if log missing
notifempty # do not rotate empty logs
create 0640 agent agent
# send HUP after rotation so the app reopens log files
postrotate
systemctl reload my-agent
endscript
}
Health Checks and Auto Recovery
HTTP Health Check Endpoint
func healthHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
checks := map[string]interface{}{
"status": "healthy",
"timestamp": time.Now().UTC(),
"version": version,
}
// check critical dependencies
if err := checkRedis(ctx); err != nil {
checks["status"] = "unhealthy"
checks["redis"] = map[string]string{"status": "down", "error": err.Error()}
w.WriteHeader(http.StatusServiceUnavailable)
} else {
checks["redis"] = map[string]string{"status": "up"}
}
// check LLM API connectivity (lightweight; do not actually call it)
if err := checkLLMConnectivity(ctx); err != nil {
checks["llm"] = map[string]string{"status": "degraded", "error": err.Error()}
} else {
checks["llm"] = map[string]string{"status": "up"}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(checks)
}
// lightweight connectivity check (only DNS resolution and TCP connection)
func checkLLMConnectivity(ctx context.Context) error {
dialer := &net.Dialer{Timeout: 5 * time.Second}
conn, err := dialer.DialContext(ctx, "tcp", "generativelanguage.googleapis.com:443")
if err != nil {
return err
}
conn.Close()
return nil
}
Advanced systemd Health Checks
[Service]
# start limit: at most 3 restarts within 5 minutes
StartLimitIntervalSec=300
StartLimitBurst=3
# service health check (systemd 245+)
ExecStartPost=/usr/bin/curl -f -s http://localhost:8080/health || exit 1
WatchdogSec=30
NotifyAccess=all
Troubleshooting Guide
Q: Process OOM killed
Diagnosis:
# check system log for OOM records
sudo dmesg | grep -i "killed process"
sudo journalctl -k | grep -i "oom"
# view process memory usage trend
systemctl show my-agent --property=MemoryCurrent
Solution:
- Lower
MaxSessionsor shortenSessionTTL - Increase
MemoryMax(if physical memory is sufficient) - Enable history summaries to reduce per-session memory usage
- Use Redis as external session storage
Q: Disk Full
Diagnosis:
# check disk usage
df -h
# check log usage
du -sh /var/log/journal/
du -sh /var/log/my-agent/
# check journal usage
journalctl --disk-usage
Solution:
- Configure journal size limit: set
SystemMaxUse=500Min/etc/systemd/journald.conf - Clean old logs:
sudo journalctl --vacuum-time=7d - Lower the application log level (from debug to info)
Q: Updating Agent Requires Restart
Zero-downtime update options:
# Option 1: use systemd reload (if app supports hot config reload)
sudo systemctl reload my-agent
# Option 2: blue-green deployment (recommended)
# 1. start new version on another port
sudo cp my-agent /opt/my-agent/my-agent.new
sudo /opt/my-agent/my-agent.new --port 8081 &
# 2. switch load balancer to new port
sudo sed -i 's/8080/8081/' /etc/nginx/conf.d/my-agent.conf
sudo systemctl reload nginx
# 3. stop old version
sudo systemctl stop my-agent
sudo mv /opt/my-agent/my-agent.new /opt/my-agent/my-agent
sudo systemctl start my-agent
# 4. switch load balancer back to original port
sudo sed -i 's/8081/8080/' /etc/nginx/conf.d/my-agent.conf
sudo systemctl reload nginx
Next Steps
CLI deployment is done; next, let’s look at Web UI deployment.
← Agent Runtime Architecture | Web 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.
