1. 项目概述为什么这个组合成了 Django 生产部署的“稳态三角”最近在帮某高校实验室部署一个课程设计管理系统后端用的是 Django 3.2前端是 Vue 单页应用需要跑在公网可访问的服务器上。客户明确提了三点硬性要求必须 HTTPS 访问、不能暴露开发服务器端口、后续要方便增删其他 Python 服务。我当场就拍板用了宝塔面板 Docker uWSGI Nginx SSL这套组合——不是为了炫技而是这套方案在真实运维场景里把“安全”“隔离”“可维护”三个痛点一次性摁死。你可能听过“Django 自带 runserver 只能本地调试”也见过有人直接用python manage.py runserver 0.0.0.0:8000暴露到公网结果第二天就被扫出一堆日志告警。更常见的是用 Nginx 反向代理到 Gunicorn但一旦项目多了Python 环境冲突、依赖版本打架、端口手动分配混乱三天两头要重启服务。而这个标题里的五个关键词每个都不是摆设宝塔面板是运维入口它不参与请求处理但把 Linux 服务器从命令行黑盒变成了可视化操作台尤其对非专职运维人员比如高校老师、创业公司产品经理极其友好Docker解决环境一致性问题Django 项目打包成镜像后Ubuntu 20.04、CentOS 7、甚至 macOS 本地测试环境运行效果完全一致彻底告别“在我机器上是好的”uWSGI是真正的应用服务器它比 Gunicorn 更重、更可控支持进程管理、内存限制、优雅重启、多线程/多进程混合模式特别适合中等流量的业务系统Nginx不只是反向代理它承担静态文件分发CSS/JS/图片、HTTP/2 支持、请求限流、SSL 终止、负载均衡预备位是整个链路的“交通指挥中心”SSL已不是可选项而是底线。Chrome 对 HTTP 站点打“不安全”标微信内嵌浏览器直接拦截 HTTP 调用连 Django 的 CSRF 保护机制都默认要求SECURE_SSL_REDIRECT True。这个组合不是技术堆砌而是层层分工Docker 提供沙箱uWSGI 处理 Python 请求Nginx 做网络层调度和安全兜底宝塔做日常维护界面SSL 是信任基石。我试过纯命令行部署也试过只用宝塔内置 Python 项目管理器最后发现——只有把 Docker 当容器、uWSGI 当引擎、Nginx 当交警、宝塔当仪表盘、SSL 当车牌整辆车才能开得又快又稳。2. 整体架构设计与选型逻辑为什么不用 Gunicorn为什么绕开宝塔的“一键部署”2.1 架构图不是画出来的是踩坑画出来的先说结论最终采用的是“Nginx宿主机 → uWSGIDocker 容器内 → Django同一容器”的三层结构其中 Nginx 运行在宿主机由宝塔管理uWSGI 和 Django 运行在独立 Docker 容器中。这不是教科书推荐而是我在三台不同配置服务器上实测对比后的选择。我对比过四种主流部署方式部署方式环境隔离性SSL 配置复杂度多项目扩展成本宝塔兼容性实测稳定性7天无故障宝塔内置 Python 项目管理器Gunicorn❌ 共享系统 Python 环境⚠️ 需手动改 Nginx 配置⚠️ 端口需人工规划易冲突✅ 原生支持62%因 pip 依赖升级导致崩溃宿主机 Nginx 宿主机 uWSGIsystemd 管理⚠️ Python 环境需 virtualenv 隔离✅ 宝塔可一键申请证书⚠️ 每个项目需单独写 systemd unit 文件✅ 可接管89%但升级 uWSGI 后需重启全部服务Docker Nginx容器内 uWSGI容器内✅ 完全隔离⚠️ SSL 证书需挂载进容器更新麻烦✅ 复制 docker-compose.yml 即可❌ 宝塔无法管理容器内 Nginx95%但宝塔监控失效Docker uWSGI容器内 Nginx宿主机宝塔管理✅ 隔离 Django 环境✅ 宝塔自动续签证书路径固定✅ 新项目只需改 Nginx 反代地址✅ 完全兼容99.2%唯一故障是磁盘满非架构问题提示很多人卡在“为什么 Nginx 不放进 Docker”答案很实在宝塔的 SSL 证书自动续签功能本质是定时执行certbot renew并重载 Nginx。如果 Nginx 在容器里每次续签都要进容器 reload还要处理证书文件挂载权限等于把宝塔最省心的功能废掉了。我们让宝塔管它最擅长的事——Web 服务和证书让 Docker 管它最擅长的事——应用环境。2.2 为什么坚持用 uWSGI 而不是更轻量的 GunicornGunicorn 确实启动快、配置简单但它在生产环境有两个硬伤无原生进程监控Gunicorn 的 master 进程不提供内存占用、请求队列长度、worker 崩溃次数等指标。我曾遇到一个 Django 视图因数据库连接未关闭导致内存泄漏Gunicorn worker 占用 1.2GB 内存却还在跑直到 OOM Killer 杀掉进程而 uWSGI 的stats接口能实时返回processes: [{ id: 1, status: idle, rss: 82456576 }]配合 Prometheus 就能设置告警热更新能力弱Gunicorn 的--reload只监听 Python 文件改动对.env配置文件、settings.py中的变量修改无效而 uWSGI 的touch-reload /path/to/reload.trigger只需echo reload.trigger就能触发零停机重启配合 CI/CD 流水线非常顺滑。实测数据同一台 2核4G 服务器部署相同 Django 项目含 DRF API Admin 后台并发 200 请求时Gunicorn4 workers平均响应时间 142ms内存占用 380MBuWSGI2 processes × 2 threads平均响应时间 118ms内存占用 310MB且 CPU 利用率波动更平缓。注意uWSGI 的processes和threads不是越多越好。我试过设成4 processes × 4 threads结果数据库连接池被打满Django 默认 20 连接反而出现大量OperationalError: too many clients already。最终定为2 processes × 2 threads既压满 CPU 又不挤爆 DB 连接这是需要根据你的数据库连接数上限反推的——公式是uWSGI 总线程数 ≤ 数据库最大连接数 × 0.8。2.3 宝塔的角色定位工具不是解决方案很多新手误以为“装了宝塔就万事大吉”结果在宝塔里点来点去发现 Python 项目管理器根本不支持 requirements.txt 多层级依赖也不支持.env环境变量注入。宝塔在这里的正确定位是Linux 服务器的图形化操作系统不是 Django 部署平台。它的核心价值体现在三件事上SSL 证书全生命周期管理从申请、验证DNS 或 HTTP、安装、自动续签默认 60 天前执行全部点几下鼠标完成证书路径固定为/www/wwwroot/your-domain.com/ssl/Nginx 配置的可视化编辑不需要记proxy_pass语法点选“反向代理”→填入目标地址http://127.0.0.1:8080→保存它自动生成标准配置块基础服务监控与告警CPU、内存、磁盘使用率超过阈值时可通过邮件或微信推送告警这对无人值守的实验室服务器至关重要。所以我们的策略很清晰用宝塔管基础设施Nginx、SSL、监控用 Docker 管应用逻辑Django、uWSGI、依赖两者通过宿主机网络桥接通信。这种解耦让任何一个环节出问题都不会牵连全局。3. 核心细节解析与实操要点从 Dockerfile 到 uWSGI 配置的每一处陷阱3.1 Dockerfile不是复制粘贴而是按需裁剪网上很多 Django Dockerfile 直接FROM python:3.9-slim然后pip install -r requirements.txt这在开发环境没问题但生产环境会埋雷。我最终采用的 Dockerfile 是这样写的# 使用多阶段构建减小镜像体积 FROM python:3.9-slim-buster AS builder # 安装编译依赖为 psycopg2 等 C 扩展准备 RUN apt-get update apt-get install -y \ build-essential \ libpq-dev \ gcc \ rm -rf /var/lib/apt/lists/* # 复制依赖文件利用 Docker 缓存加速 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 第二阶段运行时镜像不带编译工具 FROM python:3.9-slim-buster # 创建非 root 用户安全强制项 RUN groupadd -g 1001 -f appuser \ useradd -r -u 1001 -g appuser appuser USER appuser # 复制上一阶段安装的包 COPY --frombuilder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages COPY --frombuilder /usr/local/bin/ /usr/local/bin/ # 复制项目代码注意不复制 .git、.env 等敏感文件 COPY --chownappuser:appuser . . # 暴露端口仅作声明实际由 uWSGI 监听 EXPOSE 8080 # 设置工作目录和权限 WORKDIR /app RUN chmod x ./entrypoint.sh # 启动脚本见下文 ENTRYPOINT [./entrypoint.sh]关键点解析多阶段构建第一阶段装编译工具第二阶段只保留运行时依赖最终镜像从 980MB 降到 210MB非 root 用户Docker 默认以 root 运行一旦容器被攻破攻击者可获得宿主机 root 权限。这里创建appuser用户并切换是 Docker 安全基线要求不复制 .env.env文件含数据库密码、SECRET_KEY绝不能打进镜像。我们通过 Docker run 时的--env-file参数注入EXPOSE 8080这只是文档化声明uWSGI 实际监听127.0.0.1:8080不绑定 0.0.0.0防止外部直连。实操心得requirements.txt必须锁定所有依赖版本包括Django3.2.23、psycopg2-binary2.9.5。我吃过亏——某次pip install -r requirements.txt拉了新版本psycopg2它要求系统 glibc 版本 ≥2.28而 CentOS 7 的 glibc 是 2.17容器直接启动失败。现在我的流程是本地虚拟环境pip freeze requirements.txt再pip install --no-deps -r requirements.txt验证能否安装成功。3.2 uWSGI 配置12 个参数背后的血泪教训uWSGI 的配置文件uwsgi.ini是整个链路的“心脏起搏器”下面是我线上稳定运行 8 个月的配置逐行解释[uwsgi] # 基础路径 project_root /app chdir %(project_root) module myproject.wsgi:application master true processes 2 threads 2 enable-threads true # 网络与协议 socket 127.0.0.1:8080 protocol http http-timeout 30 harakiri 60 vacuum true # 进程管理 die-on-term true max-requests 5000 max-requests-delta 500 reload-on-rss 512 memory-report true # 日志与安全 logto /app/logs/uwsgi.log log-maxsize 10000000 log-backupname /app/logs/uwsgi.log.old uid appuser gid appuser chmod-socket 664 chown-socket appuser:appuser重点参数说明socket 127.0.0.1:8080必须写 127.0.0.1不能写 0.0.0.0。否则 uWSGI 会监听所有网卡Docker 容器的 8080 端口就暴露给了宿主机外网Nginx 反代形同虚设harakiri 60这是“看门狗”超时单位秒。如果一个请求处理超过 60 秒uWSGI 强制杀死该 worker防止慢请求拖垮整个服务。Django Admin 导出大数据量 Excel 时经常触发所以设为 60 而非默认 30reload-on-rss 512当单个 worker 进程 RSS 内存超过 512MB 时自动重启该 worker。这是对付内存泄漏的终极手段比max-requests更精准chmod-socket 664socket 文件权限设为 664确保 Nginx运行在 www 用户下能读写该 socket。我曾因权限是 600Nginx 报错connect() to unix:///tmp/uwsgi.sock failed (13: Permission denied)查了 3 小时才发现是这里。注意vacuum true是关键。它确保 uWSGI 退出时自动清理 socket 文件、pid 文件等临时资源。没有它容器重启后 uWSGI 启动失败报错bind(): Address already in use因为旧 socket 文件还躺在那里。3.3 启动脚本 entrypoint.sh让容器真正“活”起来Docker 容器不是虚拟机它默认只运行一个前台进程。如果直接CMD [uwsgi, --ini, uwsgi.ini]那 Django 启动后数据库迁移、静态文件收集这些初始化动作都没法做。所以必须写一个 shell 脚本统筹#!/bin/bash set -e # 任何命令失败立即退出 # 等待数据库就绪避免 Django 启动时连不上 DB echo Waiting for PostgreSQL... while ! nc -z db 5432; do sleep 2 done echo PostgreSQL is ready! # 应用数据库迁移 echo Applying database migrations... python manage.py migrate --noinput # 收集静态文件Django STATIC_ROOT 指向 /app/staticfiles echo Collecting static files... python manage.py collectstatic --noinput # 创建超级用户仅首次运行检查是否存在 if [ ! -f /app/.superuser_created ]; then echo Creating superuser... echo from django.contrib.auth import get_user_model; User get_user_model(); User.objects.filter(usernameadmin).exists() or User.objects.create_superuser(admin, adminexample.com, Admin123!) | python touch /app/.superuser_created fi # 启动 uWSGI前台运行保持容器不退出 echo Starting uWSGI... exec uwsgi --ini uwsgi.ini这个脚本解决了三个致命问题数据库依赖等待用nc -z db 5432检测 PostgreSQL 容器是否就绪避免 Django 启动时报ConnectionRefusedError静态文件自动收集Django 的collectstatic必须在每次部署时执行否则 Nginx 找不到 CSS/JS超级用户防重复创建用.superuser_created文件标记避免容器重启时反复创建 admin 用户导致密码被覆盖。提示exec uwsgi ...中的exec很关键。它用 uWSGI 进程替换当前 shell 进程使 uWSGI 成为 PID 1。这样当 uWSGI 崩溃时Docker 才会认为容器异常退出并重启否则 shell 进程一直活着Docker 认为容器正常问题就藏起来了。4. 实操过程与核心环节实现从宝塔安装到域名上线的完整流水线4.1 宿主机环境准备宝塔 Docker 的黄金搭档第一步永远是干净的系统。我推荐CentOS 7.9内核 3.10.0-1160原因很现实宝塔对 CentOS 兼容性最好Docker CE 官方也长期支持。Ubuntu 22.04 虽新但宝塔某些插件如防火墙适配不稳定。安装流程全程 root 用户# 1. 关闭 SELinux宝塔官方要求 sed -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config setenforce 0 # 2. 安装宝塔官方最新版 yum install -y wget wget -O install.sh http://download.bt.cn/install/install_6.0.sh sh install.sh # 3. 安装 Docker宝塔后台可一键安装但建议命令行确保版本 curl -fsSL https://get.docker.com | bash systemctl start docker systemctl enable docker # 4. 安装 docker-composev2.15.1兼容性好 curl -L https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose chmod x /usr/local/bin/docker-compose安装完成后登录宝塔面板http://你的服务器IP:8888完成初始化设置。关键一步在“软件商店”中安装 Nginx 1.22不要选最新版 1.24有兼容性问题和 MySQL 5.7Django 3.2 兼容性最佳。注意宝塔安装的 Nginx默认监听 80/443 端口且配置文件位于/www/server/panel/vhost/nginx/。我们后续的反向代理配置就放在这里而不是自己建目录。这是宝塔的约定破坏它会导致面板管理失效。4.2 Docker Compose 编排让多容器协作像搭积木项目根目录下创建docker-compose.yml内容如下version: 3.8 services: web: build: . restart: unless-stopped environment: - DJANGO_SETTINGS_MODULEmyproject.settings.production - SECRET_KEY${SECRET_KEY} - DEBUGFalse - ALLOWED_HOSTS${ALLOWED_HOSTS} - DATABASE_URLpostgresql://myuser:mypassdb:5432/mydb env_file: - .env.prod # 存放敏感变量不提交 Git volumes: - /www/wwwroot/myproject.com/staticfiles:/app/staticfiles - /www/wwwroot/myproject.com/media:/app/media - /www/wwwroot/myproject.com/logs:/app/logs depends_on: - db db: image: postgres:13.6 restart: unless-stopped environment: - POSTGRES_DBmydb - POSTGRES_USERmyuser - POSTGRES_PASSWORDmypass volumes: - pg_data:/var/lib/postgresql/data volumes: pg_data:关键设计点volumes挂载了三个目录staticfilesDjango 收集的静态文件供 Nginx 直接服务、media用户上传文件、logsuWSGI 日志。全部挂载到/www/wwwroot/下是为了和宝塔的 Web 根目录对齐这样 Nginx 配置就能复用宝塔的路径习惯depends_on只控制启动顺序不保证 db 就绪。所以entrypoint.sh里的nc -z db 5432是必须的environment中的DATABASE_URL使用db:5432这是 Docker 内部 DNS容器名db会自动解析为数据库容器 IP。部署命令# 进入项目目录 cd /www/wwwroot/myproject.com # 创建生产环境 .env 文件不提交 Git cat .env.prod EOF SECRET_KEYyour-very-secure-secret-key-here ALLOWED_HOSTSmyproject.com,www.myproject.com EOF # 构建并启动 docker-compose up -d --build此时docker-compose ps应显示两个容器都是Up状态。用docker logs myprojectcom-web-1查看 uWSGI 启动日志确认看到spawned uWSGI master process和spawned uWSGI worker。4.3 宝塔 Nginx 配置反向代理 静态文件直通登录宝塔在“网站” → “添加站点”域名填myproject.com根目录选/www/wwwroot/myproject.comPHP 版本选“纯静态”。添加后点击该站点的“设置” → “配置文件”在server块内删除所有默认的location块替换成以下内容# 处理静态文件CSS/JS/图片直接由 Nginx 服务不走 uWSGI location ^~ /static/ { alias /www/wwwroot/myproject.com/staticfiles/; expires 1y; add_header Cache-Control public, immutable; } location ^~ /media/ { alias /www/wwwroot/myproject.com/media/; expires 7d; } # 处理 Django Admin 静态文件Django 3.2 默认在 /static/admin/ location ^~ /static/admin/ { alias /www/wwwroot/myproject.com/staticfiles/admin/; } # 所有其他请求反向代理到 uWSGI 容器 location / { proxy_pass http://127.0.0.1:8080; 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; # 传递 WebSocket 头如果项目用 Channels proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; # 超时设置匹配 uWSGI harakiri proxy_connect_timeout 30s; proxy_send_timeout 60s; proxy_read_timeout 60s; }保存后点击“重载配置”。此时访问http://myproject.com应该能看到 Django 的欢迎页如果还没配置好会看到DisallowedHost错误说明ALLOWED_HOSTS没设对。提示location ^~ /static/中的^~表示“前缀匹配且最高优先级”比正则location ~* \.(js|css|png)$更高效。Nginx 匹配规则是精确匹配 ^~前缀 正则匹配 普通前缀所以把静态文件放在最前面能极大降低 CPU 开销。4.4 SSL 证书申请与自动续签宝塔的“免维护”魔法在宝塔“网站”列表中找到myproject.com站点点击“SSL” → “Lets Encrypt”勾选域名myproject.com和www.myproject.com验证方式选“DNS”更稳定不受端口阻塞影响。点击“申请”等待 1-2 分钟状态变成“已签发”。此时宝塔已自动将证书文件放入/www/wwwroot/myproject.com/ssl/修改 Nginx 配置添加listen 443 ssl http2和ssl_certificate等指令添加 301 重定向将 HTTP 自动跳转 HTTPS。最关键的是宝塔在/www/server/cron/下创建了一个定时任务# 每天凌晨 2 点检测证书剩余 60 天过期则自动续签 0 2 * * * /usr/bin/certbot renew --quiet --post-hook /etc/init.d/nginx reload你什么都不用做证书就自动续上了。我设置过微信告警每次续签成功都会收到消息“myproject.com SSL 证书续签成功有效期至 2025-03-15”。注意如果使用 DNS 验证需要在域名 DNS 提供商处添加一条 TXT 记录宝塔会给出具体值。这个步骤只需做一次后续续签全自动。别用 HTTP 验证它要求 80 端口开放且可被外网访问很多校园网或企业防火墙会拦截。5. 常见问题与排查技巧实录那些让你抓狂又恍然大悟的瞬间5.1 502 Bad GatewayNginx 找不到 uWSGI 的 100 种方式这是部署后最常遇到的错误。Nginx 报 502意味着它成功连接到了 upstream但 upstream 返回了无效响应。排查必须按顺序第一步确认 uWSGI 容器是否真在运行# 查看容器状态 docker-compose ps # 如果 web 服务是 Exited看日志 docker logs myprojectcom-web-1 # 常见错误django.core.exceptions.ImproperlyConfigured: The SECRET_KEY setting must not be empty. # 解决检查 .env.prod 是否存在SECRET_KEY 是否赋值第二步确认 uWSGI 是否监听了正确地址# 进入容器 docker exec -it myprojectcom-web-1 sh # 查看端口监听 netstat -tuln | grep :8080 # 正确输出应为tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN # 如果是 0.0.0.0:8080立刻改 uwsgi.ini加一行 socket 127.0.0.1:8080第三步确认 Nginx 能否访问该端口# 在宿主机执行不是容器内 curl -v http://127.0.0.1:8080 # 如果返回 HTML说明 uWSGI 正常问题在 Nginx 配置 # 如果返回 Failed to connect说明 uWSGI 没监听或 Docker 网络不通第四步检查 Nginx 错误日志# 宝塔 Nginx 错误日志路径 tail -f /www/wwwlogs/myproject.com.error.log # 最常见报错connect() to 127.0.0.1:8080 failed (111: Connection refused) # 这说明 uWSGI 没起来或监听地址不对实操心得我总结了一个 502 排查速查表贴在工位上现象最可能原因快速验证命令解决方案curl http://127.0.0.1:8080失败uWSGI 未启动或监听错地址docker logs web检查entrypoint.sh是否卡在数据库等待或uwsgi.inisocket 地址curl成功但网页 502Nginx 配置中proxy_pass地址错误grep proxy_pass /www/server/panel/vhost/nginx/myproject.com.conf改为http://127.0.0.1:8080首页 502但/static/css/app.css404静态文件路径挂载错误ls -l /www/wwwroot/myproject.com/staticfiles/检查docker-compose.ymlvolumes 路径是否拼错502 随机出现uWSGI worker 崩溃docker logs web | grep -i segfault|killed增加reload-on-rss 512降低processes数量5.2 静态文件 404Django 的“看不见的手”明明python manage.py collectstatic执行成功Nginx 配置也写了alias /staticfiles/但打开网页 F12 看 Network所有 CSS/JS 都是 404。这个问题我遇到过 7 次原因各不相同原因1Django settings 中 STATIC_ROOT 路径错误STATIC_ROOT os.path.join(BASE_DIR, staticfiles)是对的但如果写成STATIC_ROOT /app/staticfiles而 Docker 挂载的是/www/wwwroot/.../staticfiles路径就不匹配。永远用os.path.join(BASE_DIR, ...)不要写绝对路径。原因2Nginx 的 alias 末尾多了斜杠alias /www/wwwroot/myproject.com/staticfiles/;—— 这个末尾的/是致命的。Nginx 会把/static/css/app.css映射成/www/wwwroot/.../staticfiles//css/app.css双斜杠文件系统找不到。alias 的路径末尾绝对不能有/。原因3Docker volume 挂载权限问题chown -R appuser:appuser /www/wwwroot/myproject.com/staticfiles否则 Nginx运行在 www 用户下没有读取权限。宝塔默认给/www/wwwroot/目录 755 权限但子目录可能继承错。原因4浏览器强缓存了旧的 404 响应这是最隐蔽的。Nginx 对 404 响应默认缓存 1 分钟即使你修好了浏览器还是显示 404。解决方案在 Nginx 配置中加location /static/ { add_header Cache-Control no-cache; }或者彻底清空浏览器缓存。注意Django 的DEBUG False时它根本不会处理静态文件请求全部交给 Web 服务器。所以DEBUG False后页面一片空白99% 是静态文件没配好不是 Django 代码问题。5.3 数据库迁移失败relation auth_user does not exist执行docker-compose exec web python manage.py migrate报错提示表不存在。这不是 Django 的 bug而是 Docker 网络和 PostgreSQL 的经典配合问题。根本原因是docker-compose up -d启动时web 容器和 db 容器是并行启动的。虽然depends_on声明了依赖但 Docker 只等 db 容器“启动”不等 PostgreSQL 服务“就绪”。db 容器的postgres进程可能刚 fork 出来端口还没监听完web 容器的manage.py migrate就冲过去了自然连不上。解决方案就是entrypoint.sh里的等待逻辑# 等待 PostgreSQL 就绪更健壮的写法 until pg_isready -h db -p 5432 -U myuser; do echo Waiting for PostgreSQL... sleep 2 donepg_isready是 PostgreSQL 自带的健康检查工具比nc -z更精准它会真正尝试连接并认证。把这个命令放进entrypoint.sh就能 100% 避免迁移失败。实操心得我曾经为这个问题熬了通宵。最后发现docker-compose up启动后docker-compose logs -f db显示database system is ready to accept connections但docker-compose logs -f web里migrate命令已经执行完了。所以永远不要相信depends_on必须在应用层做健康检查。5.4 SSL 证书续签失败Lets Encrypt 的“温柔提醒”宝塔的自动续签偶尔会失败错误日志里写着 Failed authorization procedure. myproject.com (http-01): urn:ietf:params:acme:error:connection :: The server could not connect