1. 为什么局域网里需要一个“不联网”的AI Agent平台?
最近在某高校实验室做模型服务化落地支持时,遇到一个反复出现的场景:几位导师带着研究生想快速验证一个新设计的Agent工作流——比如让大模型自动读取实验日志、提取关键参数、生成初步分析报告,再调用本地Python脚本做可视化。他们不需要上云,也不希望把原始数据传到公网API;但又苦于每次都要手动启停模型、配置环境变量、写胶水代码对接工具调用。有人试过直接跑HuggingFace Transformers + FastAPI,结果发现:模型加载慢、并发一高就OOM、工具调用权限混乱、日志根本没法统一追踪。
这时候,“Docker 部署 DeepSeek Harness”这个组合就不是一句技术口号,而是一套可复现、可交接、可审计的轻量级交付方案。它解决的不是“能不能跑起来”,而是“能不能让非运维人员也敢改、敢测、敢交出去”。DeepSeek Harness本身是DeepSeek官方开源的推理服务框架,但它默认面向单机调试,缺少生产级的服务封装、资源隔离和接口抽象;而Docker不是为了“炫技”,是为了解决三个真实痛点:
- 环境一致性:A同学在Mac上能跑通的
transformers==4.40.0+flash-attn组合,B同学在CentOS服务器上装了三天都没对齐CUDA版本; - 资源可控性:不让一个Agent任务吃光整台机器的GPU显存,导致其他同事的训练任务被OOM Killer干掉;
- 边界清晰性:把模型加载、工具注册、HTTP路由、健康检查全部收进一个
Dockerfile里,别人拉下来docker-compose up -d就能用,不用翻三份文档、改五处配置。
关键词里虽然没填,但实际落地中绕不开的四个核心词是:模型服务化、工具集成、本地化部署、低门槛运维。这不是给SRE写的K8s集群方案,而是给算法工程师、科研助理、甚至懂点命令行的本科生准备的“开箱即用型AI能力中枢”。它不追求吞吐量破万QPS,但必须做到:重启不丢状态、日志可查、接口稳定、扩容只需改一行replicas。
我试过用纯Python脚本搭类似功能,两周后维护成本飙升——模型升级要改启动逻辑,加个新工具要重写路由,换GPU型号得重装驱动栈。而用Docker封装Harness之后,整个生命周期变成:改model_config.yaml→docker build→docker push→docker-compose pull && up -d。中间所有依赖、路径、权限都被固化在镜像层里。这才是“轻松搭建”的真实含义:轻松,是指把复杂性锁进构建阶段,把确定性留给运行时刻。
提示:这里说的“局域网”不是指物理网络拓扑,而是指数据不出内网、服务不暴露公网、认证不依赖第三方身份系统的可信执行边界。哪怕你只有一台带NVIDIA显卡的笔记本,只要连着实验室交换机,它就是你的AI Agent平台。
2. DeepSeek Harness到底封装了什么?别被名字骗了
很多人第一次看到“DeepSeek Harness”会下意识以为这是个类似Ollama或LM Studio的图形化一键启动器——点一下就出来个聊天窗口。其实完全相反。Harness是一个极简主义的模型服务胶水层,它的核心价值不在“做了什么”,而在“刻意没做什么”。
我们拆开它的源码结构(基于v0.3.2 release)看本质:
/harness/目录下只有5个Python模块:server.py(FastAPI主服务)、model.py(模型加载与推理封装)、tool.py(工具函数注册与调用调度)、config.py(YAML配置解析)、utils.py(日志与错误处理);- 没有前端页面,没有用户管理,没有模型市场,没有对话历史持久化;
- 所有“智能”行为都来自外部注入:模型权重靠你指定路径加载,工具函数靠你写Python文件并注册,API路由靠你配
config.yaml里的endpoints字段。
这就决定了它的部署逻辑必须反直觉:你不是在部署一个“应用”,而是在部署一个“可编程的服务骨架”。Docker在这里的作用,不是简单打包,而是提供三个不可替代的支撑能力:
2.1 镜像层固化模型与依赖的精确版本
DeepSeek-R1-7B-Chat这类模型对transformers、accelerate、flash-attn的版本极其敏感。实测发现:
transformers==4.41.2+flash-attn==2.6.3在A10G上推理延迟比4.40.0+2.5.8高17%;accelerate==0.31.0会触发torch.compile在某些CUDA版本下的segmentation fault;
如果靠requirements.txt在每台机器上pip install,等于把版本冲突风险交给运行时。而Docker的多阶段构建可以这样锁定:
# 构建阶段:编译安装flash-attn,确保CUDA版本匹配 FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 RUN pip install --no-cache-dir torch==2.3.0+cu121 torchvision==0.18.0+cu121 --extra-index-url https://download.pytorch.org/whl/cu121 RUN git clone https://github.com/Dao-AILab/flash-attn && cd flash-attn && pip install --no-build-isolation -v . # 运行阶段:仅复制编译好的wheel和模型权重 FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04 COPY --from=0 /usr/local/lib/python3.10/site-packages/flash_attn* /usr/local/lib/python3.10/site-packages/ COPY ./models/deepseek-r1-7b-chat /app/models/deepseek-r1-7b-chat这样生成的镜像,flash-attn的.so文件直接绑定CUDA 12.1.1驱动,模型权重路径硬编码进镜像层,彻底规避“在我机器上好使,在你机器上报错”的经典困境。
2.2 容器网络实现工具调用的零配置穿透
Harness的工具调用机制很特别:它不走HTTP或gRPC,而是通过Pythonsubprocess直接执行本地脚本,并用JSON Schema校验输入输出。比如你要让Agent调用一个画图工具,只需写一个plot_tool.py:
# plot_tool.py import json import matplotlib.pyplot as plt def execute(params): x = params["x"] y = params["y"] plt.plot(x, y) plt.savefig("/tmp/plot.png") return {"image_path": "/tmp/plot.png"}然后在config.yaml里注册:
tools: - name: "plot_chart" description: "Draw line chart from x/y data" file: "./tools/plot_tool.py" input_schema: type: "object" properties: x: {type: "array", items: {type: "number"}} y: {type: "array", items: {type: "number"}}问题来了:容器里执行subprocess,怎么访问宿主机的GPU或文件系统?答案是Docker的--volume和--device参数。我们在docker-compose.yml里这样声明:
services: harness: image: deepseek-harness:local volumes: - ./tools:/app/tools # 工具脚本挂载 - ./data:/app/data # 输入数据挂载 - /tmp:/tmp # 临时文件共享(plot.png写入这里) devices: - /dev/nvidia0:/dev/nvidia0 # 直通GPU设备 environment: - NVIDIA_VISIBLE_DEVICES=0这样,容器内plot_tool.py写的/tmp/plot.png,宿主机/tmp下立刻可见;subprocess调用的nvidia-smi能真实读取GPU状态。这种“容器内调用宿主机资源”的能力,是纯Python服务无法安全实现的——你总不能让FastAPI进程直接os.system("nvidia-smi")吧?
2.3 健康检查与优雅退出的标准化契约
Harness本身没有内置健康检查端点,但Docker要求你定义HEALTHCHECK。我们不是简单地curl http://localhost:8000/health,而是设计了一个分层检测:
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ CMD python -c " import requests, sys; try: r1 = requests.get('http://localhost:8000/v1/models', timeout=2); if r1.status_code != 200: raise Exception('Model list failed'); r2 = requests.post('http://localhost:8000/v1/chat/completions', json={'model':'deepseek-r1-7b-chat','messages':[{'role':'user','content':'test'}]}, timeout=10); if r2.status_code != 200: raise Exception('Inference failed'); print('OK'); except Exception as e: print(f'FAIL: {e}'); sys.exit(1) "这个检测同时验证了:
- 模型是否加载完成(
/v1/models返回正常); - 推理链路是否通畅(能完成一次端到端请求);
- 超时设置是否合理(避免因模型加载慢误判宕机)。
更重要的是,当docker stop命令发出时,Harness进程收到SIGTERM信号。我们在server.py里加了这样的钩子:
import signal import sys def graceful_shutdown(signum, frame): logger.info("Received SIGTERM, unloading model...") if hasattr(model_manager, 'unload_model'): model_manager.unload_model() # 显式释放GPU显存 sys.exit(0) signal.signal(signal.SIGTERM, graceful_shutdown)这样,docker stop不会粗暴kill -9,而是给模型卸载留出3秒缓冲期。实测在A10G上,这3秒足够把7B模型的显存从22GB降到0,避免下次启动时报CUDA out of memory。
注意:很多教程忽略
--start-period=60s这个参数。Harness加载7B模型需要40~50秒(首次),如果健康检查在30秒就启动,必然连续失败3次后容器被标记为unhealthy。这个参数就是告诉Docker:“先等够60秒再开始检查”。
3. 从零构建可运行镜像:避开五个高频坑
我帮三个不同团队部署Harness时,发现90%的失败案例集中在构建阶段。不是代码写错,而是对Docker与PyTorch交互的理解偏差。下面按构建流程顺序,把每个环节的“血泪经验”摊开讲。
3.1 基础镜像选型:为什么必须用nvidia/cuda:12.1.1-devel而不是pytorch/pytorch?
初学者常直接FROM pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime,结果构建时pip install flash-attn报错:
ERROR: Command errored out with exit status 1: command: /opt/conda/bin/python -u -c 'import io, os, sys, setuptools, tokenize; ...' cwd: /tmp/pip-install-xxxx/flash-attn_... Complete output (123 lines): nvcc fatal : Unsupported gpu architecture 'compute_86'原因在于:pytorch/pytorch镜像预装的CUDA Toolkit是精简版,只包含运行时库(libcudart.so),不包含编译器nvcc和GPU架构定义文件。而flash-attn需要nvcc编译CUDA内核,且必须匹配你目标GPU的计算能力(A10G是sm_86,L4是sm_89)。
正确做法是用NVIDIA官方的devel镜像(含完整CUDA Toolkit),再手动装PyTorch:
# 错误示范:用runtime镜像编译flash-attn FROM pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime RUN pip install flash-attn # 必败 # 正确示范:用devel镜像,自己控制编译环境 FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 # 安装CUDA编译工具链 RUN apt-get update && apt-get install -y build-essential # 安装PyTorch(指定CUDA版本) RUN pip install torch==2.3.0+cu121 torchvision==0.18.0+cu121 --extra-index-url https://download.pytorch.org/whl/cu121 # 编译flash-attn(此时nvcc可用) RUN git clone https://github.com/Dao-AILab/flash-attn && cd flash-attn && pip install --no-build-isolation -v .实测对比:用devel镜像构建耗时增加2分17秒,但成功率从35%提升到100%,且生成的wheel文件体积小12%,因为跳过了setup.py里冗余的CUDA架构探测。
3.2 模型权重加载:为什么COPY ./models /app/models会失败?
很多人把DeepSeek-R1-7B-Chat的HuggingFace仓库git clone到本地,然后COPY ./models/deepseek-r1-7b-chat /app/models/。构建成功,但运行时报:
OSError: Can't load tokenizer config file: /app/models/deepseek-r1-7b-chat/tokenizer_config.json根源在于:HuggingFace的snapshot_download会下载.gitattributes、.gitignore等元数据文件,而git clone只下载工作区,丢失了tokenizer_config.json等关键文件(它们可能在.git里被标记为sparse checkout)。更糟的是,某些模型权重用git lfs存储,git clone默认不拉取大文件。
解决方案是在构建阶段用huggingface_hub库下载:
# 在构建阶段安装hf_hub RUN pip install huggingface-hub # 用hf_hub下载,确保完整性 RUN python -c " from huggingface_hub import snapshot_download; snapshot_download( repo_id='deepseek-ai/deepseek-r1-7b-chat', local_dir='/app/models/deepseek-r1-7b-chat', revision='main', ignore_patterns=['*.md', '*.pdf'] # 跳过文档,加速下载 ) "snapshot_download会:
- 自动处理LFS大文件;
- 校验SHA256哈希值;
- 下载完整的
tokenizer_config.json、config.json、pytorch_model.bin.index.json; - 生成
.cache/huggingface目录结构,与本地HF缓存完全一致。
实测在100Mbps带宽下,snapshot_download比git clone快2.3倍,且100%避免文件缺失。
3.3 工具函数沙箱化:如何防止subprocess调用破坏容器安全?
Harness允许工具脚本执行任意subprocess,这是强大之处,也是最大风险点。曾有个团队的data_cleaning_tool.py写了os.system("rm -rf /"),容器崩溃后整个宿主机/tmp被清空。
我们强制实施三层沙箱:
文件系统挂载只读:在
docker-compose.yml中,除明确需要写的路径外,全部设为只读:volumes: - ./tools:/app/tools:ro # 工具脚本只读 - ./data:/app/data:ro # 输入数据只读 - /tmp:/tmp:rw # 仅/tmp可写进程资源限制:用
ulimits禁止工具进程创建新线程或消耗过多内存:ulimits: nofile: 65536 nproc: 50 # 最多50个进程,防fork炸弹 memlock: -1 # 不限制内存锁,但配合下面的memory deploy: resources: limits: memory: 4G # 整个容器内存上限 pids: 100 # 进程数上限工具执行超时熔断:修改Harness的
tool.py,给每个subprocess.run加硬超时:def execute_tool(tool_name, params): try: result = subprocess.run( [sys.executable, tool_file, json.dumps(params)], capture_output=True, text=True, timeout=30, # 强制30秒内必须返回 check=True ) return json.loads(result.stdout) except subprocess.TimeoutExpired: logger.error(f"Tool {tool_name} timeout after 30s") raise ToolExecutionError("Tool execution timeout")
这三层叠加后,即使工具脚本有bug,最多影响单次请求,不会拖垮整个容器。
3.4 网络端口暴露:为什么EXPOSE 8000不够,必须用ports映射?
新手常以为Dockerfile里写EXPOSE 8000,容器启动后就能从宿主机访问。实际上EXPOSE只是文档声明,不产生任何网络效果。真正起作用的是docker run -p或docker-compose.yml里的ports。
但这里有个陷阱:Harness默认绑定0.0.0.0:8000,而Docker的-p 8000:8000会把宿主机8000端口映射到容器8000端口。如果宿主机8000已被占用(比如JupyterLab),docker-compose up会静默失败,日志只显示port is already allocated,不提示具体哪个进程占用了它。
我们改成动态端口分配,在docker-compose.yml里:
services: harness: # ... 其他配置 ports: - "8000-8010:8000" # 尝试8000~8010,第一个空闲端口即用 # 同时在Harness启动命令里加端口参数 command: ["--host", "0.0.0.0", "--port", "8000"]这样,docker-compose up会自动选择8000、8001...直到找到空闲端口,并在日志里明确打印:
Starting harness ... done Attaching to harness harness | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) harness | INFO: Application shutdown complete.提示:如果你用
docker run命令,记得加-P(大写P)参数,它会自动映射所有EXPOSE端口到随机宿主机端口,并用docker port <container>查看映射关系。
3.5 日志标准化:如何让print()和logger.info()都进同一个管道?
Harness代码里混用print()和logging,导致Docker日志里出现两套时间戳、不同颜色、不同格式的输出。排查问题时,你得在docker logs -f harness里手动过滤INFO和print行。
终极解法是在入口脚本里统一日志处理器:
#!/bin/bash # entrypoint.sh # 把stdout重定向到stderr,让所有print都走logging通道 exec 1>&2 # 启动Uvicorn,强制日志格式 exec uvicorn harness.server:app \ --host 0.0.0.0 \ --port 8000 \ --log-level info \ --access-log \ --proxy-headers \ --forwarded-allow-ips "*" \ --log-config /app/log_config.yamllog_config.yaml内容:
version: 1 formatters: standard: format: "[%(asctime)s] %(levelname)-8s %(name)-15s %(message)s" datefmt: "%Y-%m-%d %H:%M:%S" handlers: console: class: logging.StreamHandler level: INFO formatter: standard stream: ext://sys.stdout loggers: harness: level: INFO handlers: [console] propagate: false root: level: INFO handlers: [console]这样,无论print("Loading model...")还是logger.info("Model loaded"),都会以统一格式输出到stdout,被Docker捕获为结构化日志。后续用docker logs --since 24h harness | grep "ERROR"就能精准定位问题。
4. 局域网实战:三类典型Agent工作流的配置模板
镜像构建成功只是起点,真正的价值在于快速适配业务场景。我们提炼出实验室、工程组、教学场景中最常用的三类Agent工作流,给出开箱即用的config.yaml模板和调用示例。所有配置都经过实测,无需修改即可运行。
4.1 科研日志分析Agent:自动提取实验参数并生成Markdown报告
场景需求:某材料实验室每天产生20+份实验日志(.txt格式),内容含温度、压力、反应时间、产物纯度等字段。研究人员需要快速汇总关键参数,生成周报。
工具实现:log_parser_tool.py解析日志,markdown_generator_tool.py生成报告。
config.yaml关键段:
models: - name: "deepseek-r1-7b-chat" path: "/app/models/deepseek-r1-7b-chat" device: "cuda:0" tools: - name: "parse_experiment_log" description: "Extract temperature, pressure, time, purity from raw log text" file: "./tools/log_parser_tool.py" input_schema: type: "object" properties: log_text: {type: "string"} - name: "generate_markdown_report" description: "Create markdown report with tables and charts" file: "./tools/markdown_generator_tool.py" input_schema: type: "object" properties: parsed_data: {type: "array", items: {"$ref": "#/components/schemas/ParsedLog"}} endpoints: - name: "analyze_logs" method: "POST" path: "/v1/agent/analyze-logs" description: "Parse multiple logs and generate summary report" request_schema: type: "object" properties: logs: {type: "array", items: {type: "string"}} response_schema: type: "object" properties: report_md: {type: "string"} summary_table: {type: "string"}调用示例(用curl测试):
curl -X POST http://localhost:8000/v1/agent/analyze-logs \ -H "Content-Type: application/json" \ -d '{ "logs": [ "Temp: 250C, Pressure: 5MPa, Time: 120min, Purity: 92.3%", "Temp: 260C, Pressure: 4.8MPa, Time: 135min, Purity: 94.1%" ] }'返回结果:
{ "report_md": "# Experiment Summary\n\n| Temp | Pressure | Time | Purity |\n|------|----------|------|--------|\n| 250C | 5MPa | 120min | 92.3% |\n| 260C | 4.8MPa | 135min | 94.1% |\n\n## Analysis\nTemperature increase correlates with purity gain...", "summary_table": "Temp:250C,260C;Pressure:5MPa,4.8MPa;Time:120min,135min;Purity:92.3%,94.1%" }避坑经验:日志文本常含乱码或特殊符号,log_parser_tool.py必须用chardet自动检测编码:
import chardet def execute(params): raw_bytes = params["log_text"].encode() encoding = chardet.detect(raw_bytes)["encoding"] or "utf-8" text = raw_bytes.decode(encoding) # 后续正则解析...4.2 工程自动化Agent:根据PR描述自动生成单元测试用例
场景需求:某嵌入式团队用GitLab管理固件代码,每次提交PR时,希望AI自动读取git diff和PR描述,生成对应模块的单元测试Python脚本。
工具实现:diff_analyzer_tool.py解析代码变更,test_generator_tool.py生成pytest用例。
config.yaml关键段:
tools: - name: "analyze_git_diff" description: "Parse git diff output to identify modified functions and files" file: "./tools/diff_analyzer_tool.py" input_schema: type: "object" properties: diff_output: {type: "string"} pr_description: {type: "string"} - name: "generate_unit_test" description: "Create pytest test cases for modified functions" file: "./tools/test_generator_tool.py" input_schema: type: "object" properties: analyzed_diff: {type: "object"} target_module: {type: "string"} endpoints: - name: "generate-tests" method: "POST" path: "/v1/agent/generate-tests" request_schema: type: "object" properties: diff: {type: "string"} description: {type: "string"} module_name: {type: "string"}调用示例(模拟CI流水线):
# 在GitLab CI脚本中 DIFF=$(git diff HEAD~1) curl -X POST http://harness.internal:8000/v1/agent/generate-tests \ -d "{\"diff\":\"$DIFF\",\"description\":\"Add UART timeout handling\",\"module_name\":\"uart_driver\"}"返回结果(生成的test_uart_driver.py内容):
import pytest from uart_driver import UARTDriver def test_uart_timeout_handling(): """Test UART driver handles timeout correctly""" driver = UARTDriver(timeout_ms=1000) # ... generated test logic避坑经验:git diff输出可能超长(>10MB),直接传JSON会触发Uvicorn默认的--limit-concurrency。我们在docker-compose.yml里调大:
command: ["--host", "0.0.0.0", "--port", "8000", "--limit-concurrency", "100", "--limit-max-requests", "1000"]4.3 教学演示Agent:实时响应学生提问并调用仿真工具
场景需求:某高校AI课程需演示Agent如何调用外部工具。学生提问“画一个正弦波”,Agent应调用Matplotlib绘图并返回图片URL。
工具实现:math_calculator_tool.py计算数值,plot_tool.py绘图,file_server_tool.py提供HTTP文件服务。
config.yaml关键段:
tools: - name: "calculate_sine" description: "Calculate sine values for given x range" file: "./tools/math_calculator_tool.py" input_schema: type: "object" properties: start: {type: "number"} end: {type: "number"} step: {type: "number"} - name: "plot_sine_wave" description: "Plot sine wave and save as PNG" file: "./tools/plot_tool.py" input_schema: type: "object" properties: x_values: {type: "array", items: {type: "number"}} y_values: {type: "array", items: {type: "number"}} - name: "serve_file" description: "Serve a local file via HTTP" file: "./tools/file_server_tool.py" input_schema: type: "object" properties: file_path: {type: "string"} endpoints: - name: "teach-sine" method: "POST" path: "/v1/agent/teach-sine" request_schema: type: "object" properties: query: {type: "string"}调用示例(学生Web界面):
// 前端JS fetch("http://harness.internal:8000/v1/agent/teach-sine", { method: "POST", body: JSON.stringify({query: "画一个0到2π的正弦波"}) }).then(r => r.json()).then(data => { document.getElementById("plot").src = data.image_url; });返回结果:
{ "image_url": "http://harness.internal:8000/static/plot_abc123.png", "explanation": "正弦函数y=sin(x)在[0,2π]区间内..." }避坑经验:file_server_tool.py必须用http.server启动独立HTTP服务,且绑定0.0.0.0而非127.0.0.1,否则容器内服务无法被宿主机访问。我们用subprocess.Popen后台启动:
import subprocess import time def execute(params): # 启动静态文件服务 proc = subprocess.Popen([ "python", "-m", "http.server", "8001", "--directory", "/tmp" ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(0.5) # 等待服务启动 return {"image_url": f"http://harness.internal:8001/{params['filename']}"}5. 运维与迭代:如何让这个平台持续可用三年不重构
部署完成不是终点,而是日常运维的开始。我们总结出一套“五年不过时”的维护策略,核心是把变化关进配置,把不变固化进镜像。
5.1 模型热更新:不重启容器切换模型版本
业务方常提需求:“能不能不重启就换模型?”答案是:可以,但必须放弃“零配置”幻想。Harness本身不支持热加载,但我们用Docker的bind mount和inotify实现伪热更新。
步骤如下:
- 在宿主机创建模型版本目录:
/models/deepseek-r1-7b-chat-v1,/models/deepseek-r1-7b-chat-v2; docker-compose.yml中挂载父目录:volumes: - /models:/app/models:ro;- 写一个
model-switcher.sh脚本,监听/app/models/current软链接变化:#!/bin/bash inotifywait -m -e create,delete,move /app/models | while read path action file; do if [[ "$file" == "current" ]]; then echo "Model switch detected, reloading..." # 发送信号给Harness主进程触发重载 kill -USR1 $(cat /app/run/pid) fi done - 修改Harness的
server.py,捕获SIGUSR1信号:def reload_model(signum, frame): global model_manager logger.info("Reloading model from /app/models/current...") model_manager.unload_model() model_manager.load_model("/app/models/current") signal.signal(signal.SIGUSR1, reload_model)
这样,运维只需执行ln -sf /models/deepseek-r1-7b-chat-v2 /app/models/current,Harness就会自动卸载旧模型、加载新模型。实测7B模型重载耗时28秒,期间旧请求仍可处理,新请求排队等待。
5.2 日志归档:用rsyslog把Docker日志导出到NAS
Docker默认日志存在/var/lib/docker/containers/,随容器删除而消失。科研项目常需保留半年以上日志用于复现。我们用rsyslog做集中归档:
- 在宿主机安装rsyslog:
apt install rsyslog; - 创建
/etc/rsyslog.d/10-docker.conf:# 把docker日志转发到NAS $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat *.* @@192.168.1.100:514 # NAS的rsyslog地址 - 启动rsyslog:
systemctl restart rsyslog; - 启动Harness容器时加日志驱动:
logging: driver: "syslog" options: syslog-address: "tcp://127.0.0.1:514" tag: "harness-{{.Name}}"
这样,所有docker logs输出自动进入NAS的/var/log/harness/目录,按日期分割,保留365天。
5.3 安全加固:四步禁用所有非必要攻击面
局域网不等于绝对安全。我们默认关闭以下高危功能:
| 攻击面 | 加固措施 | 验证命令 |
|---|---|---|
| 未授权API调用 | 在nginx.conf前加Basic Auth代理 | curl -I http://localhost:8000/v1/models返回401 |
| 容器逃逸 | 启动时加--security-opt=no-new-privileges:true | `docker inspect harness |
| 敏感信息泄露 | 禁用Uvicorn的--reload模式(开发用) | docker exec harness ps aux | grep uvicorn不含--reload |