梦兽编程
AI_SUITE

CLI Deployment: Production-Grade Command Line Tool

A detailed guide to deploying and operating an ADK Go Agent in production via the command line, including process management, logging, auto-start, and other production practices.

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:

ParameterPurposeProduction Recommendation
CGO_ENABLED=0Disable CGO and link staticallyRequired. Avoids glibc version dependency issues
GOOS=linuxTarget operating systemChoose based on the deployment environment
GOARCH=amd64Target architectureMainstream cloud server architecture
-ldflags='-s -w'Strip symbol table and debug infoRequired. Reduces binary size by ~30%
-ldflags="-X main.version=..."Inject version infoRecommended. Facilitates issue tracking
-trimpathStrip build path infoRecommended. 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 MethodTypical SizeUse Case
Default build~50MBDevelopment / debugging
-ldflags='-s -w'~35MBProduction
+ upx compression~12MBBandwidth-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:

  1. Lower MaxSessions or shorten SessionTTL
  2. Increase MemoryMax (if physical memory is sufficient)
  3. Enable history summaries to reduce per-session memory usage
  4. 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:

  1. Configure journal size limit: set SystemMaxUse=500M in /etc/systemd/journald.conf
  2. Clean old logs: sudo journalctl --vacuum-time=7d
  3. 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.

Frequently Asked Questions

Why is Go particularly well-suited for CLI deployment?

Go can be statically compiled into a single binary, so deployment only needs the executable plus a config file, with no extra runtime required.

What build flags are recommended for production?

Use CGO_ENABLED=0, -ldflags="-s -w", -trimpath, and inject version and build-time information.

How should a production-grade CLI manage configuration?

Support multiple sources such as CLI flags, environment variables, and config files, with priority from high to low: CLI > env > config file > defaults.

What operational support does CLI deployment require?

Process management, logging, monitoring integration, health checks, and automated build and release pipelines.

How do you optimize production binary size?

Use -s -w to strip the symbol table and debug info, reducing size by about 30%; optionally use upx for further compression.