Django+Gunicorn+Docker生产部署避坑指南

📅 2026/6/23 17:35:01
Django+Gunicorn+Docker生产部署避坑指南
1. 这不是“又一个Docker教程”为什么DjangoGunicornDocker组合在真实生产中常被误用你肯定见过这类标题“三步部署Django到Docker”、“Docker Compose一键启动Django应用”。我也写过也照着跑过——直到某次凌晨两点收到告警API响应延迟飙升至8秒用户注册失败率突破40%而监控面板上Gunicorn worker数稳定在4个CPU使用率却只有12%。排查两小时后发现问题既不在代码也不在数据库而在gunicorn.conf.py里一行被复制粘贴进来的--preload参数它让所有worker在启动时就加载了整个Django应用而Django的settings.py中又启用了DEBUGTrue和LOGGING的详细调试日志——结果每个worker进程内存暴涨至1.2GB系统开始疯狂swap服务彻底卡死。这正是当前大量Django Docker化实践中的典型盲区把Docker当成“打包工具”把Gunicorn当成“默认配置就能跑”的黑盒把Django当成“开发完扔进容器就万事大吉”的静态资产。但真实生产环境从不买账。Docker解决的是环境一致性与分发问题Gunicorn解决的是Python Web应用的并发模型适配问题而Django本身是一个高度可配置、对运行时环境极其敏感的框架。三者叠加不是简单相加而是产生新的约束条件与失效路径。我过去三年主导过7个中大型Django项目从裸机部署转向容器化其中4个在首次上线后两周内遭遇了至少一次因Gunicorn配置不当引发的雪崩式故障。最常见错误包括在Docker中错误复用开发环境的manage.py runserver命令忽略Gunicorn的worker-class与Django ORM连接池的冲突未针对容器内存限制调整worker-tmp-dir导致/tmp爆满以及——最隐蔽的——Docker镜像构建过程中COPY . /app把.git、__pycache__甚至本地.env文件一并打入生产镜像造成敏感信息泄露与启动性能下降。所以这篇内容不讲“怎么让Django在Docker里跑起来”而是聚焦一个更本质的问题如何让Django应用在Docker容器中以Gunicorn为WSGI服务器稳定、高效、可观测地承载真实业务流量它面向的不是刚学完pip install django的新手而是已经能写出REST API、用过Celery、知道DATABASE_URL环境变量意义的中级Django开发者。你不需要记住所有命令但必须理解每一行Dockerfile指令背后的资源权衡每一条Gunicorn参数对请求生命周期的影响以及Django设置项在容器上下文中的实际含义。接下来的内容全部来自线上故障复盘、压测数据对比和跨团队协作踩坑实录。2. 构建阶段的隐形陷阱Dockerfile不是脚本是资源契约很多人写Dockerfile的第一反应是“把本地能跑的命令抄进去”。于是出现这样的写法FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, manage.py, runserver, 0.0.0.0:8000]这段代码在开发机上可能“能跑”但它在生产中埋下了三颗雷第一COPY . .把整个项目目录含.git、node_modules、venv都塞进镜像导致镜像体积膨胀300%拉取时间从2秒变成15秒CI/CD流水线卡顿第二runserver是Django的开发服务器单线程、无超时、无静态文件处理能力根本不能用于生产第三python:3.11-slim基础镜像虽小但缺少gcc等编译工具当requirements.txt中包含psycopg2-binary以外的包如某些需要源码编译的C扩展时构建会静默失败或降级安装不兼容版本。真正的生产级Dockerfile核心是分层构建Multi-stage Build与最小权限原则。我们拆解一个经过23次线上迭代验证的模板2.1 构建阶段分离依赖安装与代码拷贝# 构建阶段仅用于编译和安装依赖 FROM python:3.11-slim AS builder # 安装构建所需工具仅此阶段需要 RUN apt-get update apt-get install -y --no-install-recommends \ gcc \ postgresql-client \ rm -rf /var/lib/apt/lists/* # 创建非root用户用于构建避免权限污染 RUN addgroup -g 1001 -f appgroup adduser -S appuser -u 1001 # 切换到非root用户执行后续操作 USER appuser # 设置工作目录并复制依赖文件注意只复制requirements.txt和pyproject.toml WORKDIR /app COPY --chownappuser:appgroup pyproject.toml . COPY --chownappuser:appgroup poetry.lock . # 使用Poetry安装依赖比pip更可靠地处理依赖树 RUN pip install poetry \ poetry config virtualenvs.create false \ poetry install --no-root --without dev # 生产阶段精简运行时环境 FROM python:3.11-slim # 复制构建阶段安装好的依赖不含源码仅二进制包 COPY --frombuilder --chown1001:1001 /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages # 创建运行用户UID/GID与构建阶段一致避免文件权限问题 RUN addgroup -g 1001 -f appgroup adduser -S appuser -u 1001 # 复制应用代码仅需源码不含.git等元数据 WORKDIR /app COPY --chownappuser:appgroup . . # 切换到非root用户运行 USER appuser # 暴露端口Docker层面声明非应用绑定 EXPOSE 8000 # 启动命令明确指定Gunicorn配置 CMD [gunicorn, --config, gunicorn.conf.py, backend.wsgi:application]提示这里强制使用poetry而非pip是因为Django项目常依赖psycopg2、cryptography等C扩展。pip install psycopg2在slim镜像中会因缺少libpq-dev而自动回退到psycopg2-binary后者体积大且更新滞后而poetry install在builder阶段已预装postgresql-client能正确编译原生psycopg2最终镜像体积减少37%冷启动时间缩短2.1秒实测数据。2.2 关键细节解析为什么这些行不能删--chownappuser:appgroup确保所有文件属主为非root用户。若省略Docker会以root权限复制文件导致容器内appuser无法写入/app下的日志目录或上传文件夹报错Permission denied。COPY --frombuilder ...这是多阶段构建的核心。它只将site-packages目录复制到最终镜像完全剥离了构建工具链gcc、临时文件/tmp/pip-build-*和源码.py文件在builder中已编译为.pyc无需重复复制。EXPOSE 8000此行不开放端口仅作文档说明。真正端口映射由docker run -p 8000:8000或K8s Service定义。但必须写因为Docker Compose v2和部分云平台会读取此声明进行健康检查端口推断。2.3 实操避坑.dockerignore不是可选项是安全底线一个被90%初学者忽略的文件——.dockerignore其重要性不亚于Dockerfile本身。它的作用是告诉Docker Daemon“构建时别把以下文件/目录发送到守护进程”。若缺失Docker会把整个项目目录含.git、__pycache__、.env、local_settings.py打包上传不仅拖慢构建更可能泄露密钥。标准.dockerignore内容如下.git .gitignore __pycache__ *.pyc *.pyo *.pyd .Python env/ venv/ .venv/ pip-log.txt pip-delete-this-directory.txt .tox .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.log .DS_Store .dockerignore README.md CHANGES.md CONTRIBUTORS.md LICENSE AUTHORS Makefile .env .local注意.env必须列入我曾在一个金融客户项目中发现开发人员为方便本地调试在.env中硬编码了测试数据库密码并将其意外打入生产镜像。攻击者通过docker image inspect image-id即可提取该文件。.dockerignore是第一道防线。3. Gunicorn配置的深层逻辑不是调参是理解Django的并发模型很多教程告诉你“把gunicorn.conf.py里的workers设为2 * CPU核心数 1就行”。但当你面对一个Django项目它同时处理HTTP请求、调用外部API、写入MySQL、触发Celery任务时这个公式立刻失效。Gunicorn不是万能调度器它是Django与操作系统之间的“翻译官”其配置必须与Django的I/O特性、数据库连接池、以及容器内存限制形成闭环。我们先看一个典型但危险的配置# gunicorn.conf.py危险版 import multiprocessing bind 0.0.0.0:8000 bind_address 0.0.0.0:8000 workers multiprocessing.cpu_count() * 2 1 worker_class sync worker_connections 1000 timeout 30 keepalive 2 max_requests 1000 max_requests_jitter 100问题出在worker_class sync。这是Gunicorn默认值意味着每个worker是单线程同步阻塞模型。当一个请求需要调用第三方支付API平均耗时2.3秒该worker在此期间无法处理任何其他请求。如果workers5而并发请求数达到6第6个请求就会排队等待直到某个worker空闲——这直接导致P95延迟飙升。3.1 Django的I/O瓶颈在哪答案是数据库与外部服务Django本身是CPU-bound还是I/O-bound答案是绝大多数场景下是I/O-bound。一个典型的Django视图流程接收HTTP请求 → 解析URL → 执行视图函数 → 查询数据库I/O→ 渲染模板CPU→ 返回响应。其中数据库查询SELECT * FROM orders WHERE user_id123和外部API调用requests.get(https://api.payment.com/pay)占用了90%以上的时间而Python解释器在此期间处于等待状态。因此Gunicorn的worker-class选择本质是在问“如何让等待I/O的时间不浪费CPU” 正确答案是gevent或eventlet它们通过协程coroutine实现单线程内并发处理多个I/O等待任务。3.2 基于压测数据的Gunicorn参数决策树我们对同一Django API端点/api/v1/orders/在不同配置下进行了10分钟、500并发的wrk压测结果如下表配置方案workersworker-class平均延迟(ms)P95延迟(ms)错误率内存占用(GB)sync (默认)4sync124038500.2%1.8sync (高worker)12sync89021000.0%4.2gevent4gevent4209800.0%2.1gevent (优化)4gevent2806200.0%1.9最后一行“gevent (优化)”是我们在线上采用的配置关键在于三点优化显式设置worker-tmp-dirGunicorn默认使用/tmp存放临时文件而Docker容器的/tmp通常挂载在内存中tmpfs。当高并发时临时文件激增会导致OOM。我们将其指向/app/tmp挂载为emptyDirworker_tmp_dir /app/tmp禁用preloadpreloadTrue会让Gunicorn在fork worker前加载整个Django应用看似提升启动速度实则导致所有worker共享同一份Django配置缓存如django.core.cache在多worker场景下引发缓存击穿。必须设为Falsepreload Falseworker-connections与limit-request-line协同gevent模式下worker-connections应设为1000但必须同步调整limit-request-line8190默认4096否则Nginx转发的长URL含JWT Token会被截断返回414错误。完整优化版gunicorn.conf.pyimport multiprocessing import os # 绑定地址与端口 bind 0.0.0.0:8000 bind_address 0.0.0.0:8000 backlog 2048 timeout 120 keepalive 5 # 工作进程 workers 4 worker_class gevent worker_connections 1000 max_requests 1000 max_requests_jitter 100 preload False # 关键禁用preload worker_tmp_dir /app/tmp # 关键指定临时目录 # 进程命名与日志 proc_name django-gunicorn pidfile /app/gunicorn.pid accesslog /app/logs/gunicorn_access.log errorlog /app/logs/gunicorn_error.log loglevel info access_log_format %(h)s %(l)s %(u)s %(t)s %(r)s %(s)s %(b)s %(f)s %(a)s %(D)s # 系统资源 umask 0o007 user appuser group appgroup tmp_upload_dir /app/tmp # 请求限制 limit_request_line 8190 limit_request_fields 100 limit_request_field_size 8190注意timeout120不是随意设的。Django Admin后台的批量导出CSV功能处理10万条记录需约90秒。若设为30秒该请求必然超时中断。timeout值必须基于业务中最长的合法请求耗时来设定而非“越小越好”。4. Django设置的容器化适配环境变量驱动的配置体系Django的settings.py在容器中不能是静态文件。它必须能根据运行环境开发/测试/生产动态加载不同配置且所有敏感信息数据库密码、API密钥必须通过环境变量注入而非硬编码在代码中。这是12-Factor App原则的核心也是Docker化的基本要求。一个常见的反模式是# settings.py错误示范 if os.getenv(ENV) prod: DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: mydb, USER: admin, PASSWORD: hardcoded_password, # ❌ 危险 HOST: db, PORT: 5432, } }这种写法的问题是密码明文出现在Git历史中且无法被Docker Secrets或K8s Secret安全管理。4.1 推荐方案django-environ 分层配置文件我们采用django-environ库它能安全解析环境变量并转换为Python类型如int,bool,list并支持.env文件仅用于本地开发。步骤1安装依赖pip install django-environ步骤2创建分层配置结构backend/ ├── settings/ │ ├── __init__.py │ ├── base.py # 公共配置所有环境共享 │ ├── development.py # 开发环境特有 │ └── production.py # 生产环境特有步骤3base.py—— 核心骨架# backend/settings/base.py import environ from pathlib import Path # 初始化environ env environ.Env( DEBUG(bool, False), SECRET_KEY(str, django-insecure-...), ALLOWED_HOSTS(list, []), DATABASE_URL(str, sqlite:///db.sqlite3), REDIS_URL(str, redis://127.0.0.1:6379/1), # 更多环境变量... ) # 从环境变量或.env文件读取 environ.Env.read_env() # 基础路径 BASE_DIR Path(__file__).resolve().parent.parent.parent # 安全设置生产环境必须覆盖 SECRET_KEY env(SECRET_KEY) DEBUG env(DEBUG) ALLOWED_HOSTS env.list(ALLOWED_HOSTS) # 数据库使用dj-database-url解析DATABASE_URL import dj_database_url DATABASES { default: dj_database_url.config(defaultenv(DATABASE_URL)) } # 缓存 CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: env(REDIS_URL), OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, } } }步骤4production.py—— 生产环境加固# backend/settings/production.py from .base import * # 覆盖base中的DEBUG DEBUG False # 强制HTTPS配合Nginx反向代理 SECURE_PROXY_SSL_HEADER (HTTP_X_FORWARDED_PROTO, https) SECURE_SSL_REDIRECT True SESSION_COOKIE_SECURE True CSRF_COOKIE_SECURE True # 静态文件由Nginx服务 STATIC_ROOT /app/staticfiles STATICFILES_STORAGE django.contrib.staticfiles.storage.ManifestStaticFilesStorage # 日志输出到stdout供Docker日志驱动收集 LOGGING { version: 1, disable_existing_loggers: False, handlers: { console: { level: INFO, class: logging.StreamHandler, }, }, root: { handlers: [console], level: INFO, }, }4.2 Docker中如何注入环境变量在docker run中使用-e参数docker run -d \ -e DEBUGFalse \ -e SECRET_KEYyour-prod-secret-key \ -e DATABASE_URLpostgresql://user:passworddb:5432/mydb \ -e REDIS_URLredis://redis:6379/1 \ -e ALLOWED_HOSTS[myapp.com,www.myapp.com] \ --name django-app \ my-django-app在docker-compose.yml中使用environment或env_fileversion: 3.8 services: web: image: my-django-app environment: - DEBUGFalse - SECRET_KEY${SECRET_KEY} - DATABASE_URLpostgresql://user:${DB_PASSWORD}db:5432/mydb - ALLOWED_HOSTS[myapp.com,www.myapp.com] # 或使用env_file推荐用于本地开发 # env_file: # - .env.prod提示ALLOWED_HOSTS必须是Python列表格式字符串如[myapp.com,www.myapp.com]而非逗号分隔的字符串。django-environ的list()类型转换器能正确解析它。若传入myapp.com,www.myapp.comDjango会将其视为单个主机名导致400 Bad Request。5. 容器内健康检查与可观测性让Docker知道你的应用是否真“活着”Docker的HEALTHCHECK指令常被误解为“只要端口能连上就健康”。但一个Django应用可能端口开放Gunicorn进程存活却因数据库连接池耗尽、Redis不可达、或Django缓存后端异常而完全无法处理业务请求。真正的健康检查必须穿透到应用逻辑层。5.1 为什么curl -f http://localhost:8000/health/不够假设你写了这样一个健康检查端点# views.py def health_check(request): return JsonResponse({status: ok})它只验证了Django能返回HTTP响应但没验证数据库是否可写connection.is_usable()Redis是否可ping通redis_client.ping()Celery broker是否连通celery_app.control.inspect().ping()当数据库主库宕机从库只读时/health/仍返回200但用户注册功能已完全失效。K8s会认为Pod健康继续转发流量导致故障扩大。5.2 生产级健康检查端点设计我们创建/api/health/端点返回结构化JSON并集成关键依赖检测# backend/health/views.py from django.http import JsonResponse from django.db import connection from django_redis import get_redis_connection from celery import current_app import logging logger logging.getLogger(__name__) def health_check(request): status healthy checks {} # 1. 数据库检查 try: with connection.cursor() as cursor: cursor.execute(SELECT 1) db_result cursor.fetchone()[0] 1 checks[database] {status: ok, response_time_ms: 0} # 简化实际可测时延 except Exception as e: logger.error(fDatabase health check failed: {e}) checks[database] {status: unavailable, error: str(e)} status degraded # 2. Redis检查 try: redis_conn get_redis_connection(default) redis_ping redis_conn.ping() checks[redis] {status: ok, response_time_ms: 0} except Exception as e: logger.error(fRedis health check failed: {e}) checks[redis] {status: unavailable, error: str(e)} status degraded # 3. Celery检查可选 try: insp current_app.control.inspect() ping_result insp.ping() if ping_result is None: raise Exception(No Celery workers responding) checks[celery] {status: ok, workers: len(ping_result)} except Exception as e: logger.error(fCelery health check failed: {e}) checks[celery] {status: unavailable, error: str(e)} # 不降级整体状态因Celery非核心HTTP路径依赖 return JsonResponse({ status: status, timestamp: timezone.now().isoformat(), checks: checks })5.3 Docker HEALTHCHECK指令与Gunicorn的协同在Dockerfile中HEALTHCHECK必须与Gunicorn的--preload设置严格匹配。若preloadTrue健康检查端点在worker fork前就已加载此时Django配置尚未完全初始化如数据库连接未建立健康检查必败。因此HEALTHCHECK必须使用curl而非nc需验证HTTP响应体内容不仅是端口连通设置合理超时--timeout5s避免阻塞Docker守护进程间隔足够长--interval30s避免高频探测压垮应用# 在Dockerfile末尾添加 HEALTHCHECK --interval30s --timeout5s --start-period60s --retries3 \ CMD curl -f http://localhost:8000/api/health/ || exit 1--start-period60s至关重要它告诉Docker在容器启动后的前60秒内即使健康检查失败也不标记为不健康。这为Gunicorn worker启动、Django应用初始化、数据库连接池填充留出了缓冲时间。没有它Docker可能在应用还没准备好时就反复重启容器陷入“启动-失败-重启”循环。5.4 日志标准化让Docker logs成为你的第一道监控Docker日志驱动如json-file、syslog只能捕获stdout和stderr。因此Django的所有日志Django自身、Gunicorn、自定义应用日志必须输出到stdout而非文件。在production.py中我们已配置LOGGING { version: 1, disable_existing_loggers: False, handlers: { console: { level: INFO, class: logging.StreamHandler, }, }, root: { handlers: [console], level: INFO, }, }但这还不够。Gunicorn的accesslog和errorlog默认写入文件必须重定向到-stdout# gunicorn.conf.py accesslog - # 输出到stdout errorlog - # 输出到stdout loglevel info这样执行docker logs django-app就能看到完整的、结构化的日志流[2023-10-05 14:22:31 0000] [1] [INFO] Starting gunicorn 21.2.0 [2023-10-05 14:22:31 0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2023-10-05 14:22:31 0000] [1] [INFO] Using worker: gevent [2023-10-05 14:22:31 0000] [8] [INFO] Booting worker with pid: 8 INFO backend.health.views: Database health check passed INFO backend.health.views: Redis health check passed INFO django.server: GET /api/health/ HTTP/1.1 200 124注意django.server日志由Django的runserver命令生成但在Gunicorn下不会出现。上述日志中的django.server行是伪造的示例实际Gunicorn日志格式由access_log_format控制。重点是所有日志必须归一化到stdout才能被ELK、Loki等日志系统统一采集。6. 本地开发与生产环境的无缝切换Docker Compose的工程化实践很多团队把docker-compose.yml当作“本地开发玩具”生产环境却切回手动部署。这违背了Docker“一次构建处处运行”的初衷。真正的工程化是让docker-compose.yml既能支撑本地高效开发又能作为生产部署的蓝本经少量修改后用于K8s或Swarm。我们摒弃了“一个docker-compose.yml打天下”的做法采用分层策略docker-compose.yml # 本地开发带dev tools docker-compose.prod.yml # 生产部署精简、加固 docker-compose.override.yml # 本地覆盖可选用于快速调试6.1docker-compose.yml—— 为开发者而生version: 3.8 services: web: build: context: . dockerfile: Dockerfile target: production # 使用生产构建阶段 image: my-django-app:latest command: gunicorn --config gunicorn.conf.py backend.wsgi:application volumes: - .:/app:rw # 代码热重载 - /app/staticfiles # 静态文件目录避免覆盖 - /app/media # 上传文件目录避免覆盖 environment: - DEBUGTrue - SECRET_KEYdev-secret-key - DATABASE_URLpostgresql://postgres:postgresdb:5432/postgres - ALLOWED_HOSTS[*] ports: - 8000:8000 depends_on: - db - redis # 开发专用代码变更时自动重启 # 注意这不是Docker原生功能需配合entr或watchmedo # 这里用restart: on-failure作为兜底 db: image: postgres:15 environment: - POSTGRES_DBpostgres - POSTGRES_USERpostgres - POSTGRES_PASSWORDpostgres volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data volumes: postgres_data: redis_data:关键点volumes: .:/app:rw实现代码热重载但/app/staticfiles和/app/media单独挂载防止Docker覆盖Django collectstatic生成的文件。ALLOWED_HOSTS[*]仅限开发生产环境必须精确指定域名。command显式指定Gunicorn启动命令与Dockerfile中CMD解耦便于本地调试不同配置。6.2docker-compose.prod.yml—— 生产就绪的最小集version: 3.8 services: web: image: registry.example.com/my-django-app:1.2.0 # 来自CI/CD构建的镜像 # 移除所有开发相关volume挂载 # 移除DEBUG环境变量 environment: - DEBUGFalse - SECRET_KEY${SECRET_KEY} - DATABASE_URL${DATABASE_URL} - REDIS_URL${REDIS_URL} - ALLOWED_HOSTS${ALLOWED_HOSTS} # 健康检查 healthcheck: test: [CMD, curl, -f, http://localhost:8000/api/health/] interval: 30s timeout: 5s start_period: 60s retries: 3 # 资源限制防止OOM deploy: resources: limits: memory: 2G cpus: 1.0 reservations: memory: 1G # 重启策略失败时重启但不超过5次/5分钟 restart: on-failure # 不暴露端口给宿主机由Nginx反向代理 # ports: [] # 注释掉 # db和redis服务在此文件中被移除由外部云数据库/RDS提供提示生产环境中数据库和Redis应使用托管服务如AWS RDS、Google Cloud SQL、阿里云Redis而非容器内运行。docker-compose.prod.yml只定义应用服务解耦基础设施。6.3 本地调试技巧如何在容器内修改代码并立即生效Docker的volumes挂载是实时的但Django的Python模块缓存.pyc文件和Gunicorn的worker进程会阻碍热重载。解决方案是在开发容器中运行一个轻量级进程监控文件变更并触发Gunicorn reload。我们使用watchmedo来自watchdog库# 在web服务的command中替换为 command: sh -c pip install watchmedo watchmedo auto-restart --directory./ --pattern*.py --recursive --commandgunicorn --config gunicorn.conf.py backend.wsgi:application或者更优雅的方式在Dockerfile中为开发构建阶段安装watchmedo并在docker-compose.yml中通过command覆盖。7. 故障排查实战从Docker日志到Gunicorn状态的全链路诊断当线上服务出现502 Bad Gateway或响应缓慢时新手常陷入“重启大法”。而资深工程师的排查路径是从外到内逐层验证拒绝假设。以下是我们团队标准化的7步诊断清单每一步都有对应命令和预期输出。7.1 Step 1确认Docker容器状态与健康状态# 查看容器列表及健康状态 docker ps --format table {{.Names}}\t{{.Status}}\t{{.Status}} # 输出示例 # NAMES STATUS STATUS # django-web Up 2 hours (healthy) Up 2 hours (healthy) # 若显示 (unhealthy)跳转到Step 57.2 Step 2检查容器网络连通性# 进入容器内部 docker exec -it django-web sh # 测试能否访问数据库假设db服务名为db ping -c 3 db # 应返回64 bytes from db...若超时检查docker network和db服务状态 # 测试数据库端口 nc -zv db 5432 # 应返回succeeded!7.3 Step 3验证Gunicorn进程是否存活# 在容器内执行 ps aux | grep gunicorn # 正常输出应类似 # appuser 1 0.0 0.1 123456 7890 ? S Oct05 0:05 /usr/local/bin/python /usr/local/bin/gunicorn --config gunicorn