资讯详情

Docker+Nginx组合部署Python Web应用:从镜像构建到反向代理

📅 2026/10/21 21:53:22 | 华诺云谱 👁 阅读
Docker+Nginx组合部署Python Web应用:从镜像构建到反向代理
我最早部署Python Web应用的时候还是老老实实在服务器上装Python、建虚拟环境、再写一个systemd服务去托管进程。后来发现这套流程实在太脆弱了服务器上同时跑两三个项目时Python版本互相打架依赖包经常冲突一次系统升级就能把环境搞坏迁移到新服务器更是从头再来一遍。所以我从某个阶段开始新项目部署基本统一改成“Docker Nginx”这套组合应用打成镜像环境彻底隔离再通过Nginx统一对外提供访问入口。这篇文章整理的就是这套部署流程的完整实操记录从Dockerfile编写、docker-compose编排到Nginx反向代理配置再加上我在真实环境里踩过的各种坑。如果你手头正好有一个准备上线的Python Web应用或者刚拿到一台新服务器想建立一套规范的部署方式这篇内容可以直接拿来当操作手册。1. 部署方案选型为什么选择 Docker Nginx 的组合1.1 裸机部署踩过的坑先说说我为什么放弃裸机部署。很多开发者第一次上线Python应用时习惯沿用本地开发的方式在服务器上装Python、执行pip install -r requirements.txt、然后用nohup或者systemd把Gunicorn拉起来。看起来简单但实际维护起来问题一个接一个。最典型的是Python版本冲突。当时我负责的一个服务器上老项目依赖Python 3.6新项目要用Python 3.11系统自带的Python版本又不能随便动最后只能从源码编译了一套新解释器。更麻烦的是依赖包A项目的某个第三方库要1.x版本B项目却必须用2.x版本放在同一个环境里根本没法共存。为了隔离我试过virtualenv、试过pyenv但每次换服务器都要重新装一遍系统依赖、编译工具链光环境搭建就能耗掉大半天。进程管理也是痛点。用systemd管理Gunicorn写service文件倒不难但服务崩溃后的重启策略、开机自启、多实例端口分配每个细节都在消耗精力。遇到一个接口把内存吃满进程被OOM Killer干掉如果没写好Restartalways服务就悄悄挂了用户访问直接超时。这种“半夜服务挂掉”的经历我相信很多运维过的朋友都有体会。这些问题的本质是应用和运行环境强耦合。裸机上的环境是共享的、脆弱的、不可重复的任何一次变更都可能波及其他项目。所以我后来的选择很明确用容器把应用连同运行环境一起打包。1.2 Docker 与传统部署方式的核心差异Docker解决的核心问题可以概括成一句话让应用和环境一起交付。传统部署方式里开发者交付的是代码运行环境由服务器临时拼凑。Docker交付的是一个镜像镜像里不仅有代码还有Python解释器、系统依赖库、配置文件、启动命令。不管镜像在什么机器上运行跑起来的行为都是一致的。这个特性在项目迁移时尤其有用旧服务器退役新服务器只要装上Docker把镜像拉过去再启动应用就回来了。我习惯用“搬家”来类比以前部署应用就像带着一箱锅碗瓢盆搬到新房子但新房子有没有煤气灶、水龙头接口对不对得上都不确定Docker相当于把整个厨房都打包成了集装箱到了新地方直接接上水电就能做饭。当然Docker不是没有代价。镜像会占用额外的磁盘空间容器多了一层文件系统排障时也要适应新的命令体系。但相比它带来的环境一致性、进程隔离、快速伸缩这些收益这点代价完全值得。尤其是在服务器上同时运行多个项目时每个项目一个容器互不干扰端口冲突、依赖冲突这类问题从根源上消失。1.3 Nginx 在这套架构里扮演什么角色容器把应用跑起来了但还需要一个统一的入口这就是Nginx的活。Python Web应用通常用Gunicorn或uvicorn监听某个端口比如8000。如果直接把8000端口暴露给公网会有一堆问题一个服务器上可能有多个应用80端口只能给一个用Python进程直接处理静态文件请求效率很低HTTPS证书的配置和续期也缺少一个统一入口。Nginx作为反向代理正好把这些事情都接了过去。在我的部署架构里Nginx负责四个核心任务接收外部的HTTP/HTTPS请求转发给后端的Python应用容器把/static/、/media/这类静态资源请求直接返回文件完全不经过程序进程终结SSL证书统一处理HTTPS加密和跳转作为流量的统一入口后续如果要加负载均衡、限流也都在这一层实现。关于Nginx放在宿主机还是容器里我见过两种做法。如果服务器上只有一个Web应用把Nginx放进docker-compose里一起编排最省心如果一台服务器上已经装了Nginx并管理着多个站点那宿主机Nginx反向代理到容器端口会更合适。本文后面的配置按Nginx容器方案写因为这个方式结构清晰新增服务时也容易扩展。2. 部署前的准备环境、目录与应用改造2.1 服务器环境初始化部署前先把服务器的底层环境准备好。我这里以Ubuntu/Debian系为例其他发行版思路类似。首先安装Docker和Compose插件sudo apt update sudo apt install -y docker.io docker-compose-plugin sudo systemctl enable --now docker sudo usermod -aG docker $USER安装完记得重新登录一次让用户组权限生效。我习惯创建专门的部署用户而不是直接用root跑服务减少误操作风险。接下来确认防火墙。如果是云服务器安全组里放行80和443端口如果是本机ufw可以执行sudo ufw allow Nginx Full数据库、Redis这类基础服务只在内网使用不要暴露到公网避免不必要的安全隐患。另外用nproc看一下服务器核心数后面配置Gunicorn worker数会用到。我在服务器上的项目目录一般按这个结构规划/opt/apps/example/ ├── app/ # 项目代码 │ ├── Dockerfile │ ├── docker-compose.yml │ ├── requirements.txt │ └── src/ ├── data/ # 应用产生的数据挂载进容器 ├── logs/ # 宿主机的日志备份目录 └── nginx/ # Nginx 配置 └── default.conf代码放在app/下运行产生的数据放data/Nginx配置单独建目录。这样备份时只需备份data/和nginx/代码随时能用git重新拉取数据不丢配置也好找。2.2 应用代码需要做哪些适配不是所有本地能跑的Python应用都能直接容器化上线前需要做几个小改造。第一监听地址改成0.0.0.0。本地开发时经常监听127.0.0.1这个地址在容器里意味着只有容器内部能访问宿主机和其他容器都访问不到。Gunicorn启动参数里的-b 0.0.0.0:8000就是这个作用。第二配置项要从代码里剥离出来通过环境变量注入。比如数据库连接串、SECRET_KEY、DEBUG这些不同环境的值不一样写死在代码里既不安全也不利于容器化。我通常会在项目里写一个读取环境变量的配置模块import os DEBUG os.getenv(DEBUG, false).lower() true SECRET_KEY os.getenv(SECRET_KEY, change-me) DB_HOST os.getenv(DB_HOST, localhost)第三日志输出到标准输出而不是写入文件。容器里写日志文件不是不行但维护麻烦还是依赖Docker的日志机制更干净。后面会用docker compose logs统一查看日志。第四静态文件处理。Django项目要执行collectstatic把静态文件收集到指定目录Flask项目要明确静态目录的路径后面再配置Nginx直接托管这些文件。还有一个容易忽略的点如果应用要连接数据库容器环境里的数据库地址不是localhost而是docker-compose里定义的服务名。比如数据库服务叫db那连接地址就是db:5432。这个细节在容器化初期很容易让人卡住。2.3 本地能跑不代表容器能跑这句话是我踩坑踩出来的。本地依赖是直接装在系统Python里的容器里使用的是全新环境requirements.txt里漏掉任何一个间接依赖容器启动就报ModuleNotFoundError。为了减少这类问题我建议在项目里维护一份完整且固定的依赖列表。用pip freeze生成虽然简单但会带上很多无关包更推荐在requirements.in里写顶层依赖然后用工具锁定完整版本或者直接手动维护并定期更新。镜像构建时采用固定版本号避免某天第三方库发布新版本导致行为变化。基础镜像的选择也有讲究。我目前统一用python:3.11-slim它在体积和兼容性之间比较平衡。纯python:3.11镜像太大alpine版本虽然更小但基于musl libc遇到需要编译C扩展的包时经常要现装编译工具链反而拖慢构建。如果你的应用依赖Pillow、lxml这类重量级库slim版本会更省心。3. Docker 镜像构建与容器编排3.1 编写优雅的 DockerfileDockerfile写得够不够好直接决定镜像大小、构建速度和安全程度。我目前的模板长这样FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --upgrade pip \ pip install --no-cache-dir --prefix/build/deps -r requirements.txt FROM python:3.11-slim ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ TZAsia/Shanghai WORKDIR /app COPY --frombuilder /build/deps /usr/local COPY src/ /app/src/ COPY manage.py ./ RUN useradd --system --uid 10001 appuser chown -R appuser:appuser /app USER appuser EXPOSE 8000 CMD [gunicorn, config.wsgi:application, -b, 0.0.0.0:8000, -w, 2]这里有几个关键点值得展开说。多阶段构建是我特别强调的。第一阶段builder负责安装Python依赖把结果装在/build/deps目录第二阶段只拷贝依赖和代码不带编译工具链。这样做的好处很明显最终镜像体积更小也更安全。很多初学者直接把pip install放在最终镜像里如果项目里有需要编译的C扩展最终镜像就会残留编译器白白增加体积和攻击面。依赖缓存顺序也需要注意。COPY requirements.txt和RUN pip install放在前面后面才复制代码。Docker构建时每一层都会缓存只要requirements.txt没变重新构建就不会重复安装依赖如果先复制全部代码再安装依赖那随便改一行代码都会触发依赖重新安装构建速度会慢很多。另外两个环境变量值得养成习惯PYTHONUNBUFFERED1让Python输出不缓冲docker compose logs能实时看到日志PYTHONDONTWRITEBYTECODE1防止生成__pycache__文件保持容器文件系统干净。容器内的用户处理我从某次安全扫描建议后改成了非root运行。创建appuser用户并切换到它万一容器被攻破攻击者得到的也不是root权限。这句USER appuser看起来简单但很多人一开始都不当回事。3.2 用 gunicorn 还是 uvicorn 启动应用启动命令是整个Dockerfile的入口值得单独说明。Gunicorn和Uvicorn的选择主要取决于应用框架。Django和Flask这类传统WSGI应用默认用Gunicorn就够了。Gunicorn是成熟的进程管理器worker崩溃会自动拉起配合docker-compose的restart策略稳定性有保障。如果项目用了FastAPI、或者Flask/Django里加了异步视图那就要用Uvicorn来跑ASGI或者用Gunicorn配合uvicorn.workers.UvicornWorker既保留Gunicorn的进程管理又支持异步协议。worker数量不是越多越好。Gunicorn官方给的参考值是CPU核心数*21但这只是起点实际要看应用是CPU密集还是IO密集。我见过一个内存吃紧的小服务器配置了8个worker结果每个worker占几百MB内存服务器直接OOM。我的习惯做法先用nproc查核心数再给一个保守的worker数量跑起来观察内存和响应时间慢慢往上加。起步阶段-w 2通常够用压力大了再调整。如果用了python:3.11-slim镜像注意系统里可能没有curl等调试工具。想要在容器里做健康检查要么在最终镜像里安装curl要么直接用Python发起请求看项目需求自行取舍。3.3 用 docker-compose 定义服务拓扑单容器部署只能算第一步一个完整的Web应用通常还要数据库、对象存储这些配套服务。docker-compose正是用来把这套服务串起来的工具。我的一份标准docker-compose配置大致如下version: 3.8 services: web: build: . restart: unless-stopped env_file: - .env volumes: - ./data:/app/data - ./static:/app/static depends_on: db: condition: service_healthy networks: - appnet expose: - 8000 db: image: postgres:15-alpine restart: unless-stopped env_file: - .env volumes: - dbdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U $${POSTGRES_USER}] interval: 5s timeout: 5s retries: 5 networks: - appnet nginx: image: nginx:1.27-alpine restart: unless-stopped depends_on: - web ports: - 80:80 - 443:443 volumes: - ./nginx:/etc/nginx/conf.d:ro - ./static:/srv/static:ro networks: - appnet volumes: dbdata: networks: appnet:注意web服务没有映射宿主机端口只用expose声明容器内端口。这意味着外界无法直接访问8000端口只能通过Nginx容器进入。我特别推荐这个设计应用服务不直接暴露公网减少攻击面也让架构更清晰。depends_on加上condition: service_healthy是我后期改进的。单纯的depends_on只控制启动顺序不保证数据库已经就绪。如果Web应用启动得比数据库快数据库连接就会失败。给db服务加上healthcheck后Web容器会等到数据库健康检查通过再启动这个坑就彻底避免了。Nginx容器挂载了配置目录和静态目录都是只读挂载。配置文件放宿主机的好处是改完配置马上生效不需要重新构建镜像静态目录直接挂载进去Nginx才能高效地返回静态文件。3.4 构建与启动的实操记录环境准备就绪后部署就变成了几条命令的事cd /opt/apps/example/app docker compose config # 校验配置语法 docker compose build # 构建镜像 docker compose up -d # 后台启动所有服务 docker compose ps # 查看服务状态第一次构建通常比较慢因为要下载基础镜像和装依赖。后续构建如果requirements.txt没变会直接命中缓存几分钟就能完成。启动后先验证Nginx能否正常返回页面curl -I http://127.0.0.1如果返回502先别急按下面的顺序排查先看容器状态是不是Up再看Web容器日志有没有报错最后进容器里直接用curl访问Gunicorn端口docker compose exec web curl 127.0.0.1:8000这一步能快速定位问题到底出在应用层还是Nginx层。我见过太多人一看到502就疯狂改Nginx配置最后发现是Gunicorn压根没起来白白浪费几小时。4. Nginx 配置反向代理、静态文件与 HTTPS4.1 最简反向代理配置Nginx配置是整个部署中的关键一环。我在nginx/default.conf里写的最简配置如下server { listen 80; server_name app.example.com; client_max_body_size 20m; location / { proxy_pass http://web: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_http_version 1.1; proxy_set_header Connection ; proxy_read_timeout 60s; } }proxy_pass http://web:8000;里的web就是docker-compose里的服务名。Docker内置DNS会把服务名自动解析成对应的容器IP所以Nginx容器和Web容器能在同一网络里通过服务名互相访问。proxy_set_header Host $host;这行容易被忽略但它非常重要。Django的ALLOWED_HOSTS校验依赖Host请求头如果Nginx转发时不带上真实的HostDjango会直接拒绝请求。X-Real-IP和X-Forwarded-For则是为了让应用能拿到用户真实IP否则日志里看到的所有来源IP都是Nginx容器的地址。proxy_http_version 1.1和Connection 这两行是为了支持长连接。默认Nginx转发给后端的HTTP版本是1.0每次请求都要重新建立连接性能受影响改成1.1并清空Connection头可以让Gunicorn的keep-alive生效减少连接开销。client_max_body_size默认只有1m如果应用有上传功能不改这个配置就会遇到上传文件直接报413的错误。20m是相对通用的设置具体按业务调整。4.2 静态文件与上传文件的托管让Nginx处理静态文件是部署Python Web应用时性价比最高的一项优化。Python进程处理静态请求既慢又浪费资源Nginx返回文件的速度快好几个量级。我在Nginx配置中通常会加一段location /static/ { alias /srv/static/; expires 7d; access_log off; add_header Cache-Control public, max-age604800; } location /media/ { alias /srv/media/; }这里有个高频坑alias和root的区别。如果写成root /srv/static;用户访问/static/css/app.css时Nginx会在/srv/static/static/css/app.css里找文件多了一层目录结果就是404。用alias /srv/static/;并保证路径末尾带斜杠才能把URL里的/static/部分直接替换成/srv/static/。静态文件的缓存也需要注意。expires 7d给带版本号的静态资源一个7天的缓存时间能显著减少重复请求。但如果项目发布时没有给静态文件名追加版本号缓存太激进反而会导致用户看到旧文件这点要结合前端发布策略来定。挂载目录的问题我提一下docker-compose.yml里Nginx容器要把静态目录挂载进去否则Nginx容器里根本没有这些文件。我在实际操作中不止一次忘记在compose文件里加挂载结果是Nginx日志一直报404应用却一切正常排查了半小时才发现是挂载问题。4.3 配置 HTTPS 与自动跳转生产环境不配置HTTPS相当于把用户的数据明文裸奔在网络上。证书申请我用的是自动续期的方案申请下来的证书存放在宿主机/opt/apps/example/certs/然后挂载进Nginx容器。443端口的SSL server配置server { listen 443 ssl; server_name app.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location / { proxy_pass http://web:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location /static/ { alias /srv/static/; } }HTTP访问直接跳转到HTTPSserver { listen 80; server_name app.example.com; return 301 https://$host$request_uri; }ssl_protocols建议至少保留TLSv1.2以上低版本协议存在已知漏洞没有理由为了兼容老浏览器而牺牲安全性。证书续期后记得docker compose exec nginx nginx -s reload让新证书生效。4.4 长连接与上传限制等附加配置如果应用里有WebSocket或者轮询接口Nginx需要额外做升级头转发location /ws/ { proxy_pass http://web:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }这段配置的目的是让浏览器与Nginx之间、Nginx与后端之间的连接都保持HTTP/1.1升级协议WebSocket握手才能通过。如果只是普通HTTP请求加不加无所谓一旦涉及到实时通信缺失这段配置会出现页面能打开、连接却在几十秒后自动断开的现象。还有一个容易踩的配置坑如果应用不是挂在域名的根路径而是某个子路径比如app.example.com/myapp/那Nginx的proxy_pass写法会有微妙差异。proxy_pass http://web:8000;会把完整的URL路径原样转发而proxy_pass http://web:8000/;末尾带斜杠时会去掉location匹配到的前缀再转发。这个区别需要细心测试不然很容易得到一堆404或路由错乱。5. 容器运维数据持久化、日志与升级5.1 数据持久化方案容器本身是无状态的容器一删里面的文件就全没了。所以只要是有保存价值的数据比如数据库内容、用户上传文件、应用生成的临时数据都必须通过volume挂载到宿主机。docker-compose里有两种挂载方式我分别用在了不同地方场景挂载方式我这么用的原因数据库数据named volume如dbdata不依赖宿主机目录方便备份和迁移应用产生的文件bind mount如./data:/app/data直接在宿主机上能看到文件方便排查代码和配置bind mount只读修改后不用重新构建镜像开发调试效率高bind mount有一个权限问题经常让人头疼。容器里通常以非root用户运行但宿主机目录的属主可能不是那个UID。我在compose里把./data挂载进容器后经常遇到“Permission denied”。解决方法是把宿主机目录的属主改成容器内运行的用户UID比如前面Dockerfile里创建的用户是10001那执行sudo chown -R 10001:10001 /opt/apps/example/data就能让应用正常读写数据目录。数据库的named volume则不需要手动权限管理镜像会自动处理。5.2 日志收集与轮转这个坑几乎所有跑过容器的人都会遇到。如果不在compose里配置日志轮转Docker默认会用json-file驱动无限保存日志时间一长某个错误循环刷日志就能把磁盘填满。我目前在web、db、nginx服务上都配置了统一的日志限制logging: driver: json-file options: max-size: 10m max-file: 3意思是单个日志文件超过10MB就轮转最多保留3个文件。这个配置足够应对大部分小型项目既保证了日志可查又不会侵占磁盘空间。日志量确实很大的场景后续可以考虑接入集中日志系统但单机部署阶段docker-compose加日志轮转已经够用。应用本身的日志处理也需要配合日志库的handler要输出到标准输出docker compose logs才能看到。我遇到过好几次应用把日志写进了容器内文件而容器日志里什么都没有排障时完全无从下手。5.3 更新发布流程有了Docker之后发布流程变得非常标准化。我的日常操作大致是cd /opt/apps/example/app git pull origin main docker compose build web docker compose up -d docker image prune -f先拉最新代码再构建新镜像然后up -d更新容器。如果项目有数据库迁移脚本发布前先在命令里执行一遍迁移再重启Web服务。Django项目的常见命令是docker compose run --rm web python manage.py migrate这里强调一下迁移的时机。应该先执行迁移再发布新版本代码否则新代码还没上去数据库已经改成新结构旧代码和旧库一旦有兼容性问题就会直接把服务打挂。反过来如果先发布代码再迁移新代码可能直接访问到旧库结构。顺序这件事最好在发布脚本里固化下来。我还会在正式发布前把旧镜像保留一个tag方便回滚docker tag example-web:latest example-web:backup-$(date %Y%m%d%H%M%S)真出问题时回滚到旧tag比重新拉代码、重新构建快得多。6. 常见问题与排查实录6.1 502 Bad Gateway502是部署NginxPython应用后最常见的报错。Nginx返回502本质上就是它无法从后端拿到正常响应。常见的根因有这么几类现象原因处理方式connect() failed (111: Connection refused)后端容器没起来或端口不对检查Gunicorn启动参数、查看web容器日志upstream timed out接口响应时间超出Nginx等待时长调大proxy_read_timeout或优化接口容器一直重启Gunicorn worker异常退出看启动日志里的异常栈定位代码或依赖问题我的排查顺序很固定先docker compose ps看容器状态再docker compose logs web看应用日志然后进容器手动curl后端端口确认监听正常。整套流程十分钟内基本能定位。这里想提醒的是502的根因九成在后端不要一上来就反复检查Nginx配置文件。6.2 静态文件 404/403静态文件相关的问题部署时出现率也很高。404的情况我总结下来主要是这几种alias和root用混导致查找路径多了一层目录静态文件没有挂载进Nginx容器容器里根本找不到目录项目没有执行collectstatic静态文件压根不存在Nginx容器内进程对挂载目录没有读取权限报403。排查时可以先用这个命令确认Nginx容器里到底能不能看到文件docker compose exec nginx ls -l /srv/static/如果目录为空回到宿主机执行collectstatic再刷新。如果是权限问题把宿主机目录设为可读或调整属主一般就能解决。6.3 容器里连不上数据库这个坑对第一次容器化的人尤其致命。应用日志里出现could not connect to server: Connection refused或者Name or service not known基本都是数据库连接配置问题。最典型的原因连接配置里还写着localhost。容器里的localhost指的是容器自己不是宿主机更不是数据库容器所以连不上。正确做法是把DB_HOST环境变量设成docker-compose的服务名比如db。另一种情况是Web容器启动太快数据库还没就绪连接失败后应用直接退出。前面提过用depends_on加condition: service_healthy配合数据库的healthcheck可以彻底解决。排查时先确认环境变量是否传对docker compose exec web env | grep DB_ docker compose exec db pg_isready看到DB_HOSTdb并且pg_isready返回正常再稍微等几秒让应用重启问题通常就消失了。6.4 日志把磁盘写满有一次我负责的项目部署了一个多月某天突然收到磁盘告警。上去一看/var/lib/docker/containers/下面某个容器的json.log文件已经到了几十GB全是某个接口反复打错误日志撑起来的。发现后处理两步先清理现有日志文件再给所有服务加上轮转限制。清理已有日志时不要直接用rm删除日志文件正被容器进程占用着删掉并不会立即释放空间。应该用truncate把文件内容清空sudo truncate -s 0 /var/lib/docker/containers/container-id/*-json.log然后按前面的配置给compose的每个服务加上logging限制。此外在服务器/etc/docker/daemon.json里也可以设置默认的日志轮转参数但compose里的配置覆盖性更强也更容易随项目同步。6.5 重启后服务没有自动拉起排队重启、服务器重启结果容器没一起恢复这种情况一旦遇到就是应用无法访问的线上事故。最常见的原因是compose里的restart策略没配置或者配置成了no。检查已有容器的策略docker inspect -f {{.HostConfig.RestartPolicy.Name}} 容器名如果结果是空或者no执行命令修改docker update --restart unless-stopped 容器名在compose里面我给每个服务都写了restart: unless-stopped服务器重启后容器会自动恢复除非手动执行docker stop否则容器不会被正常关闭。另外别忘了确认Docker服务本身设了开机自启sudo systemctl enable docker。还有一个小细节如果容器启动后依赖另一个服务而那个服务还没就绪restart: unless-stopped会反复重启直到成功。配合健康检查后这个重试过程是正常的不要一看日志在报拒绝连接就惊慌。这套部署流程我前后用过好几个项目印象最深的不是某个配置细节而是“可预期”这三个字。以前换一台服务器光是搭Python环境、装依赖、调系统服务就要折腾一整天现在镜像还保留着新机器装好Docker配置拉过去一条docker compose up -d就能跑起来。最后分享几个我每天在用的习惯改Nginx配置前先复制一份备份新版本上线前先在测试环境跑一遍collectstatic和migratedocker compose up -d之后别急着离开等到日志里出现启动成功的标志再休息。部署这种事确实要多踩几次坑才能熟练但如果一开始就用对工具链后面的路会平坦很多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑