
“在我电脑上能跑”——程序员最不想说的六字真言。上个月我把 daily-report-agent 发给一个朋友。他折腾了一个下午装 Go、配环境变量、装 Redis、调 Ollama…最后还是跑不起来——他的 macOS 版本太旧Go 1.22 装不上。你的 Agent 写完只是完成了 50%。剩下 50% 是让别人包括你自己下个月能一个命令跑起来。这篇把 Agent 全套打包进 Docker——包含 LLM 调用、Redis 缓存、向量数据库。一个docker compose up -d全部就绪。为什么必须容器化不是赶时髦是三个实实在在的痛环境不一致—— 你用的 Go 1.22服务器上可能还是 1.18依赖地狱—— Agent 需要 Redis、向量数据库、本地模型手动装一遍要半小时重启不恢复—— 服务器重启后你记不住上次是怎么启动的又要查历史命令Docker 解决这三个问题而且不增加复杂度——写两个文件就够了。Dockerfile多阶段构建镜像从 800MB 瘦到 25MB# deploy/Dockerfile # ── 阶段 1构建 ── FROM golang:1.22-alpine AS builder # 安装编译依赖 RUN apk add --no-cache git ca-certificates WORKDIR /build # 先复制依赖文件利用 Docker 缓存层 COPY go.mod go.sum ./ RUN go mod download # 复制源码并编译 COPY . . RUN CGO_ENABLED0 GOOSlinux GOARCHamd64 \ go build -ldflags-s -w -o /build/agentd \ ./cmd/agentd # ── 阶段 2运行 ── FROM alpine:3.19 # 安全不 root 用户运行 RUN addgroup -S appgroup adduser -S appuser -G appgroup # 安装运行时依赖 RUN apk add --no-cache ca-certificates tzdata # 设置时区 ENV TZAsia/Shanghai # 复制二进制 COPY --frombuilder /build/agentd /app/agentd # 复制配置和 Prompt 文件 COPY configs/ /app/configs/ # 切换到非 root 用户 USER appuser WORKDIR /app # 健康检查 HEALTHCHECK --interval30s --timeout5s --retries3 \ CMD wget -qO- http://localhost:8080/health || exit 1 EXPOSE 8080 9090 ENTRYPOINT [./agentd]多阶段构建的好处编译时用 800MB 的 golang 镜像运行时只需要 15MB 的 alpine。最终镜像 25MB推送和拉取都很快。-ldflags-s -w去掉调试信息和符号表能再减 30% 体积。docker-compose.yml全家桶一键启动Agent 不是孤立的——它需要 Redis 做记忆存储可能还需要向量数据库做 RAG。用 docker-compose 把它们绑在一起# deploy/docker-compose.ymlversion:3.8services:# ── Agent 主服务 ──agent:build:context:.dockerfile:deploy/Dockerfilecontainer_name:daily-report-agentrestart:unless-stoppedports:-8080:8080# Agent API-9090:9090# Prometheus 指标environment:-LLM_API_KEY${LLM_API_KEY}-LLM_BASE_URLhttps://api.deepseek.com/anthropic-LLM_MODELdeepseek-v4-flash-REDIS_ADDRredis:6379-QDRANT_URLhttp://qdrant:6334-LOG_LEVELinfovolumes:-./configs:/app/configs:ro# 配置文件只读挂载depends_on:redis:condition:service_healthyqdrant:condition:service_startedhealthcheck:test:[CMD,wget,-qO-,http://localhost:8080/health]interval:30stimeout:5sretries:3networks:-agent-network# ── Redis 缓存 记忆存储 ──redis:image:redis:7-alpinecontainer_name:agent-redisrestart:unless-stoppedports:-6379:6379command:redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lruvolumes:-redis_data:/datahealthcheck:test:[CMD,redis-cli,ping]interval:10stimeout:3sretries:5networks:-agent-network# ── Qdrant 向量数据库RAG 用可选──qdrant:image:qdrant/qdrant:latestcontainer_name:agent-qdrantrestart:unless-stoppedports:-6333:6333-6334:6334volumes:-qdrant_data:/qdrant/storagenetworks:-agent-network# ── 可选本地 Ollama 做降级模型 ──# ollama:# image: ollama/ollama:latest# container_name: agent-ollama# restart: unless-stopped# ports:# - 11434:11434# volumes:# - ollama_data:/root/.ollama# networks:# - agent-network# ── Grafana可视化监控──grafana:image:grafana/grafana:latestcontainer_name:agent-grafanarestart:unless-stoppedports:-3000:3000environment:-GF_SECURITY_ADMIN_PASSWORDadminvolumes:-grafana_data:/var/lib/grafana-./deploy/grafana/dashboards:/etc/grafana/provisioning/dashboardsdepends_on:-prometheusnetworks:-agent-network# ── Prometheus指标收集──prometheus:image:prom/prometheus:latestcontainer_name:agent-prometheusrestart:unless-stoppedports:-9091:9090volumes:-./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml-prometheus_data:/prometheuscommand:---config.file/etc/prometheus/prometheus.yml---storage.tsdb.path/prometheusnetworks:-agent-networkvolumes:redis_data:qdrant_data:grafana_data:prometheus_data:# ollama_data:networks:agent-network:driver:bridge用.env文件管理敏感信息# .env不要提交到 GitLLM_API_KEYsk-your-key-hereLLM_MODELdeepseek-v4-flash.gitignore加一行.env一个命令跑起来# 1. 克隆项目gitclone https://github.com/lobster-bujiaban/daily-report-agent.gitcddaily-report-agent# 2. 配置 API Keycp.env.example .env# 编辑 .env填入你的 Key# 3. 一键启动dockercompose up-d# 4. 查看状态dockercomposeps# 输出# NAME STATUS PORTS# daily-report-agent running (healthy) 0.0.0.0:8080-8080/tcp# agent-redis running (healthy) 0.0.0.0:6379-6379/tcp# agent-qdrant running 0.0.0.0:6333-6334-6333-6334/tcp# agent-grafana running 0.0.0.0:3000-3000/tcp# agent-prometheus running 0.0.0.0:9091-9090/tcp一个命令全套基础设施就绪。给 Agent 加一个 HTTP APIAgent 在容器里跑需要暴露一个接口让外部调用。加一个最简 HTTP 服务// cmd/agentd/api.gopackagemainimport(encoding/jsonlognet/httptimeagent-project/internal/agent)typeServerstruct{agent*agent.Agent http*http.Server}funcNewServer(ag*agent.Agent)*Server{mux:http.NewServeMux()s:Server{agent:ag,http:http.Server{Addr::8080,Handler:mux,ReadTimeout:30*time.Second,WriteTimeout:5*time.Minute,// Agent 执行可能需要几分钟},}mux.HandleFunc(/api/v1/run,s.handleRun)mux.HandleFunc(/health,s.handleHealth)returns}func(s*Server)handleRun(w http.ResponseWriter,r*http.Request){ifr.Method!http.MethodPost{http.Error(w,只接受 POST,http.StatusMethodNotAllowed)return}varreqstruct{Taskstringjson:task}iferr:json.NewDecoder(r.Body).Decode(req);err!nil{http.Error(w,解析请求失败,http.StatusBadRequest)return}ifreq.Task{http.Error(w,task 不能为空,http.StatusBadRequest)return}log.Printf([API] 收到任务: %s,truncate(req.Task,100))result,err:s.agent.Run(r.Context(),req.Task)iferr!nil{log.Printf([API] 任务失败: %v,err)http.Error(w,err.Error(),http.StatusInternalServerError)return}w.Header().Set(Content-Type,application/json)json.NewEncoder(w).Encode(map[string]interface{}{answer:result.Answer,iterations:len(result.Iterations),total_tokens:result.TotalTokens,})}func(s*Server)handleHealth(w http.ResponseWriter,r*http.Request){w.Header().Set(Content-Type,application/json)w.WriteHeader(http.StatusOK)json.NewEncoder(w).Encode(map[string]string{status:healthy,time:time.Now().Format(time.RFC3339),})}func(s*Server)Start()error{log.Printf([Server] 启动在 %s,s.http.Addr)returns.http.ListenAndServe()}func(s*Server)Shutdown(ctx context.Context)error{returns.http.Shutdown(ctx)}调用示例curl-XPOST http://localhost:8080/api/v1/run\-HContent-Type: application/json\-d{task: 生成本周工作报告}常用运维命令# 查看 Agent 日志dockercompose logs-fagent# 只看错误dockercompose logs agent|grepERROR# 重启 Agent不影响 Redis 和 Qdrantdockercompose restart agent# 全部停止dockercompose down# 完全清理包括数据卷dockercompose down-v下一篇Agent 在服务器上跑稳了但每次发版手动写 Release Note 很烦。让 Agent 自动生成。