Docker 部署实战:一个命令跑起你的 Agent
·
“在我电脑上能跑”——程序员最不想说的六字真言。
上个月我把 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_ENABLED=0 GOOS=linux GOARCH=amd64 \
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 TZ=Asia/Shanghai
# 复制二进制
COPY --from=builder /build/agentd /app/agentd
# 复制配置和 Prompt 文件
COPY configs/ /app/configs/
# 切换到非 root 用户
USER appuser
WORKDIR /app
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
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.yml
version: "3.8"
services:
# ── Agent 主服务 ──
agent:
build:
context: .
dockerfile: deploy/Dockerfile
container_name: daily-report-agent
restart: unless-stopped
ports:
- "8080:8080" # Agent API
- "9090:9090" # Prometheus 指标
environment:
- LLM_API_KEY=${LLM_API_KEY}
- LLM_BASE_URL=https://api.deepseek.com/anthropic
- LLM_MODEL=deepseek-v4-flash
- REDIS_ADDR=redis:6379
- QDRANT_URL=http://qdrant:6334
- LOG_LEVEL=info
volumes:
- ./configs:/app/configs:ro # 配置文件只读挂载
depends_on:
redis:
condition: service_healthy
qdrant:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
networks:
- agent-network
# ── Redis 缓存 & 记忆存储 ──
redis:
image: redis:7-alpine
container_name: agent-redis
restart: unless-stopped
ports:
- "6379:6379"
command: >
redis-server
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- agent-network
# ── Qdrant 向量数据库(RAG 用,可选)──
qdrant:
image: qdrant/qdrant:latest
container_name: agent-qdrant
restart: unless-stopped
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
networks:
- 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:latest
container_name: agent-grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
- ./deploy/grafana/dashboards:/etc/grafana/provisioning/dashboards
depends_on:
- prometheus
networks:
- agent-network
# ── Prometheus(指标收集)──
prometheus:
image: prom/prometheus:latest
container_name: agent-prometheus
restart: unless-stopped
ports:
- "9091:9090"
volumes:
- ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
networks:
- agent-network
volumes:
redis_data:
qdrant_data:
grafana_data:
prometheus_data:
# ollama_data:
networks:
agent-network:
driver: bridge
用 .env 文件管理敏感信息:
# .env(不要提交到 Git)
LLM_API_KEY=sk-your-key-here
LLM_MODEL=deepseek-v4-flash
.gitignore 加一行:
.env
一个命令跑起来
# 1. 克隆项目
git clone https://github.com/lobster-bujiaban/daily-report-agent.git
cd daily-report-agent
# 2. 配置 API Key
cp .env.example .env
# 编辑 .env,填入你的 Key
# 3. 一键启动
docker compose up -d
# 4. 查看状态
docker compose ps
# 输出:
# 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 API
Agent 在容器里跑,需要暴露一个接口让外部调用。加一个最简 HTTP 服务:
// cmd/agentd/api.go
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"agent-project/internal/agent"
)
type Server struct {
agent *agent.Agent
http *http.Server
}
func NewServer(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)
return s
}
func (s *Server) handleRun(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "只接受 POST", http.StatusMethodNotAllowed)
return
}
var req struct {
Task string `json:"task"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "解析请求失败", http.StatusBadRequest)
return
}
if req.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)
if err != 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)
return s.http.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.http.Shutdown(ctx)
}
调用示例:
curl -X POST http://localhost:8080/api/v1/run \
-H "Content-Type: application/json" \
-d '{"task": "生成本周工作报告"}'
常用运维命令
# 查看 Agent 日志
docker compose logs -f agent
# 只看错误
docker compose logs agent | grep ERROR
# 重启 Agent(不影响 Redis 和 Qdrant)
docker compose restart agent
# 全部停止
docker compose down
# 完全清理(包括数据卷)
docker compose down -v
下一篇:Agent 在服务器上跑稳了,但每次发版手动写 Release Note 很烦。让 Agent 自动生成。
更多推荐




所有评论(0)