做资源导航站那阵子,我每天最不想干的事就是打开一堆网盘分享链接逐个验证。后来我在 GitHub 上翻到 PanCheck 这个项目——一个专门做网盘链接检测的自建服务,直接打算用容器化部署到自己服务器上,从此链接巡检基本没再手动碰过。这篇东西就把整个部署流程、踩过的坑、以及运行半年后的维护经验完整记录下来,给需要自建网盘链接检测服务的朋友当参考。不管你是第一次碰 Docker 的初学者,还是已经跑过不少容器服务的老手,这套流程照着走都能少走弯路。
1. 动手之前:PanCheck 到底解决了什么问题
1.1 手动验证网盘链接的老大难问题
网盘分享链接的失效原因多种多样——有效期到期、分享被取消、文件因举报被删除、上传者主动撤回、后台检测到违规自动屏蔽等。对个人来说,一两个链接失效无所谓;但如果手里有几十上百条链接的清单,比如资源导航站、课程资料整理、社群文件索引,失效链接就会直接影响访问者的信任度。
失效这种事不是一次性的,今天有效的链接下个月可能就没了,必须周期性巡检。手动操作时,每条链接要经历:打开页面、等待加载、人工判断结果、记录状态、换下一条。一百条链接就是一百次重复劳动,而且判断标准还不统一,有人把“需要提取码”也算失效,有人则不算。
PanCheck 解决的就是这个场景:把链接批量导入,由程序自动访问、自动判断、自动记录,还能按照设置好的频率定期复查,把人工巡检变成后台静默执行的任务。我实际用下来最大的感受是:它不是在“替你点链接”,而是在“替你维护一张链接健康表”,状态变化一目了然,该处理哪条直接看列表就行。
1.2 PanCheck 是怎么设计检测逻辑的
我实际用下来,PanCheck 的检测不是简单发一个 HTTP 请求看看返回 200 就算完事,而是混合了多种判断策略。
第一层是状态码判断,404、410 这类基本可以直接认定资源没了;第二层是页面内容特征匹配,不同网盘在分享页失效时会渲染出固定文案,比如“链接已失效”“分享文件已被取消”“你来晚了,分享的文件已经被删除了”之类的提示,通过匹配这些特征词做二次确认;第三层是重定向跟踪,有些链接会跳转到登录页或者首页,说明该分享可能需要登录、可能存在风控,结果会被标记为“异常”而不是简单的“失效”。
部分网盘因为前端是 JS 动态渲染的,PanCheck 还支持用无头浏览器做真实访问,代价是更耗内存和 CPU,但准确率会高不少。理解这一层,对后面配置检测参数很有帮助——比如你觉得某个网盘类型误报太多,多半就是特征词库没有及时更新,或者需要打开浏览器渲染模式来应对页面动态化。
1.3 为什么一定要用容器化部署
从部署方式来说,PanCheck 依赖的组件不少:Web 服务、异步任务执行器、缓存队列、数据库,如果直接在宿主机上裸装,光是 Python 依赖、Redis 版本、PostgreSQL 权限这些就够折腾半天。而且每个人的宿主机环境不一样,别人能在 CentOS 上跑起来,换到 Ubuntu 可能就是另一堆问题。
用 Docker 之后,这些依赖全部被锁进镜像里,宿主机只要有一个 Docker 环境就能跑。升级的时候也是一样,旧容器停掉、新容器拉起来,数据在数据卷里保存着,基本无感。回滚更简单,镜像 tag 切回去就完事。这种“基础设施代码化”的思路,跟现在很多开源项目的容器化部署方案是一致的,Dify 这类平台的容器化部署同样是依赖 Docker Compose 把多个服务编排在一起,你如果以前折腾过那一套,再看 PanCheck 的 compose 文件会非常亲切。
2. 部署前的环境规划与镜像选择
2.1 服务器配置与操作系统选择
PanCheck 整体上算轻量应用,但需要同时跑 4 个容器,内存是主要瓶颈。我的建议是至少 1 核 2G,如果检测任务量大,或者打算开启无头浏览器检测模式,直接上 2C4G,体验完全不一样。1G 内存的机器不是不能用,只是 Redis 和 PostgreSQL 会把内存吃得很紧,Worker 一旦并发跑十几个检测请求,很容易触发 OOM,表现为容器反复重启、任务一直卡在 pending 状态。
磁盘方面,20G 起步吧,系统、镜像、数据卷、日志加起来,时间长了 10G 是压不住的。操作系统用 Debian 12 或者 Ubuntu 22.04 LTS 都行,我不太建议用太老的发行版,因为新版 Docker 对旧内核的兼容性越来越差;如果你手头只有 CentOS 7,内核 3.10 跑新版 Docker 会碰到各种奇怪问题,尽量升级系统再说。
2.2 Docker 环境准备
这一步虽然是基础,但最容易出问题。以 Debian/Ubuntu 为例,安装 Docker 的标准流程是先加官方 apt 源再安装:
sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin注意最后一行,新版 Docker 已经把 Compose 作为插件直接打包了,装完就有docker compose命令,不需要再单独安装老旧的 docker-compose 二进制。验证方法:
docker --version docker compose version docker run --rm hello-world有一条经验:如果你是真·新手,别在系统自带软件源里直接 apt install docker,那往往是老版本,Compose 支持不全,后面编排多容器服务时会踩到语法不兼容的坑。
2.3 镜像版本选择策略
PanCheck 的官方镜像发布在 Docker Hub 上,仓库地址你要在项目 README 里确认一下,我下面示例里统一用pancheck/pancheck作为占位。这里有一个通用建议:不要长期跟着 latest 标签走。latest 的问题在于你无法确定它什么时候变了、变成了什么版本,出了问题不好回滚。
我习惯的做法是到镜像仓库页面先把版本列表翻一遍,找一个最近的稳定版,比如 1.6.x,然后在 compose 文件里固定写死版本号。等项目发布新版本、确认更新日志里有值得升级的内容,再手动改版本号拉新镜像。另外,如果项目同时提供 web 和 worker 两个镜像,尽量保证它们用的是同一个版本号,否则可能出现消息格式不兼容、任务反序列化失败的诡异问题。
镜像来源也要看一眼。尽量从官方仓库或者可信的个人仓库拉取,不要贪图“某个镜像源更快”就随便换源拉一个同名镜像,镜像被篡改的事情在开源生态里不是没发生过。如果你不知道怎么判断,就记住一条:README 里给的命令是什么,你就用什么。
3. 用 Docker Compose 编排整套服务
3.1 服务器目录规划
部署前先在服务器上规划好目录结构,后面维护会省很多事情。我用的是:
/opt/pancheck/ ├── docker-compose.yml ├── .env ├── data/ │ ├── postgres/ │ └── redis/ └── logs/docker-compose.yml和.env放在根目录,数据目录和日志目录单独挂出来。这样做的目的很明确:容器随时可以删掉重建,但数据在宿主机上不会丢。日志单独放一个目录是方便做日志轮转和排查问题,不用每次进容器里翻。
3.2 docker-compose.yml 逐段拆解
下面这份是我在实际环境里跑着的 compose 文件,核心结构删掉了无关的注释:
services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: pancheck POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: pancheck volumes: - ./data/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U pancheck"] interval: 10s timeout: 5s retries: 5 networks: - app redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] volumes: - ./data/redis:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: - app web: image: pancheck/pancheck:1.6.2 restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: DATABASE_URL: postgresql://pancheck:${POSTGRES_PASSWORD}@postgres:5432/pancheck REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY} TZ: Asia/Shanghai volumes: - ./logs:/app/logs ports: - "127.0.0.1:8000:8000" networks: - app worker: image: pancheck/pancheck:1.6.2 restart: unless-stopped command: ["python", "main.py", "worker"] depends_on: postgres: condition: service_healthy redis: condition: service_healthy web: condition: service_started environment: DATABASE_URL: postgresql://pancheck:${POSTGRES_PASSWORD}@postgres:5432/pancheck REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY} TZ: Asia/Shanghai volumes: - ./logs:/app/logs networks: - app networks: app: driver: bridge几个关键点分别说一下。第一,端口我绑的是127.0.0.1:8000:8000,也就是只允许本机访问,公网流量全部走 Nginx 反代。这样做的目的是少暴露一个面,不要直接把 8000 端口暴露到公网让所有人裸连。第二,depends_on配了健康检查条件,确保 Web 和 Worker 在数据库真正就绪后才启动,避免了最常见的“容器启动顺序不对导致反复崩溃”问题。第三,数据卷用的是 bind mount(宿主机目录挂载),这个比 named volume 更直观,备份时直接打包目录就行。
3.3 .env 文件与密钥管理
compose 文件里的密码和密钥通过环境变量注入,而不是写死。.env文件长这样:
POSTGRES_PASSWORD=change_this_strong_password SECRET_KEY=change_this_tooSECRET_KEY可以用一条命令生成随机值:
openssl rand -hex 32生成之后填进去就行。这里有一个所有部署里都必须做的动作:.env文件不要提交到 Git 仓库里,不要把密码发到聊天群里,不要把密钥粘贴到截图里。我见过有人在调试的时候顺手把.env内容贴到 issue 里,结果几分钟后服务器就开始被挖矿程序扫描。该保密的必须保密。
4. 初始化、迁移与首次启动
4.1 首次启动顺序与日志观察
配置文件准备好之后,切换到项目目录直接启动:
cd /opt/pancheck docker compose pull docker compose up -d docker compose psdocker compose ps的输出里,如果四个服务都显示 healthy 或者 running,那基本就绪了。第一次启动建议打开一个终端实时看日志:
docker compose logs -f主要看两件事:一是 Web 服务有没有正常打印监听地址,二是 Worker 有没有成功连接到 Redis 和 PostgreSQL。如果 Worker 一直在打印重试连接的错误信息,多半是数据库还没就绪,正常现象,等一会儿就好;如果持续几分钟还不行,再检查是不是健康检查配置有问题,或者数据库密码不匹配。
4.2 数据库迁移与管理员账号创建
多数项目第一次跑都要先执行数据库迁移,PanCheck 的后端是 Python 写的,初始化命令在项目文档里有明确说明,我这边执行的是这一组(不同版本可能略有差异,以 README 为准):
docker compose exec -T web python manage.py migrate docker compose exec -T web python manage.py createsuperusermigrate会在数据库里建好所有表结构,如果不执行,页面访问基本都会报 500 或者表不存在的错误。createsuperuser按提示输入用户名、邮箱、密码,创建完后用这个账号登录后台。
另外一个容易忽略的点是时区。容器默认时区是 UTC,如果项目内部用datetime.now()生成任务时间,而你的服务器是北京时间,就会出现“计划在凌晨 3 点跑的巡检任务,实际在上午 11 点才跑”这种错位。我在 compose 里已经加了TZ: Asia/Shanghai,如果你部署的机器不在这个时区,记得改成你自己的。
4.3 用 Nginx 做统一入口
Web 容器只监听在 127.0.0.1:8000,外面要访问就得加一层反向代理。Nginx 的配置段如下:
server { listen 80; server_name pan.example.com; client_max_body_size 50m; location / { 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; } }如果机器上已经有 Nginx,直接把这段塞进 sites-available 里,再做个软链到 sites-enabled,然后nginx -t && systemctl reload nginx。没有 Nginx 的话装一下就行:apt install nginx。如果你不想维护 Nginx 配置,用 Caddy 会更省事,官方镜像一条命令就能起一个带自动 HTTPS 的反代,但对新手来说概念又多了一层,自己权衡。
5. 核心功能验证:链接检测是怎么跑通的
5.1 界面里添加链接
服务跑起来后,浏览器访问http://pan.example.com,用刚才创建的管理员账号登录。添加链接这一步,PanCheck 支持手动逐条添加,也支持批量粘贴——把链接按行粘贴进去,一次提交十几条、几十条都没问题。提交之后界面会看到每条链接的状态从“待检测”变成“检测中”再变成最终状态,这个过程通常几秒到几十秒不等,取决于网盘方的响应速度和 Worker 的并发设置。
我建议你在正式导入大量链接之前,先拿三五条不同状态的链接做一次小批量验证:一条正常有效的、一条故意改成错误文件 ID 的、一条带提取码的、一条需要登录才能访问的。把这几种情况跑一遍,基本就能确认你的部署是正常的,后面再导入几千条也不会心里没底。
5.2 Worker 是怎么处理任务的
在界面提交任务只是第一步,真正干活的其实是 worker 容器。它的执行链路是:Web 把任务写入 PostgreSQL 的任务表,同时往 Redis 队列里推一条消息;Worker 订阅这个队列,拿到任务 ID 后从数据库读取链接信息,然后调用对应的检测器发起请求。
为什么要拆成异步而不是直接同步请求?因为检测一个链接可能需要几秒甚至十几秒(遇到 JS 动态渲染的页面还要启动无头浏览器),如果用户在提交链接的 HTTP 请求里同步等待,接口延迟会高得离谱,而且一旦检测任务量上来,Web 服务很容易被拖垮。拆成异步之后,Web 只负责收任务、展示结果,真正的耗时操作全部交给 Worker 慢慢消化,两者互不拖累。
如果你怀疑某个链接检测不出来,最直接的排查方法是看 Worker 日志:
docker compose logs -f worker正常的日志会一条条打出检测记录,包含链接 ID、目标 URL、最终判定的状态和耗时。如果发现任务提交了但 Worker 没反应,优先检查 Redis 队列是不是有积压、Worker 有没有崩。
5.3 检测结果的状态含义
PanCheck 的检测结果不是非黑即白,我整理了一份状态表:
| 状态 | 含义 | 常见场景 |
|---|---|---|
| 有效 | 链接可以正常访问分享页面 | 普通正常分享 |
| 已失效 | 资源不存在或链接被取消 | 404、分享已取消 |
| 需提取码 | 页面需要提取码才能进入 | 设置了访问密码 |
| 需登录 | 页面跳转到登录页 | 分享设定了仅指定用户可见 |
| 异常 | 无法稳定判断,需要人工复核 | 触发风控、页面结构变更、网络超时 |
这里有一个值得注意的实际问题:不要把“需提取码”当成“失效”。很多人的维护脚本一刀切,看到非 200 就标记失效,结果把一批其实还能用的链接给误杀了。而在真实的资源分发场景里,带提取码的链接往往是资源方刻意设置的访问控制,不是不可用。所以我在用 PanCheck 时会给不同状态配置不同的后续动作:已失效的进回收站待确认,需提取码的保持原样但单独建一个分组方便追踪。这种细分粒度,是手动验证完全做不到的。
6. 从踩坑到稳定运行的常见问题
6.1 容器磁盘占用越来越大
跑了一段时间后我发现服务器磁盘告警,查到最后是容器日志文件在无限膨胀。Docker 默认把日志作为 JSON 文件存在宿主机上,如果不加限制,长时间运行后单个容器的日志文件能到好几个 GB。解决办法是在/etc/docker/daemon.json里加上日志轮转配置:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "5" } }然后重启 Docker 服务让配置生效。这个配置对后面新创建的容器生效,已经产生的巨型日志文件手动清一次:
truncate -s 0 /var/lib/docker/containers/*/*-json.log注意清理之前先看一眼磁盘空间,如果你日志文件确实很大,truncate 到 0 之后可以立刻释放空间。如果还想压缩的话,可以先 tar 打包再清理,没必要保留庞大的原始日志。
6.2 Worker 内存涨得停不下来
第二个坑是 Worker 内存持续上涨,最后被系统 OOM Killer 干掉,容器反复重启。排查路径是这样的:先用docker stats看每个容器的内存占用,确认是 worker 在涨;再去看 Worker 日志,发现同一批任务反复出现重试记录。
根因有两点,一是检测请求的并发数配得太大,网盘方响应慢时大量请求挤在一起,内存里的 HTTP 连接和页面内容没有及时释放;二是某些无头浏览器实例用完没有被正确关闭,等于每次检测都泄漏一块内存。解决思路分几步:
- 在管理后台或者配置里把并发数调到比较保守的值,我当时调成 4;
- 如果代码里暴露了浏览器实例的复用逻辑,确认它有没有空闲超时销毁;
- 给 worker 容器加一个
mem_limit: 1g的硬限制,超过就由系统 OOM,再由restart: unless-stopped拉起来,至少不会把整个服务器拖死。
6.3 网盘方限流导致大量异常状态
大量批量检测一定会引起网盘方注意,尤其是短时间内对同一个网盘域名发起几十上百个请求,很容易触发风控。表现就是刚才还能正常检测的链接,突然一大批全部返回“异常”,页面可能被重定向到验证码页。
解决这个问题的核心思路是三个字:降强度。把并发降下来,把每次请求的间隔拉长,给队列任务设置一个合理的调度频率。检测本来就是后台任务,不是实时服务,没必要追求秒级完成。我现在的配置是每个 Worker 同时最多处理 4 个任务,每个任务之间间隔至少 1 秒,跑一个几千条链接的任务清单大概需要一两个小时,但链路非常稳定,基本不会再出现整批风控的情况。另外要注意,不同的网盘服务商风控策略差异很大,单独设置阈值时建议参考项目文档给出的推荐值。
6.4 升级之后任务反序列化失败
有次我把版本从 1.5.x 升到 1.6.x,升级后旧的 pending 任务全部报错,任务队列里全是反序列化失败的日志。原因是新版本的异步任务代码改了消息序列化结构和字段名,而 Redis 里还积压着旧版本提交的任务消息,Worker 用新代码一读就懵。
这个问题的解决方案现在也变成我的标准升级流程:升级之前先停 Worker,把队列里的积压任务清掉或者等它跑完,再升级代码和镜像,最后启动新 Worker。如果是无缝升级,至少也要把旧任务全部标记为失败,不要在中间状态上硬撑。
7. 上线后的维护建议
7.1 定时巡检怎么落
链接检测服务最有价值的功能就是定期巡检,让所有链接自动保持最新状态。实现方式不复杂,PanCheck 提供了命令行巡检入口,在宿主机上写一条 crontab 即可:
0 3 * * * cd /opt/pancheck && docker compose exec -T web python manage.py check_all >> /opt/pancheck/logs/cron.log 2>&1每天早上 3 点跑一次全量巡检,错开业务高峰。注意 crontab 使用的是宿主机时区,跟容器内的TZ没有关系,如果你的系统时区不是东八区,时间要自己换算。日志重定向到 cron.log 是为了出问题时能快速定位。
如果你手里的链接量比较大,也可以拆成分批巡检:周一到周六每天晚上巡检一部分,周日不跑,避免某一次全量任务把网盘方惹毛。
7.2 日志与系统监控
维护阶段有一个朴素的道理:你不可能等到用户告诉你服务挂了再动手。至少要保证日志可查、进程可看、资源可控。日志层面,除了 Docker 的日志轮转,我还写了一个简单的日志检查脚本,每天扫一遍 logs 目录里的 ERROR 关键字,发现异常就发一封通知邮件。资源层面,每天看一眼docker stats的趋势,重点关注 worker 的内存和四个容器的整体 CPU 占用。
如果以后规模大了、要管多台机器,可以考虑上 Prometheus + Grafana,但对 PanCheck 这个体量来说,刻意上全家桶反而增加维护成本,没必要。先把手里的日志和资源数据用起来,比堆工具可靠得多。
7.3 数据备份与恢复
数据库里存的是所有链接和检测历史的唯一数据源,容器可以丢,数据库不能丢。备份我用的是 PostgreSQL 官方工具:
docker compose exec -T postgres pg_dump -U pancheck pancheck > /opt/pancheck/backups/pancheck_$(date +%F).sql再配合一条 crontab 每天凌晨备份,保留最近 7 天的文件,更早的自动删除:
find /opt/pancheck/backups -name "pancheck_*.sql" -mtime +7 -delete备份有没有用,要看恢复演练过没有。起码做一次:先把服务停掉,用一个全新的数据库卷,把备份文件导进去,确认数据和检测记录都在。没做过恢复演练的备份,只能算心理安慰。
最后再分享一个很小但很实际的体会:PanCheck 这类自建服务,部署完那一刻往往是最有成就感的,真正的考验是后面三个月。链接检测服务的准确性高度依赖各个网盘网站的变化,某天某个网盘改版了页面结构,哪怕只是一个 CSS class 变了,特征匹配就可能失效,你会看到某一个网盘类型的所有链接整批出现误判。这时候别慌,先看日志确认是不是特征词没有命中,更新一下对应规则就好。容器化部署让你随时可以重建、回滚、搬迁,这恰恰是它最大的底气。希望这份流程能帮你少踩几个坑,把更多时间留给真正需要人的判断力的事情上。