上个月我帮团队把 Deepseek Harness 部署到了云服务器上,顺带解决了公网访问和鉴权的问题。这套流程做完之后,整个部署从裸奔状态升级到了“没有 API Key + 账号密码 + IP 白名单三层校验就进不来”的安全状态。整个过程踩了不少坑,其中有些坑在文档里根本找不到答案,只能靠自己慢慢磨。今天把完整流程和背后的思路整理出来,希望能帮你少走点弯路。
这个部署流程适用的人群很明确:有 Linux 基础、用过 Docker、想要把自托管的模型网关或 Agent 编排平台暴露到公网供团队使用的开发者。不管你是要给内部工具做 AI 接口汇总,还是想把模型调度能力开放给多个业务方,这套“云服务器 + 容器化部署 + 反代鉴权”的思路都能直接用得上。我也会把每种选型的“为什么”讲透,而不是只丢命令给你抄。
1. 整体设计与思路拆解
1.1 核心需求分析
先把需求讲透。部署 Deepseek Harness,本质上不是简单地跑一个服务,而是要把整个模型调用链路收拢到一个统一入口。这个入口要解决以下问题:
第一,模型能力的统一暴露。现在团队里可能有人在写 Python 脚本直连 API,有人在用别的工具调用模型接口,还有人写着写着就把 API Key 硬编码在代码里了。Harness 要做的就是把这一堆散乱的调用方式,收敛成一个统一的管理入口。
第二,多用户场景的隔离问题。公网部署意味着不是你自己一个人在访问,可能是产品同学、前端同事、数据分析师都要用。每个人访问的权限范围、调用频率、用量统计都要分开。这就是为什么鉴权不能只做一层,而是要做分层。
第三,安全和可用性的兼顾。这是整篇文章的核心命题。很多人在云服务器上部署服务时,最常犯的错误是:直接给容器的 8000 端口绑定公网 IP,或者干脆在安全组里把端口全开。这个操作等于把你家里的门拆了,只在门口挂了一块“请随手关门”的牌子。正确思路是:服务进程本身只监听内网地址,公网入口由反向代理统一接管,鉴权层放在反代和 Harness 之间。
1.2 架构选型对比
我在动手之前对比了三套方案,这里直接给出结论和原因。
第一套方案是裸进程部署。直接在云服务器上装 Python 环境,用 systemd 管 Harness 进程。这套方案看起来轻量,但实际上把依赖管理、环境隔离、版本升级全变成了噩梦。尤其在服务器上还要跑其他业务的时候,Python 包版本冲突是迟早的事。
第二套方案是 Docker 单容器部署。比裸进程好一些,至少环境隔离了。但这个方案的问题是:日志、数据、配置全混在容器里,一旦容器重建,数据和配置都可能丢。而且单容器模式下,你没法单独扩容某个组件。
第三套方案就是最后采用的 Docker Compose 多服务编排。核心服务跑在容器里,数据挂载到宿主机,PostgreSQL 和 Redis 独立成容器,Nginx 在宿主机上做反向代理。这套方案的好处是:每一层职责清晰、日志隔离、数据持久化安全、组件可以单独伸缩。
具体拓扑如下:
- Harness 主服务:监听 127.0.0.1:8000,只接受本机请求
- PostgreSQL:存元数据和用户信息,不映射公网端口
- Redis:存会话状态和任务队列,同样不映射公网端口
- Nginx:监听 443,终结 TLS,做反向代理和鉴权
- 防火墙层:仅放行 22/80/443 三个端口
这个架构里最关键的一个决定就是“内核只监听 127.0.0.1”。这个决定让所有外部流量必须经过 Nginx 才能到达 Harness,这就构成了安全的第一道物理边界。
1.3 端口规划与容器编排原则
关于端口规划,有一个容易忽略但很重要的原则:容器端口映射不是越多越好,而是能不映射就不映射。
我见过很多人部署服务的时候,Redis 映射了 6379、PostgreSQL 映射了 5432,理由是“方便本地调试”。但代价是什么?云服务器上跑着nmap扫描的脚本比你想象的要多得多。任何一个暴露到公网的端口,都在被自动化工具反复试探。Redis 没设密码被提权的案例,在安全圈子里多了去了。
所以我的端口规划只有一条规则:凡是内部组件,一律不映射端口。只有 Harness 主服务的 8000 端口映射到宿主机的 127.0.0.1:8000。如果本地调试真的需要看 Redis 或者 PostgreSQL,用docker exec进容器或者用 SSH 隧道临时看,不要开公网端口。
Docker Compose 编排还有一个常被忽略的点:容器间的网络通信应该使用 Compose 内部的网络别名,而不是容器 IP。因为容器重建后 IP 会变,但网络别名是稳定的。Harness 容器访问数据库时,直接连postgres:5432,这个postgres就是 Compose 自动分配的网络别名。这样配置的好处是:无论容器怎么重建、IP 怎么变,服务间的连接字符串都不用改。
2. 环境准备与云服务器基础配置
2.1 云服务器选型建议
云服务器的配置建议直接给结论:双核 CPU、4GB 内存起步,硬盘至少 40GB,操作系统选 Ubuntu 22.04 LTS 或 Debian 12。如果团队并发访问量在 10 人以内,这套配置完全够用。如果后续要做模型推理或者 Agent 编排这类计算密集任务,建议 CPU 升到 4 核、内存升到 8GB。
选型时有一个很多人忽略的点:带宽和公网 IP 的分配方式。有些云厂商会给独立的弹性公网 IP,有些是 NAT 方式共享出口。我建议一定要选择支持独立公网 IP 的方案。原因在于:后面要做 HTTPS 证书、要做 DNS 解析、要做 IP 白名单,这些都需要一个稳定的、独立的外网出口。共享 IP 的方案看似省钱,遇到同 IP 其他用户被封或者被限速的情况,你会欲哭无泪。
安全组的配置也要在部署之前做好。记住一个原则:只放行必要端口。一般来说,22 端口用于 SSH(建议改成非默认端口或仅限你的办公 IP 访问)、80 端口用于 HTTP 跳转、443 端口用于 HTTPS。其他端口一律不放行。等部署好了再想改安全组,很容易因为漏改造成暴露面扩大。
2.2 Docker 环境安装与初始化
Docker 的安装有一点要注意:尽量不要用云厂商镜像里预装的 Docker,因为版本可能比较老,而且可能和系统自带的包管理器冲突。我习惯的安装方式是卸载旧版本后,用官方脚本全新安装。
安装完成之后,有几项初始化配置建议先做好:
创建普通用户并加入 Docker 组,避免日常操作都要sudo。我见过有人直接把所有服务跑在 root 下,结果容器内文件权限全乱了,后面修都修不回来。
配置 Docker 的日志滚动策略。这个必须在部署前配好,否则容器日志会无限增长,直到把磁盘塞满。就在/etc/docker/daemon.json里加一段配置。
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }配置好之后重启 Docker 服务。这步如果不做,跑上一两个月,磁盘就被日志撑爆了。我曾经在另一台服务器上见过/var/lib/docker/containers目录占了几十 GB 的情况,全是 JSON 日志文件,删的时候还得先停容器。预防成本就是一条配置的事,不值得偷懒。
2.3 项目目录规划
目录规划是很多人不重视但回报率极高的环节。一个好的目录结构,能让你在半年之后还能一眼找到配置文件和日志位置。
我推荐的目录结构如下:
/opt/deepseek-harness/ ├── compose.yml ├── .env ├── nginx/ │ ├── conf.d/ │ │ └── harness.conf │ └── ssl/ │ ├── fullchain.pem │ └── privkey.pem ├── data/ │ ├── postgres/ # 数据库数据文件 │ ├── redis/ # Redis 持久化文件(如果开启 RDB/AOF) │ └── harness/ # Harness 自身的数据目录 └── logs/ ├── nginx/ └── harness/所有数据都放在挂载卷里,容器只承担计算和逻辑。这样无论容器怎么重建、怎么升级版本,数据都不会丢。日志统一收在/opt/deepseek-harness/logs下,排查问题的时候直接进这个目录翻文件,比docker logs方便得多,因为 Nginx 可以配置成按天切分日志,历史日志不会挤占同一个文件。
3. 核心部署实操:从 Compose 编排到服务启动
3.1 编写 Docker Compose 编排文件
这部分是整个部署的核心,我会把 compose 文件拆开来讲,每段说明用途和选择原因。
version: "3.8" services: postgres: image: postgres:15-alpine container_name: harness-postgres restart: always environment: POSTGRES_DB: harness POSTGRES_USER: harness POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - /opt/deepseek-harness/data/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U harness -d harness"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: harness-redis restart: always command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes volumes: - /opt/deepseek-harness/data/redis:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 harness: image: deepseek-harness:latest container_name: harness-app restart: always depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: DATABASE_URL: postgresql://harness:${DB_PASSWORD}@postgres:5432/harness REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 SECRET_KEY: ${SECRET_KEY} API_KEYS: ${API_KEYS} MODEL_API_KEY: ${MODEL_API_KEY} ports: - "127.0.0.1:8000:8000" volumes: - /opt/deepseek-harness/data/harness:/data这里有几个关键细节要解释。
首先是depends_on的用法。我加了条件判断,只有 PostgreSQL 和 Redis 的健康检查通过后,Harness 才会启动。这是因为 Harness 启动时会做数据库迁移和初始化操作,数据库没就绪的话,启动大概率会报错退出。早期的 compose 只支持启动顺序(先启动依赖服务)但不管依赖服务是否就绪,加了condition: service_healthy才能做到“等我健康了你再启动”的效果。
其次是restart: always。这个策略的含义是:不管容器因为什么原因退出(正常退出除外),只要 Docker 守护进程还在运行,就会拉起重启。配合healthcheck,可以做到简单的自愈。如果 Harness 崩溃了,Docker 会在几秒内把它重新拉起,不会让服务长时间处于不可用状态。
再就是端口映射的写法“127.0.0.1:8000:8000”。这种写法是把容器的 8000 端口映射到宿主机的127.0.0.1:8000。注意这个127.0.0.1前缀很重要。如果不写这个前缀,Docker 默认会绑定到0.0.0.0,也就是公网也能直接访问容器的 8000 端口。虽然安全组没放行 8000 端口,但万一安全组配置失误,就成了一个没上锁的入口。
3.2 配置环境变量与密钥生成
环境变量我单独放在.env文件里,而不是直接写在 compose 文件中。这样做的原因显而易见:compose 文件往往要放进 Git 仓库共享给团队,但密钥密码不能进仓库。
.env文件的基本样子:
# PostgreSQL 密码,建议 32 位以上随机字符串 DB_PASSWORD=$(openssl rand -hex 32) # Redis 密码,同样建议强随机 REDIS_PASSWORD=$(openssl rand -hex 32) # Harness 的 Django/Flask Secret Key,用于会话加密 SECRET_KEY=$(openssl rand -hex 32) # 用于团队成员的 API Key,多个 Key 用逗号分隔 API_KEYS=sk-harness-user1-xxxx,sk-harness-user2-yyyy # 你实际调用模型的 API Key(这是 Harness 上游使用的) MODEL_API_KEY=sk-actual-model-key生成密钥的命令可以直接在服务器上跑:
openssl rand -hex 32这里有个容易被忽略的安全细节:.env文件的权限一定要设置成 600,只有 root 或者部署用户可读。如果服务器上同时跑着其他服务,配置文件权限过宽等于把密码送给别人看。
API Key 的设计我多说一句。我给团队里每个成员生成了独立的 Key,这样可以在 Harness 后台分别统计每个人的调用量和频率。千万不要所有人共用一个 Key,不然出了问题你根本不知道是谁在捣乱。如果 API Key 泄露了,单个 Key 吊销不影响其他人使用。
3.3 数据目录与持久化策略
数据持久化的设计是这套部署里容易被低估的部分。很多人部署完觉得能用就行,直到某一天容器挂了重启之后数据全丢了才追悔莫及。
我的策略是分三层考虑。
第一层,数据库数据。PostgreSQL 的数据目录挂载到/opt/deepseek-harness/data/postgres。这样即使容器被整个删掉重新创建,数据库里的用户信息、配置、历史记录都还在。同时我会定期用pg_dump做逻辑备份,备份文件存到另一个目录,再同步到对象存储或者远程服务器。
第二层,Redis 数据。Redis 的appendonly yes配置开启了 AOF 持久化,加上数据目录挂载,即使服务器断电重启,Redis 也能从 AOF 日志恢复。这里有个小技巧:Redis 的 AOF 文件会随着操作增长,可以在低峰期执行BGREWRITEAOF手动重写,压缩文件大小。
第三层,Harness 的应用数据。这个目录用来存 Harness 自身产生的文件,比如上传的模板、生成的报告、缓存的模型响应等。这个目录也要定期备份。
备份的频率我建议是:数据库每天凌晨 3 点自动pg_dump,应用数据每天增量同步一次,备份保留 7 天。够用,又不至于占用太多存储。
3.4 启动、验证与日常运维命令
配置完成后,启动命令很简单:
cd /opt/deepseek-harness docker compose up -d-d参数让所有容器后台运行。执行完之后用下面的命令确认三个容器都处于 healthy 状态:
docker compose ps输出里会显示每个容器的状态是running (healthy)还是running或者restarting。如果只有running没有healthy,说明 healthcheck 还没通过,等几秒再查看。如果一直停在restarting,基本是启动就崩溃在重启循环里,这时候要查日志。
验证 Harness 主服务有没有正常监听:
curl -s http://127.0.0.1:8000/health返回{"status":"ok"}类似的 JSON 就说明服务起来了。这时候你在服务器本机访问是没问题的,但公网还不能访问,因为 Nginx 还没配置。
日常运维里,我自己会用一个简单的巡检脚本,每隔 5 分钟检查一次三个容器是否正常。如果某个容器异常,脚本会调用 Webhook 发通知到内部群。这套补丁式的监控在团队规模不大的时候,比上完整的监控系统要划算得多。给容器配置了log-opts max-size之后,日志文件运维过一阵子就会比较省心,几十上百 MB 的大日志文件基本见不到了。
4. 公网访问与多层鉴权配置
4.1 Nginx 反向代理配置
Nginx 是整个公网访问体系的守门人。所有外部流量必须先经过它,由它做 TLS 终结、鉴权、转发。配置如下:
server { listen 80; server_name harness.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name harness.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 基础鉴权:账号密码 auth_basic "Restricted Access"; auth_basic_user_file /etc/nginx/.htpasswd; # IP 白名单 allow 122.10.55.xxx; allow 180.20.11.xxx; deny all; location / { # 转发到本机 Harness 容器映射端口 proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_read_timeout 300s; } location /health { # 健康检查接口不做鉴权,方便监控 proxy_pass http://127.0.0.1:8000/health; allow 127.0.0.1; deny all; } }这里要说几个容易被忽略的点。
proxy_read_timeout我特意调到了 300 秒。默认值是 60 秒,对于普通的 Web 应用够用,但 Harness 这种有模型调度、Agent 编排能力的服务,一个请求可能要处理比较久才能返回。如果模型推理慢一些、上游接口响应慢一些,60 秒超时直接给你返回 504。这个坑我踩过一次,排查了半天,最后才发现是 Nginx 默认超时太短。
location /health单独放行给本机监控用,不套鉴权。这样监控脚本可以方便地探测服务是否存活,而不用带着账号密码和 Key。生产环境做健康检查时,这个路径非常有用。
4.2 两层基础鉴权:HTTP Basic Auth 与 API Key 校验
Nginx 层的鉴权我用了两层,每一层的定位都不同。
第一层是 HTTP Basic Auth。这里的作用是“挡住随便扫到端口的人”。用htpasswd命令生成密码文件:
htpasswd -c /etc/nginx/.htpasswd admin输入两次密码就生成了。Basic Auth 的强度确实不算高,但它便宜、直观、Nginx 原生支持,给第一道门加个锁足够了。关键问题是,这层只要拦住绝大多数路过的人,真正的安全还得靠后面的防线。
第二层是 API Key 校验。这一层在 Harness 应用内部实现。也就是你在.env里配置的API_KEYS,Harness 框架会对每个请求校验Authorization头里的 Key 或者X-API-Key头。如果 Key 不匹配或者缺失,直接返回 401。
为什么需要 Basic Auth 和 API Key 同时存在?因为它们是两层独立的防线。Basic Auth 拦在 Nginx 层,API Key 校验在应用层。即使第一层被攻破了(比如密码字典爆破成功),攻击者进入应用层还有第二道门,调用模型和读写数据的能力仍然被锁住。这就是纵深防御里最基本的多层冗余理念。
4.3 HTTPS 证书申请与自动续期
公网访问必须上 HTTPS,这条没有商量余地。原因不只是“浏览器会提示不安全”,更重要的是 HTTP 明文传输会把你的 API Key 和安全凭证全部暴露在网络上。如果你的账号密码和 API Key 走 HTTP 传输,那就像把保险箱钥匙放在家门口的花盆底下。
证书申请我用的是 certbot 的 Nginx 插件,一步搞定:
apt install certbot python3-certbot-nginx certbot --nginx -d harness.example.comcertbot 会自动修改 Nginx 配置、申请证书、配置好 TLS。重要的是后续的自动续期。certbot 会创建一个 systemd timer,每周自动检查证书剩余有效期并执行续期。但还是建议自己加一个 crontab 做双保险:
0 3 * * * /usr/bin/certbot renew --quiet续期完成后需要重载 Nginx 让新证书生效:
systemctl reload nginx我习惯把这两行写成一个脚本,放在 cron 里,每天半夜检查一次。这样证书还剩多少天、有没有续期成功,都是有据可查的。证书有效期过了导致服务中断的案例我见过不止一次,全是因为忘了续期这件事。
4.4 加固层:连接数与限流控制
公网服务跑久了,你一定会遇到恶意扫描和突发流量。Nginx 在这一层的处理很重要。
连接数限制。每个人 IP 同时最多开 30 个连接,超出直接拒绝:
limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { ... limit_conn conn_limit 30; }请求速率限制。API 接口按 IP 限制每秒最多 5 个请求,超出返回 429:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s; location /api/ { limit_req zone=api_limit burst=10 nodelay; proxy_pass http://127.0.0.1:8000; }这两个配置对正常用户完全无感,但对恶意脚本和故障代码来说,直接封住了一整类问题。我看到过有同事的脚本死循环调用接口,如果没有限流,整个 Harness 服务会被拖死,其他用户全部卡住。加了限流之后,这种事故最多只是那一个用户被限流,不影响其他人。
还有一个更硬的防护:fail2ban。它可以监控 Nginx 的鉴权失败日志,如果某个 IP 在短时间反复触发 401 鉴权失败,自动调用 iptables 拉黑这个 IP。这个工具我之前试过,对爆破密码的场景效果拔群,但要注意配置好白名单,别把自己团队的 IP 误伤了。
5. 常见问题与排查实录
5.1 高频问题速查表
把这段时间遇到的典型问题整理成了表格,方便你对照处理:
| 症状 | 可能原因 | 解决方式 |
|---|---|---|
| 公网访问提示 502 Bad Gateway | Harness 容器没启动成功,或 healthcheck 还在进行中 | docker compose ps看容器状态,查看docker compose logs harness |
| 访问时报 504 Gateway Timeout | Nginx 的 proxy_read_timeout 太短 | 调整到 300s 以上并 reload Nginx |
| 页面能打开但登录一直失败 | 数据库初始化未完成,或 API Key 配置不一致 | 检查 Harness 容器的启动日志,确认数据库迁移已完成 |
| URL 能访问但提示连接不安全 | 证书过期,或证书链不完整 | 执行 certbot renew,或检查fullchain.pem是否包含中间证书 |
| 访问 HTTPS 直接拒绝连接 | 安全组未放行 443 端口 | 到云厂商控制台检查安全组规则 |
| 接口响应很慢且有大段等待 | 上游模型 API 响应慢,或 Nginx 缺少 proxy_buffering 配置 | 调大超时时间,合理配置缓存,必要时做异步任务而非同步等待 |
| 容器反复重启,状态一直是 restarting | 启动时依赖的服务(数据库/Redis)未就绪,或配置错误 | 看容器日志,确认依赖服务健康,检查环境变量是否正确 |
5.2 两个踩过的坑,值得你绕开
第一个坑是关于proxy_set_header的。我第一版配置里只写了Host和X-Real-IP,没有X-Forwarded-Proto。结果 Harness 后台生成的页面里,静态资源 URL 全被写成了 HTTP 而不是 HTTPS。浏览器加载这些资源时被拦掉,页面样式全乱了。排查了半天才发现是反代没有把原始请求协议告诉应用。加上proxy_set_header X-Forwarded-Proto $scheme;之后立刻恢复。
第二个坑是 API Key 的换行符问题。我从一个在线工具站复制了几组 Key 到.env里,结果始终有一组 Key 校验不过。查了很久才意识到,在线工具生成的 Key 末尾带了\n换行符,被环境变量读进去之后和请求头里的 Key 对比永远不一致。这种隐蔽的字符问题最磨人,建议生成密钥后先检查一下有没有多余的空格和换行,或在.env里编辑完顺便跑一遍 key 比对测试。
5.3 日常运维与监控建议
部署完成只是开始,长期稳定的运维才是真正的考验。根据自己的实际体验,给出几条运维建议。
日志监控不要等到出了问题再去看。我目前的做法是把 Nginx 的访问日志按天切分,并做了一个简单的统计脚本,每天统计当天的 4xx、5xx 状态码数量。如果某个时间段异常增长,说明可能有程序在爆破或者业务出现了大面积错误。比起装一套完整的监控系统,这种轻量脚本在团队规模不大的时候足够实用。
数据库备份要经常做恢复演练。再强调一遍备份的重要性,备份文件存了一堆,但从来没恢复过,等于没有备份。我每两周会在一台测试服务器上恢复一次备份数据,确认备份文件可用。成本不高,但线上数据出问题时,心里有底。
最后一点,给 Harness 的升级保留一个“可回滚”的路径。升级前先docker compose pull拉新镜像,然后在.env里打个备份,如果有异常,用备份文件快速回退。我在一次升级后遇到过兼容性问题,因为之前留了备份,回滚只花了五分钟,团队几乎无感知。
6. 结语:关于这套方案的一些个人体会
整个部署跑下来,我最深的体会是:部署本身不难,难的是把每个环节的安全意识贯穿始终。云服务器上的服务,默认就应该假定网络环境是不友好的。监听地址从0.0.0.0改成127.0.0.1,端口映射把范围从大到小收口,反代层加 Basic Auth 再在应用层加 API Key 校验,每一步都不是操作上的高难度动作,但合在一起,就是一层层递进的安全纵深。
自己踩过坑之后,我也学会了排查问题时先看“是不是 Nginx 超时”,再看“是不是证书过期了”,而不是盲目地重启容器。这些经验在文档里不一定找得到,但都是运维常识的一部分。如果你按照这套流程部署,第一遍可能也会遇到我踩过的某一个坑。不要慌,按表格里的排查思路走,基本能在 10 分钟内定位问题。把每一次踩坑的时间花进去,后面服务运行稳定带来的收益是值得的。