本节摘要:数据库服务模板给出 MySQL、PostgreSQL、MongoDB 三份可直接抄的 compose 配置,覆盖环境变量初始化用户与库、数据卷持久化、健康检查、初始化脚本挂载、字符集与时区五项能力,并讨论数据库端口为何不该对外暴露、主从变体怎么写。读完后你能在五分钟内起一个带账号、带健康检查、数据不会丢的数据库服务。
数据库镜像优先用官方镜像。官方镜像经过安全审查,行为也最接近原生发行版。版本号要写具体而不是 latest:mysql:8.0、postgres:14、mongo:5.0、redis:7 都比 latest 可靠,因为 latest 会在你不知情时升级大版本,主版本升级往往伴随不兼容变更,哪天重启后连不上库,排查成本远高于当初选版本的成本。
选型还要看业务形态:MySQL 生态成熟、运维资料多,是默认选择;PostgreSQL 在复杂查询、JSON 字段、窗口函数上更强;MongoDB 适合文档型数据模型,字段结构频繁变化的业务最省心。三份模板结构几乎一样,差异只在环境变量名和数据目录。
services: db: image: mysql:8.0 container_name: mysql_db restart: always environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_DATABASE: my_database MYSQL_USER: my_user MYSQL_PASSWORD: my_password ports: - "3306:3306" volumes: - db_data:/var/lib/mysql - ./initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] timeout: 20s retries: 3 start_period: 5s volumes: db_data:
关键行逐个说:
MYSQL_ROOT_PASSWORD:root 账号密码,首次初始化数据目录时生效并写入配置。MYSQL_DATABASE 加 MYSQL_USER 加 MYSQL_PASSWORD:初始化时自动建库、建用户、授权,业务连接串直接用它,不要用 root 跑业务。db_data:/var/lib/mysql:数据目录挂命名卷。MySQL 8 的数据目录结构在首次启动时生成,卷里一旦有数据,环境变量就不再生效——这是理解数据库容器的一把钥匙:初始化只发生在空卷上。./initdb:/docker-entrypoint-initdb.d:ro:把宿主机的 SQL 脚本目录挂进去,首次初始化时按文件名顺序自动执行。mysqladmin ping 探测本机,-p${MYSQL_ROOT_PASSWORD} 引用 compose 同目录 .env 里的变量,避免把密码写死在 yml 里。start_period: 5s 表示启动后 5 秒内不计数失败,给首次初始化留时间;retries: 3 是连续失败三次才判定不健康。字符集与时区:MySQL 8 默认字符集是 utf8mb4,表情符号等四字节字符没问题,但排序规则默认是 latin1 系的旧习惯可能要改。稳妥做法是在配置里显式声明:
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: TZ: Asia/Shanghai
command 追加 mysqld 启动参数;TZ 环境变量让容器内日志时间与业务时区对齐。写入的数据和时区是两回事,时区只影响 NOW 这类函数的取值,连接串里也可以单独指定,但容器层先对齐能少很多迷惑。
连接串写法:应用容器里主机名直接写服务名 db,Python 的 SQLAlchemy 连接串形如 mysql+pymysql://my_user:my_password@db:3306/my_database,端口是容器内部端口 3306,与宿主机映射无关。
PostgreSQL 与 MySQL 的模板只差三个环境变量名和一个数据目录:
services: db: image: postgres:14 container_name: postgres_db restart: always environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_password POSTGRES_DB: my_database TZ: Asia/Shanghai ports: - "5432:5432" volumes: - db_data:/var/lib/postgresql/data - ./initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U my_user -d my_database"] timeout: 20s retries: 3 start_period: 5s volumes: db_data:
POSTGRES_USER 同时创建超级用户与同名数据库,POSTGRES_DB 再补一个业务库。注意这里没有独立的 root 概念,默认用户就是 POSTGRES_USER,密码即 POSTGRES_PASSWORD。/var/lib/postgresql/data 挂命名卷。不要挂成绑定目录加子路径,比如把宿主目录直接挂到该路径,PostgreSQL 会因为目录权限或属主不一致拒绝启动。pg_isready -U my_user -d my_database,它只回答"服务器能不能接受连接",不验证密码,正合适做探活。/docker-entrypoint-initdb.d,MySQL、PostgreSQL、MongoDB 三家的镜像共用这套约定:只放 .sql 或 .sh 文件,首次空卷初始化时按字典序执行。时区写法不同:PostgreSQL 里容器 TZ 只影响日志,数据库内部时区要用 command: ["postgres", "-c", "timezone=Asia/Shanghai"] 或建库时指定。实践里更常见的做法是应用层统一用 UTC 存储、展示层转换,避免每个环境改时区。
⚠️ PostgreSQL 的 UID 是 999:官方镜像以 uid 999 运行。如果做绑定挂载,宿主目录要先 chown 999:999,否则容器没有写权限,日志里会出现 permission denied。用命名卷则没有这个问题,卷由 Docker 管理属主。
MongoDB 默认不鉴权,直接映射端口等于裸奔,模板里第一步就是开启账号:
services: db: image: mongo:5.0 container_name: mongo_db restart: always environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: root_password ports: - "27017:27017" volumes: - db_data:/data/db - ./initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "mongosh", "--eval", "db.runCommand({ping:1})"] timeout: 20s retries: 3 start_period: 5s volumes: db_data:
MONGO_INITDB_ROOT_USERNAME 与 MONGO_INITDB_ROOT_PASSWORD 创建 root 管理员;MONGO_INITDB_DATABASE 可选,指定后把初始化脚本应用到该库。/data/db。MongoDB 4.4 起官方镜像不再支持 --auth 参数,鉴权完全由环境变量控制,所以这两个变量是必填项而不是可选项。mongosh --eval db.runCommand({ping:1}),返回 ok 即存活。老版本镜像里没有 mongosh 只有 mongo shell,命令要换成 mongo --eval "db.runCommand({ping:1})",镜像版本换了检查命令也要跟着换。mongosh 创建业务库和业务用户,脚本同样只在首次空卷初始化时执行。三家数据库镜像都支持 /docker-entrypoint-initdb.d,但执行规则要记准,这是线上事故的高发区:
01_init.sql、02_seed.sql 这样的编号控制顺序。mysql -u root -p"$MYSQL_ROOT_PASSWORD" < /docker-entrypoint-initdb.d/data.sql。docker compose logs db 能看到具体报错行。整套启动逻辑用状态图可以一眼看穿,排查"为什么我的脚本没执行"时先对照这张图:
模板里 ports 映射是为了本地调试方便,但数据库端口暴露到宿主机意味着:任何能访问宿主机的人都能直接连数据库,绕过应用层的鉴权和限流。生产部署我们通常这么做:
services: db: image: postgres:14 environment: POSTGRES_USER: my_user POSTGRES_PASSWORD: my_password POSTGRES_DB: my_database volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U my_user -d my_database"] app: build: ./app depends_on: db: condition: service_healthy environment: DATABASE_URL: postgresql://my_user:my_password@db:5432/my_database
删掉 ports 段,应用容器与服务名 db 同网络,直接走内部端口 5432。需要临时管理数据库时,用 docker compose exec db psql -U my_user -d my_database 进容器操作,或者只把管理端口映射到回环地址 "127.0.0.1:5432:5432",宿主机以外的机器连不进来。
💡 备份也走内部网络:备份容器与数据库同网络,定时任务里用服务名连接。备份策略的完整做法见第 4 章,这里只提醒一点:先验证备份文件能恢复,再谈备份周期。
连接池与连接数上限。数据库容器默认的连接上限是镜像出厂值,MySQL 是 151,PostgreSQL 是 100。多个应用容器共用一个库时,每个应用连接池的默认大小乘容器数量,很容易撞上限,症状是间歇性的 connection refused。两类解法配合用:应用侧把连接池 max 压到合理值,比如 10 到 20;数据库侧按需调大,MySQL 在 command 里加 --max_connections=300,PostgreSQL 用 command: ["postgres", "-c", "max_connections=200"]。改上限前先想清楚:每个连接都有内存开销,盲开一千个连接不如让应用复用连接。
账号权限最小化。初始化环境变量只造了"一个用户一个库"的粗粒度权限。要再细分,初始化脚本里补授权语句:给只读报表应用建 GRANT SELECT 账号,给运维建带备份权限的专用账号,业务账号只拥有自己库的 DML 权限。授权语句写进 initdb 脚本的好处是随环境重建自动生效,不用每次手动敲。权限最小化在单机开发时看不出价值,但库一旦暴露在办公网,一个万能账号就是全网数据库的后门。
单库扛不住读写分离需求时,可以先用 compose 拉一个演示级主从。以 MySQL 为例,主库和两个从库各挂各的卷:
services: master: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: root_password MYSQL_REPLICATION_PASSWORD: replication_password ports: - "3306:3306" volumes: - master_data:/var/lib/mysql slave1: image: mysql:8.0 restart: always environment: MYSQL_REPLICATION_PASSWORD: replication_password MYSQL_REPLICATION_HOST: master volumes: - slave1_data:/var/lib/mysql depends_on: - master slave2: image: mysql:8.0 restart: always environment: MYSQL_REPLICATION_PASSWORD: replication_password MYSQL_REPLICATION_HOST: master volumes: - slave2_data:/var/lib/mysql depends_on: - master volumes: master_data: slave1_data: slave2_data:
MYSQL_REPLICATION_HOST 指向主库的服务名 master,从库靠它找到复制源;depends_on 保证主库先启动。这套配置能跑通主从同步,但离生产还有距离:没有 GTID 校验、没有故障自动切换、从库也不能自愈。真要上生产,要么用云厂商托管,要么上专门的复制编排工具,compose 只负责把拓扑搭出来。

数据库模板配完只是开始,真正决定数据安全的是备份链路。compose 里加一个备份容器是常见套路,以 PostgreSQL 为例:
backup: image: postgres:14 restart: "no" depends_on: db: condition: service_healthy environment: PGPASSWORD: my_password volumes: - ./backups:/backups command: > sh -c "pg_dump -h db -U my_user -d my_database -F c -f /backups/db_$(date +%F_%H%M).dump && ls -t /backups/*.dump | tail -n +8 | xargs -r rm"
-F c 输出自定义格式,恢复用 pg_restore,比纯 SQL 更灵活,可以只恢复部分表。ls -t | tail -n +8 | xargs rm 只保留最近 7 份,防止备份目录无限膨胀。restart: "no":备份是一次性任务,跑完即退出,不该被 restart 策略反复拉起。定时执行交给宿主 cron 或 3.6 节的 Airflow。mysqldump -u my_user -p密码 -h db my_database > 文件.sql,MongoDB 对应 mongodump --uri,三条命令的结构完全一致:进容器、连服务名、导出到挂载目录。恢复演练比备份本身重要。备份文件躺了三个月没人碰,等到事故现场才发现格式不对、权限不对,那备份等于没有。建议每季度做一次恢复演练:起一个全新 compose 项目,把 dump 恢复进去,抽查几条关键数据比对。MySQL 的恢复要小心:mysqldump 默认不含建库语句时,要先手动 CREATE DATABASE 再导入,漏了这步会报 Unknown database。
卷级备份是兜底。逻辑备份(dump)之外,数据卷本身也要有副本:停库状态下把卷内容复制出来,或者用 docker run --rm -v db_data:/data -v 宿主机目录:/backup alpine tar czf /backup/db_data.tar.gz -C /data . 打一个卷的 tar 包。卷级备份恢复快,但只适合同版本数据库;逻辑备份可跨版本迁移,两者互补,别只做一种。
资源限制:数据库容器建议直接给足资源上限,避免与其他服务抢内存。compose 里加 mem_limit: 1g 与 cpus: "2.0",MySQL 的 buffer pool、PostgreSQL 的 shared_buffers 都按这个上限调参。数据库是内存敏感型应用,配额给少了性能跳水,给多了挤垮邻居,这个度要靠压测找。
| 项目 | MySQL | PostgreSQL | MongoDB |
|---|---|---|---|
| 镜像 | mysql:8.0 | postgres:14 | mongo:5.0 |
| 超级用户变量 | MYSQL_ROOT_PASSWORD | POSTGRES_USER | MONGO_INITDB_ROOT_USERNAME |
| 业务库变量 | MYSQL_DATABASE | POSTGRES_DB | MONGO_INITDB_DATABASE |
| 数据目录 | /var/lib/mysql | /var/lib/postgresql/data | /data/db |
| 健康检查 | mysqladmin ping | pg_isready | mongosh ping |
| 初始化目录 | docker-entrypoint-initdb.d | docker-entrypoint-initdb.d | docker-entrypoint-initdb.d |
| 容器内 UID | 默认 root 运行 | 999 | 默认 root 运行 |
下一节给数据库加上加速器与解耦层——消息队列与缓存模板,四种组件各有各的性格。