Docker 部署的四个关键决定:数据、密钥、迁移、标签

作者:程序员白大力 · 2026-09-28 09:14
Docker 部署的四个关键决定:数据、密钥、迁移、标签

封面

把小项目装进 Docker,很多人当成「写个 Dockerfile 再 docker compose up」就完事。第一周确实一切正常,直到某次更新:停掉旧容器、拉新镜像、起新容器——数据库空了,用户上传的文件没了。或者反过来,一切正常运行,某天有人拉了镜像随手 docker history 一看,密钥明晃晃躺在里面。

这类事故的共同点在于:容器化不是打包技巧,而是四个必须想清楚的决定——数据放哪、密钥怎么进、数据库迁移什么时候跑、镜像叫什么名字。这篇文章把这四个决定逐个理清。

一、数据放哪:容器是用完即弃的

先理解一个机制:容器 = 只读镜像层 + 一层很薄的可写层。应用运行时写进容器文件系统的所有东西——SQLite 数据库文件、用户上传的图片——都落在那层可写层上,而可写层跟着容器实例走,容器一删,数据就地蒸发。注意是「删」而不是「停」:docker stop 再 docker start,数据还在;docker rm 之后再起新容器,数据就没了。

所以原则只有一句话:容器里不存任何有价值的东西,有价值的都通过挂载落到宿主机。两种挂载方式怎么选:

维度 命名卷(named volume) 绑定挂载(bind mount)
位置 Docker 统一管理,宿主机路径不直观 宿主机上的明确目录
备份 需先定位卷的实际路径 直接进目录拷贝
权限坑 少,Docker 处理初始归属 可能遇到 UID 不匹配
适用 生产默认选择 需要在宿主机直接看文件时

以 SQLite 这类文件型数据库为例,数据库文件所在的整个目录都要挂出来,而且要挂本地磁盘(命名卷或宿主机目录都行)——SQLite 依赖文件锁,放在 NFS 之类的网络文件系统上会出现锁失效导致数据损坏,这是官方文档明确警告过的场景。如果你已经在应用层配好了 WAL 模式和超时参数,容器化本身不冲突,重点是那一对 -wal/-shm 文件和数据库文件在同一个挂载目录里即可。

一个典型的 compose 片段,数据落在命名卷、密钥从环境变量注入(下一节展开):

services:
  web:
    image: myapp:1.4.0        # 具体版本号,不用 latest
    volumes:
      - app-data:/app/data    # 数据全部落卷
    environment:
      DJANGO_SETTINGS_MODULE: mysite.settings.production
    restart: unless-stopped
volumes:
  app-data:

为什么这么写:卷独立于容器生命周期,更新镜像、重建容器,数据原地不动;镜像用具体版本号的原因在第四节。

二、密钥怎么进:构建期和运行期是两个问题

最常见的错误写法是在 Dockerfile 里直接 ENV SECRET_KEY=xxx,或者把 .env 文件 COPY 进镜像,之后即使删掉也于事无补——镜像层是不可变的,ENV/ARG 的值会永久留在镜像配置和层元数据里,任何拿到镜像的人跑一下 docker inspect 或 docker history 就能原样取回。Docker 官方甚至为这个错误专门内置了构建检查规则(SecretsUsedInArgOrEnv),明确写着 ARG/ENV 不应用于敏感数据。

正确思路是把「构建期需要什么」和「运行期需要什么」分开:

场景 正确做法 说明
构建时要拉私有依赖 BuildKit secret 挂载 只存在于那一步 RUN 里,不落层
运行时用 SECRET_KEY、数据库密码 docker run -e、--env-file 或 compose 的 environment/env_file 随容器启动注入,不进镜像
兜底 .dockerignore 排除 .env、*.pem、密钥目录 防止 COPY . . 顺手把密钥打进构建上下文

配置层面再守一条线:Django 的 SECRET_KEY、数据库连接串这类值,settings 里一律从环境变量读,代码仓库里永远只有变量名,没有值。

三、迁移什么时候跑:构建期跑 migrate 是错的

Django 项目容器化时,一个高频困惑是 migrate 放在哪一步。两个位置是明确错的:

一是写在 Dockerfile 的 RUN 里。 构建镜像时数据库通常根本不存在(单机部署时数据库文件还在卷里,跟构建过程毫无关系),这一步要么报错要么跑了寂寞。更本质的原因是:镜像是构建产物,迁移是部署动作,两者生命周期不同——同一个镜像今天部署到 A 环境、明天部署到 B 环境,各自面对的数据库状态不一样,迁移不可能提前「烧」进镜像。

二是让 makemigrations 出现在任何自动化环节。 它属于开发流程:改完模型,开发者在代码库里生成迁移文件、进版本控制。放进构建或启动脚本里自动跑,容易产生不同容器各自生成、迁移文件历史不一致的问题。

正确位置是部署时、启动前:migrate 是幂等的——已应用的迁移再跑一次是无操作,不会重复执行。所以常见做法是交给 entrypoint 脚本,在启动应用服务器之前先跑一遍:

#!/bin/sh
set -e
python manage.py migrate --noinput   # 幂等,已应用则跳过
exec gunicorn mysite.wsgi:application --bind 0.0.0.0:8000

为什么这么写:首启和每次升级都能自动把库结构对齐,exec 让 gunicorn 接管 1 号进程、正确接收信号。多副本同时扩容的场景例外——多个容器并发跑迁移有竞争风险,那时应把迁移拆成独立的先导步骤,跑完再起应用容器;单机小项目没有这个顾虑。

四、镜像叫什么:latest 不是「最新版本」的意思

docker run myapp 不写标签时,默认补上的就是 latest,很多人以为它指「最新发布的版本」。实际语义是:最后一次未指定标签的 push。Docker 官方博客专门澄清过这个误解——先推了 1.0.1 再有人推了个不带标签的构建,latest 指向的是后者,可能比 1.0.1 还旧。标签是可变的:任何人都可以把 latest(甚至 v1.0.0)重新指向另一个镜像,它不提供任何版本保证。

后果很具体:出问题时你说不清线上跑的到底是哪个构建;想回滚时,本地的 latest 可能已经被新镜像覆盖。生产环境的做法:

■
每次构建打具体版本号(如 myapp:1.4.0),部署文件里写死这个版本号;
■
最好同时打一个 git 提交号标签,方便从线上容器反查到源码;
■
对可复现性要求高的场景,可以进一步用摘要(@sha256:...)锁定镜像,摘要不可变,连镜像被覆盖的风险都消除。
结尾

回头看,这四个决定其实是同一条原则的四个切面:镜像负责「程序」,容器负责「跑」,一切有状态的东西——数据、密钥、数据库结构——都要想清楚它存在哪、跟着谁的生命周期走。想清楚这四件事,容器化才算真正省心而不是埋雷。如果你也被其中某一坑过,欢迎转发给还没踩到的同事。

参考文章
■
《Docker 官方文档:Volumes(持久化数据)》
■
《Docker 官方构建检查:SecretsUsedInArgOrEnv》
■
《Docker 官方博客:Using Tags and Labels to Manage Docker Image Sprawl》
■
《Stack Overflow:How do you perform Django database migrations when using Docker-Compose?》
■
《Docker 官方文档:Dockerfile 最佳实践(镜像标签与摘要)》

#Docker #Django #部署运维 #SQLite #容器化 #后端开发

本文由 AI 辅助生成并经人工审核发布,内容仅供参考,不构成法律意见。