“在我电脑上能跑”——程序员最不想说的六字真言。

上个月我把 daily-report-agent 发给一个朋友。他折腾了一个下午:装 Go、配环境变量、装 Redis、调 Ollama…最后还是跑不起来——他的 macOS 版本太旧,Go 1.22 装不上。

你的 Agent 写完只是完成了 50%。剩下 50% 是让别人(包括你自己下个月)能一个命令跑起来。

这篇把 Agent 全套打包进 Docker——包含 LLM 调用、Redis 缓存、向量数据库。一个 docker compose up -d,全部就绪。


为什么必须容器化

不是赶时髦,是三个实实在在的痛:

  1. 环境不一致 —— 你用的 Go 1.22,服务器上可能还是 1.18
  2. 依赖地狱 —— Agent 需要 Redis、向量数据库、本地模型,手动装一遍要半小时
  3. 重启不恢复 —— 服务器重启后,你记不住上次是怎么启动的,又要查历史命令

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 自动生成。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐