ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker部署FastAPI生产实践:Dockerfile与Compose

Docker部署FastAPI生产实践:Dockerfile与Compose Docker 部署 FastAPI 这件事第一次做和第十次做完全是两种体感。第一次你会觉得它无非就是把几条命令塞进一个文件docker run一敲页面出来了收工。做过几次之后你的关注点会彻底变掉镜像多大、构建缓存有没有命中、容器里的时区和宿主机差几个小时、数据库还没起来的时候 API 是不是直接崩掉、日志到底写进了 stdout 还是某个第二天就找不到的文件里。我前阵子把一个内部用的 FastAPI 服务从裸机搬到容器本来预算两小时收工结果从基础镜像选型一路折腾到 Nginx 转发丢 header中间踩的坑足够写一整篇长文。这篇不打算解释 Docker 是干什么的也不打算讲 FastAPI 怎么返回 JSON默认你已经能写几个接口、能在本地跑起uvicorn main:app --reload、也敲过docker run hello-world。我要讲的是把这个组合推到能上线上班的状态中间到底需要处理哪些细节Dockerfile 怎么分层才不浪费构建时间depends_on为什么救不了你CORS 为什么本地好使线上 403uvicorn的workers该填几健康检查到底检查什么以及那些看起来毫无头绪的报错信息背后真正的成因。刚把 FastAPI 跑通、准备往服务器上搬的人适合看已经部署过但总觉得哪里不对劲的人也适合看。1. FastAPI 服务离开本机后问题到底出在哪一层1.1 裸机部署的三个隐形成本很多人第一次上线 FastAPI 是用nohup uvicorn main:app --host 0.0.0.0 --port 8000 这种方式的。跑是能跑但三个成本会在后面陆续找上门。第一个是环境漂移开发机是 Python 3.11服务器上是 3.8本地装某个依赖时是直接下 wheel服务器上没有预编译包pip 只能现场编译于是你需要gcc、python3-dev、libpq-dev、libffi-dev一串系统包只要缺一个就是error: command gcc failed with exit status 1。第二个是进程管理nohup不会在进程挂掉后重启服务器重启后服务不会自己起来日志默认写进nohup.out然后无限增长一个月后你可能发现这个文件已经 8 个 G。第三个是扩容与回滚想多开两台机器就得写脚本同步代码、同步虚拟环境、同步 systemd 配置回滚靠git checkout加手动重启中间那几分钟服务是抖的。这三个成本的性质是一样的它们都不是业务代码的问题而是运行环境没有被当成一件可交付的产物。容器化要解决的恰恰是这件事不是帮你把 Python 装得更好看。1.2 容器化真正冻结下来的东西把 FastAPI 装进镜像之后实际被冻结的不只是 Python 版本而是整条依赖链解释器、系统库、pip 包版本、环境变量、启动命令、健康检查方式。镜像就是交付物开发和线上跑的是同一个 ID 的镜像sha256都对得上环境漂移这件事从根上被消掉了。进程管理也换了主体docker run --restart unless-stopped或者编排文件里的重启策略会替你做守护docker logs统一收口输出再配合日志驱动做轮转就不用担心磁盘被写爆。回滚也从改代码 重启变成了把旧 tag 的镜像重新拉起来秒级完成。但容器不是银弹它会顺手把几个新问题推到你面前配置怎么进去环境变量还是挂载文件、状态往哪存容器内写入的数据随容器生命周期消失、网络怎么连容器里的127.0.0.1是它自己的回环不是宿主机的、时间对不对默认 UTC日志里全是差八小时的记录。后面几章基本都在处理这四件事的衍生问题。2. Dockerfile 逐行拆解从能跑到构建得快2.1 基础镜像选型slim、alpine 和完整版怎么选选基础镜像这件事新手最容易凭越小越好直接选 alpine然后在装pandas、numpy、psycopg2或者cryptography的时候痛不欲生。原因是 alpine 用的是 musl libc而上游 PyPI 上的 wheel 绝大多数是按 glibc 编译的pip 找不到匹配的预编译包就会退回到源码编译编译过程中又缺各种头文件最后要么加一堆apk add把镜像搞得比 slim 还大要么直接报错放弃。我的实际取舍是这样绝大多数 FastAPI 服务用python:3.11-slim体积大概 120MB 起步glibc 环境绝大部分 wheel 能直接下确认依赖链里没有编译型包、纯 CPU 计算的轻量服务可以试 alpine完整版python:3.11基本不用除非你要在里面跑一些对系统工具依赖很深的脚本。另外版本号别只写python:3.11尽量写python:3.11.9-slim这种带补丁号的否则某天基础镜像悄悄升级你的构建结果就变了。基础镜像体积量级依赖安装体验适用场景python:3.11-slim约 120MB好wheel 命中率高默认首选python:3.11-alpine约 50MB差编译型包易翻车纯轻量纯逻辑服务python:3.11约 900MB最好工具齐全调试期或重系统依赖自建 distroless约 60MB需要手工搬文件安全要求高的场景2.2 依赖层缓存为什么 requirements.txt 要先 COPYDockerfile 的每条指令是一层只有内容变了才会失效后面的缓存。很多人写成COPY . .之后才RUN pip install -r requirements.txt结果是你改一行业务代码整个镜像的依赖安装层全部失效每次构建都要重新下几百兆包在服务器上构建一次要等五六分钟。正确顺序是先只把依赖清单拷进去、装完依赖再把源码拷进去COPY requirements.txt . RUN pip install --upgrade pip pip install -r requirements.txt COPY . .这样只要requirements.txt没动改业务代码时依赖层直接命中缓存构建时间能压到十几秒。配套的还有.dockerignore这是被严重低估的一个文件。默认上下文里如果有.git、__pycache__、.venv、node_modules、测试数据构建时会全部打包上传给 Docker daemon几百兆的上下文传一次就够你喝一壶而且.env这种含密钥的文件还可能被误打进镜像。我的.dockerignore至少会写这些.git .gitignore .venv __pycache__ *.pyc .pytest_cache .mypy_cache node_modules dist *.log .env .env.* !.env.example2.3 多阶段构建把编译工具留在门外如果依赖里确实有需要编译的包比如没有现成 wheel 的 C 扩展构建阶段就得装build-essential、python3-dev这些工具但这些东西在运行阶段完全是负担镜像白白大几百兆攻击面也变大。多阶段构建的套路是用一个 builder 阶段把虚拟环境装好再把整个 venv 目录搬到运行阶段。这里有个细节要注意venv 是可以整体搬运的只要两边 Python 版本和小版本完全一致路径也保持一样都放在/opt/venvshebang 就不会指错。# ---------- 构建阶段 ---------- FROM python:3.11.9-slim AS builder WORKDIR /build RUN python -m venv /opt/venv ENV PATH/opt/venv/bin:$PATH \ PIP_NO_CACHE_DIR1 \ PIP_DISABLE_PIP_VERSION_CHECK1 COPY requirements.txt . RUN pip install --upgrade pip pip install -r requirements.txt # ---------- 运行阶段 ---------- FROM python:3.11.9-slim AS runtime ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ TZAsia/Shanghai \ PATH/opt/venv/bin:$PATH RUN apt-get update \ apt-get install -y --no-install-recommends tzdata curl \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ echo $TZ /etc/timezone \ rm -rf /var/lib/apt/lists/* COPY --frombuilder /opt/venv /opt/venv RUN useradd -m -u 1000 appuser WORKDIR /app COPY --chownappuser:appuser . . USER appuser EXPOSE 8000 HEALTHCHECK --interval30s --timeout3s --start-period20s --retries3 \ CMD curl -fsS http://127.0.0.1:8000/api/health || exit 1 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000, \ --proxy-headers, --forwarded-allow-ips, *]PYTHONUNBUFFERED1这行必须留着。Python 默认会缓冲 stdout容器里跑起来后你在docker logs看不到实时日志以为服务卡住了其实只是缓冲区没满。useradd那两行也别省用非 root 用户跑进程是基本习惯能避免容器被拿到后直接拥有 root 权限。这里有个坑COPY --chown里的 UID 和你在useradd时指定的 UID 要对上不然文件属主会变成一个不存在的数字程序读文件时可能报权限错误。2.4 镜像瘦身与可复现性之间的平衡镜像小确实有好处拉取快、部署快、冷启动快。但别为了压体积把curl也删掉因为HEALTHCHECK要用它也别把tzdata删了因为后面时区问题排查起来很烦。真正值得做的是清apt缓存、用--no-install-recommends、PIP_NO_CACHE_DIR1、把多阶段做好。至于requirements.txt我强烈建议用pip freeze生成、或者更规范地拆成requirements.txt直接依赖写松一点的版本范围requirements.lock锁定完整依赖树构建时只装 lock 文件这样能避免某天某个间接依赖发了新版本导致构建结果变化。3. compose 编排本地调试和生产部署是两套逻辑3.1 docker-compose.yml 里最容易写错的字段version字段在新版 Compose 里已经废弃了写了只会看到一句 warning删掉即可。ports和expose的区别也常被搞混expose只是声明端口给同网络的其他容器用不对外发布ports: [8000:8000]才是在宿主机的 8000 上监听。生产环境里如果前面挂了 Nginx其实完全不需要把 API 的端口发布到宿主机只发布 Nginx 的 80/443API 只expose就够了这样宿主机上不会莫名其妙多出好几个监听端口也少了一层暴露面。环境变量有三种塞法environment直接写在文件里、env_file指向一个.env、build args在构建期传入。密钥类的东西千万别写成build args构建参数会留在镜像历史里docker history一查就能看到。写进env_file是目前比较舒服的做法只要记得把.env加进.gitignore然后在仓库里放一份.env.example当模板。3.2 depends_on 不等待健康数据库没起来时服务怎么活这是新手最常踩的坑写了depends_on: [db]就以为 API 会等数据库就绪实际上depends_on只保证容器启动顺序不保证容器内的服务可用。数据库容器可能已经 running但postgres进程还在初始化数据目录这时候 API 连过去就是connection refused如果连接池初始化失败又没有重试进程直接退出重启策略再把它拉起来如此往复。正确做法是给依赖服务加healthcheck然后让 API 通过条件等待depends_on: db: condition: service_healthy cache: condition: service_started同时healthcheck本身要选对检测手段。数据库用pg_isready或者mysqladmin pingRedis 用redis-cli ping别用curl去探一个不存在的 HTTP 端口。另外应用层还是要做重试因为编排只能覆盖启动阶段运行期数据库重启、主从切换你的连接池都得能自己恢复。SQLAlchemy 里配pool_pre_pingTrue、pool_recycle1800是两行几乎零成本但收益很高的设置。3.3 热重载、卷挂载与生产环境的取舍开发阶段我一般用一个docker-compose.override.yml把源码目录挂进去、启动命令换成--reloadservices: api: volumes: - ./app:/app/app:ro environment: - LOG_LEVELDEBUG command: [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000, --reload]注意:ro只读挂载能防止容器内进程意外写坏宿主机上的源码。生产环境绝不能挂载源码也绝不能用--reload--reload会起一个监视进程内存占用上来文件多了 CPU 也一直有开销而且它在容器里监听文件变化的行为跟宿主机文件系统驱动有关系某些环境上根本收不到事件。至于restart策略unless-stopped比always更符合直觉你手动docker stop掉的容器重启宿主机后不会自己又冒出来。资源限制也建议写死不然某个接口写出内存泄漏一个容器能把宿主机吃干deploy: resources: limits: cpus: 2.0 memory: 1g4. FastAPI 在容器里最容易翻车的四个配置4.1 CORS为什么本地好使、线上 403前后端分离场景下前端跑在https://web.example.comAPI 在https://api.example.com浏览器跨域。本地开发时你写了app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[*], allow_headers[*], )本地用 cookie 测试居然通过了因为浏览器对localhost的同源判断比较宽松。上线后带 cookie 的请求全部被拦控制台报The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include。这是规范限制allow_credentialsTrue和通配符源不能同时存在。解决办法只有一条——显式列出源settings get_settings() app.add_middleware( CORSMiddleware, allow_originssettings.cors_origins, allow_credentialsTrue, allow_methods[GET, POST, PUT, PATCH, DELETE, OPTIONS], allow_headers[Authorization, Content-Type, X-Request-Id], expose_headers[X-Request-Id], max_age600, )这里有个很隐蔽的重复配置坑如果 Nginx 那边也加了add_header Access-Control-Allow-Origin *浏览器会看到两个值拼在一起*, https://web.example.com直接判定无效。CORS 只在一个地方加我倾向于全放在 FastAPI 里Nginx 只管转发。另外max_age600是减少预检请求的手段浏览器会把 OPTIONS 的结果缓存十分钟简单请求就不用每次都打一次预检。4.2 配置读取环境变量、文件、优先级FastAPI 本身不带配置系统用pydantic-settings是最顺手的做法。SettingsConfigDict里声明env_file之后读取优先级是环境变量 .env 文件 代码默认值。这个优先级顺序在容器里非常关键因为它意味着你可以把一个默认配置打进镜像然后靠 compose 里的environment覆盖掉特定值不用重新构建。from functools import lru_cache from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config SettingsConfigDict( env_file.env, env_file_encodingutf-8, extraignore, ) app_name: str fastapi-demo database_url: str redis_url: str redis://cache:6379/0 cors_origins: list[str] [] log_level: str INFO lru_cache def get_settings() - Settings: return Settings()lru_cache保证全局只解析一次避免每个请求都重新读环境变量。这里有个特别容易踩的点list[str]这种复合类型从环境变量读的时候pydantic 期望的是JSON 字符串所以你不能写CORS_ORIGINShttps://a.com,https://b.com得写成CORS_ORIGINS[https://web.example.com,https://admin.example.com]如果你的部署环境不方便写这种格式比如某些 CI 的变量里带引号会出问题那把字段定义成str再写个property做 split 更省心。关于密钥我的习惯是敏感字段一律不给默认值让它在缺失时直接抛错而不是悄悄用空字符串启动然后等到某个接口报 500 才发现。容器里最常见的错误就是.env写好了但 compose 里忘了env_file本地跑得好好的容器里database_url缺失。这种启动就崩比跑起来才崩好排查一百倍。4.3 workers 到底填几以及 gunicorn 还要不要uvicorn --workers N在 FastAPI 场景下基本够用。参数怎么定一个粗略的经验是同步阻塞代码多的话按 CPU 核数来N 核数 1纯异步 IO、数据库和 Redis 网络等待占大头的话N 取核数甚至更少都行因为异步本身在单进程里就能扛住并发多开 worker 主要是为了跨 CPU 核心。盲目调大 worker 是有代价的每个 worker 一份独立内存连接池也是各算各的max_connections要按 worker 数乘一下不然数据库连接数会被打满。还有一个新手常问的问题要不要换成gunicorn -k uvicorn.workers.UvicornWorker。现在 uvicorn 自己的多进程管理器已经够稳定而且--workers简单直接没必要再套一层。它的真正价值在于你需要更细的进程管理和信号处理策略时但那是另一个话题了。容器里记得用exec形式写 CMD就是 JSON 数组形式这样 PID 1 是 uvicorn 本身能正确接收SIGTERM做优雅退出如果写成 shell 形式CMD uvicorn ...信号会先给 shell容器停止时会等超时才被强杀正在处理的请求直接被砍断。另外在容器内不要改--host必须绑0.0.0.0。绑127.0.0.1的话宿主机和同网络的其它容器都连不进来你会看到curl: (7) Failed to connect排查半天以为是端口映射问题。4.4 时区和日志容器里最容易被忽略的两件事Python 服务容器化后日志时间差八小时几乎人人都遇到过因为基础镜像默认是 UTC。处理方式是在运行阶段装tzdata并设置TZ环境变量前面 Dockerfile 里已经写了compose 里也可以再补一层environment: [TZAsia/Shanghai]做保险。注意TZ生效要求系统里有对应的时区文件光设变量不装tzdata是没用的这一点很多教程会漏掉。日志方面容器环境下唯一正确的输出目标是stdout/stderr也就是别写logging.FileHandler。写文件的话文件在容器内容器一重建就没了而且你也没法用docker logs看。FastAPI 里我会把 logger 的 handler 直接指到logging.StreamHandler()每条日志带上request_id然后用docker logs -f --tail 200 api实时看。要落地到文件做长期留存交给 Docker 的日志驱动和宿主机的轮转配置应用层不掺和。5. 反向代理、健康检查与受限网络下的应对5.1 Nginx 转发到容器那些丢 header 的细节生产上一般是宿主机 Nginx 转发到127.0.0.1:8000或者 Nginx 也在容器里、通过 compose 网络用服务名访问。两种都行但配置里有两处必须处理一是传递真实客户端信息不然后端拿到的request.client.host全是反代的 IP做限流和审计都没法用二是文件上传体积Nginx 默认只允许 1MB前端传个图片就 413。server { listen 80; server_name api.example.com; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; 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_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_connect_timeout 5s; proxy_read_timeout 60s; } }proxy_pass结尾有没有斜杠这件事一定要留意proxy_pass http://127.0.0.1:8000;无斜杠会把原始路径整段带过去proxy_pass http://127.0.0.1:8000/;有斜杠会把你location匹配到的那部分前缀替换掉。多数情况下你希望保持原路径就别加那个斜杠。同时 FastAPI 侧要用--proxy-headers --forwarded-allow-ips *启动它才会去信X-Forwarded-*这些头生成的文档 URL 和重定向地址才不会是http://加内部 IP。如果 API 挂在子路径下比如/api/v1还要处理root_path的问题否则 OpenAPI 文档页里的接口地址会缺前缀。可以在 Nginx 里proxy_set_header X-Forwarded-Prefix /api;FastAPI 侧用FastAPI(root_path/api)或者在启动参数里加--root-path /api。5.2 健康检查该检查什么优雅退出怎么做HEALTHCHECK里最忌讳的就是只检查端口通不通。端口开着但连接池已经废了、Redis 断了、磁盘满了这些情况进程都还活着。所以健康检查的接口要真的做一点实事查一下数据库、ping 一下 Redis但不要把慢查询放进去健康检查每隔 30 秒打一次必须轻量超时要能秒级返回。from fastapi import APIRouter, Response, status router APIRouter() router.get(/api/health) async def health(response: Response): try: async with engine.connect() as conn: await conn.execute(text(SELECT 1)) await redis_client.ping() except Exception: response.status_code status.HTTP_503_SERVICE_UNAVAILABLE return {status: degraded} return {status: ok}优雅退出这块FastAPI 用lifespan上下文管理器比老的app.on_event更合适因为它在启动和关闭时都能跑而且能在关闭阶段做一些等待from contextlib import asynccontextmanager from fastapi import FastAPI asynccontextmanager async def lifespan(app: FastAPI): await init_pool() yield await close_pool() # 给正在处理的请求留出收尾时间 app FastAPI(lifespanlifespan)compose 里再配stop_grace_period: 30s让 Docker 在发SIGTERM之后给足时间再SIGKILL。做过这一步之后滚动更新时用户基本感觉不到请求被中断。5.3 镜像拉取和受限环境下的应对内网服务器、离线机器、网络受限的环境下docker pull拉不动是最耽误时间的事。通常的处理是在daemon.json里配镜像加速地址配完要systemctl restart docker才生效。国内可用的加速地址这几年变动频繁比较稳的做法是在公司内网搭一个 registry 做中转把基础镜像和构建好的业务镜像都推进去这样线上机器只从内网拉速度和稳定性都可控。场景推荐做法注意点公网机器配置镜像加速改完 daemon.json 必须重启 Docker内网集群自建 registry 中转记得给镜像打全限定 tag完全离线docker save/docker load传输文件很大注意校验摘要跨架构docker buildx多平台构建arm 机器上跑 amd64 镜像性能差跨架构这件事值得单独提一句。如果目标机器是 arm 架构比如某些国产处理器平台或者树莓派类设备你在 amd64 开发机上构建出来的镜像跑不了或者靠模拟层跑但慢得离谱。正确做法是用docker buildx build --platform linux/arm64针对目标架构构建或者在目标机器上直接构建。6. 排错现场报错背后的真实原因6.1 权限与套接字连接类报错permission denied while trying to connect to the Docker daemon socket这个报错几乎每个 Linux 新手都撞过。意思是当前用户不在docker组里没有权限访问/var/run/docker.sock。解决方式是sudo usermod -aG docker $USER然后必须重新登录或者newgrp docker才生效很多人执行完不重登然后困惑为什么还是不行。还有一类是Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?在 Linux 上是服务没起来systemctl status docker看状态多半是配置改错了导致启动失败在 Windows 上看到的可能是failed to connect to the Docker API at npipe:////./pipe/dockerDesktopLinuxEngine这种那是 Docker Desktop 的后台服务没跑起来先开 Docker Desktop 等状态变成 running再执行命令。6.2 Windows 上 Docker Desktop 起不来Windows 环境下最常见的一条报错是虚拟化相关的提示大意是未检测到虚拟化支持。它的成因通常有三个层次BIOS/UEFI 里的虚拟化开关没打开、Windows 的虚拟化功能组件没启用、或者机器上同时装了两个基于虚拟化的软件导致冲突。排查顺序是先在任务管理器里看虚拟化是否显示为已启用再看系统功能里对应的组件有没有开最后检查是不是装了其它会占用虚拟化能力的工具。改完通常需要重启一次。另外 Windows 上 Docker Desktop 默认用 WSL2 后端如果 WSL 没装或者版本太旧也会连带起不来wsl --update是常规操作。6.3 端口占用、连不上的容器和装不上的包三个高频问题我整理成排查表遇到时按顺序过一遍基本能定位现象大概率原因排查动作port is already allocated宿主机端口被占ss -lntp看占用人改映射或停服务容器内连不上数据库用了127.0.0.1改成 compose 里的服务名容器内能连宿主机连不上只expose没ports补ports或从反代走pip 安装超时默认源太慢配置index-url指向内网源装包报编译失败缺编译工具或架构不匹配换 slim、装 build-essential、检查平台这里重点说下容器内连不上数据库。在 compose 网络里服务之间用服务名互相解析db、cache这些名字就是主机名。你在容器里写localhost:5432永远连不上因为那是容器自己的回环。这个错误在本地开发时不容易暴露因为很多人本地跑数据库用的是宿主机端口映射代码里写localhost恰好也能通一上生产就崩。6.4 依赖管理里的版本漂移pip install -r requirements.txt在本地成功、在容器里失败的场景八成是版本漂移。原因是你本地之前装过一堆包requirements.txt里只列了直接依赖间接依赖靠环境里已有的旧版本兜住了。容器是干净环境pip 去解析依赖树解出来的组合可能跟本地不同于是出错。解决办法前面提过用pip freeze生成完整锁定清单或者用pip-compile从requirements.in生成带哈希的 lock 文件。另外装依赖时加--no-cache-dir能省镜像体积但会牺牲重装速度构建环境里可以不加多阶段构建里的 builder 阶段本来就不进最终镜像。7. 从本地镜像到线上服务完整流程与日常习惯7.1 一份可以直接抄的 compose 配置把前面所有要点串起来生产版本的编排长这样services: api: build: context: . dockerfile: Dockerfile image: registry.internal/fastapi-demo:1.3.0 restart: unless-stopped env_file: - .env environment: - TZAsia/Shanghai expose: - 8000 depends_on: db: condition: service_healthy cache: condition: service_started healthcheck: test: [CMD, curl, -fsS, http://127.0.0.1:8000/api/health] interval: 30s timeout: 3s retries: 3 start_period: 20s stop_grace_period: 30s deploy: resources: limits: cpus: 2.0 memory: 1g logging: driver: json-file options: max-size: 50m max-file: 5 db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: app POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: appdb TZ: Asia/Shanghai volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U app -d appdb] interval: 10s timeout: 5s retries: 10 cache: image: redis:7-alpine restart: unless-stopped command: [redis-server, --appendonly, yes] volumes: pgdata:注意logging那段max-size和max-file是对日志轮转的直接约束不写的话默认不限制跑几个月磁盘就告急。这个配置比任何事后清理脚本都省事。7.2 常用命令速查与日常动作日常运维真正高频的命令其实就那么几个我贴一份自己常敲的# 构建并打上版本 tag docker compose build --no-cache api docker tag registry.internal/fastapi-demo:latest registry.internal/fastapi-demo:1.3.0 # 只重建 api 服务不碰数据库 docker compose up -d --no-deps --build api # 实时看日志只看最近 200 行 docker compose logs -f --tail 200 api # 进容器排查 docker compose exec api bash # 看资源占用确认没有内存泄漏 docker stats --no-stream # 清理无用镜像和构建缓存 docker image prune -f docker builder prune -f --filter until168hdocker compose up -d --no-deps --build api这个组合是我用得最多的它只重建 API 容器不会顺手把数据库也重启一遍。docker builder prune建议加上时间过滤无脑prune -a会把所有构建缓存清光下次构建又得从头来。7.3 版本、标签和回滚习惯镜像标签千万别只用latest。我的做法是git commit短哈希 语义版本双标签每次构建推两个 tag 上去回滚时直接改 compose 里的 tag 然后up -d。这样出问题时从发现问题到恢复服务基本在一分钟内完成而且回滚点非常明确不会出现回滚到 latest结果 latest 已经是新的了这种尴尬。数据库迁移脚本要单独考虑因为它不像镜像那样能随意回退所以每次上线前先把迁移脚本在预发环境跑一遍并且确认它是可前滚的。7.4 上线前我会过一遍的清单养成交付前自查的习惯之后线上事故少了一大半。我自己固定会确认这几件事镜像里没有.envdocker run --rm image ls -a查一下docker history里看不到明文密钥容器内时区正确docker compose exec api date健康检查接口在数据库断开时确实返回异常状态日志能在docker logs里实时看到资源限制和日志轮转都配了stop_grace_period大于接口最长处理时间最后一件事是在预发环境完整跑一遍前端调接口的流程因为 CORS 和 header 问题只有浏览器能真正暴露出来curl是测不出来的。最后分享一个我自己踩坑之后固定下来的习惯任何一次本地跑得好、容器里不行的问题先执行docker compose exec api env | sort看一眼环境变量再执行docker compose exec api python -c import sys; print(sys.path)看一眼模块路径。这两条命令能解决掉我遇到过的八成莫名其妙的问题比反复看日志快得多。
返回列表