前阵子带一个新人,让他用 Docker 起个 MySQL,他从某篇博客抄了条命令:
docker run --name some-mysql --link some-app:app -d mysql跑不通,来问我。我一看就知道这教程是七八年前的——--link 这个参数 Docker 官方早就标记废弃了,现在推荐用自定义网络。更离谱的是那篇教程里 MySQL 还是 5.6、Node.js 还是 0.10,这些版本现在连安全更新都没有了。
网上关于 Docker 镜像的教程有个通病:抄来抄去,越抄越老。很多内容的源头是同一篇十年前的文章,命令和版本号一路被复制粘贴到今天,早就跟现实脱节了。
这篇我按 2026 年的实际情况重新整理一遍,覆盖:镜像命名规则怎么看懂、基础镜像怎么选、常用中间件(MySQL/Redis/MongoDB/Nginx/PostgreSQL)的现代正确用法、语言运行时镜像、以及镜像瘦身和版本锁定。所有命令都在 Docker 24+ / Compose v2 上实测过。
先搞懂镜像名字的结构
很多人用了一年 Docker 也说不清 registry/namespace/repository:tag 到底每段是啥。拆开看:
docker.io/library/mysql:8.4 │ │ │ │ │ │ │ └─ tag(版本标签) │ │ └─────── repository(镜像名) │ └─────────────── namespace(命名空间/组织) └───────────────────────── registry(仓库地址)几个省略规则:
- docker pull mysql 实际等于 docker pull docker.io/library/mysql:latest
- Docker Hub 官方镜像的 namespace 是 library,可以省略,所以 mysql、nginx、redis 这些都是官方镜像
- 第三方镜像必须带 namespace,比如 bitnami/redis、grafana/grafana
- tag 不写默认是 latest——这是个坑,后面单独讲
官方镜像 vs 第三方镜像怎么分辨:
| 类型 | 例子 | 特点 |
|---|---|---|
| Docker Official Image | mysql、nginx、redis | Docker Hub 页面有蓝色 ✔ 认证标,Docker 官方团队维护,安全和更新有保障 |
| Verified Publisher | bitnami/redis、hashicorp/consul | 厂商官方发布,有认证标 |
| 普通第三方 | someuser/mysql | 任何人可上传,质量参差,生产环境慎用 |
优先用 Docker Official Image。在 Docker Hub 搜索时勾选 "Docker Official Image" 过滤器,能过滤掉一堆来路不明的镜像。第三方镜像不是不能用(比如 Bitnami 的镜像封装得很规范),但要先看清楚维护者和更新频率。
基础系统镜像选型
自己写 Dockerfile 时,FROM 后面选什么基础镜像,直接决定了最终镜像的体积和安全性。主流选择:
| 基础镜像 | 体积 | 适用场景 | 注意 |
|---|---|---|---|
| ubuntu:24.04 | ~78MB | 需要完整 Linux 环境、glibc 依赖多 | 体积偏大 |
| debian:12-slim | ~74MB | 通用,比 ubuntu 稍精简 | 平衡之选 |
| alpine:3.20 | ~8MB | 追求极小体积 | 用 musl libc,某些依赖 glibc 的程序会有兼容问题 |
| distroless | ~2MB | 生产环境、安全优先 | 没有 shell,调试困难 |
| scratch | 0 | 静态编译的单文件程序(Go/Rust) | 什么都没有,需自带一切 |
Alpine 的坑:体积小是真香,但它用的是 musl libc 而不是 glibc。一些依赖 glibc 特性的程序(比如某些版本的 Oracle JDK、需要特定 DNS 解析行为的程序)在 Alpine 上会出诡异问题。Java 项目建议用基于 Debian 的 eclipse-temurin 而不是 alpine 版。
# Ubuntu 起个交互环境 docker run --rm -it ubuntu:24.04 bash # Alpine(进去后用 sh,默认没有 bash) docker run --rm -it alpine:3.20 shCentOS 特别说明:CentOS 8 已经在 2021 年底 EOL,centos:7 也在 2024 年 6 月停止维护。现在别再用 CentOS 镜像了,社区替代品是Rocky Linux或AlmaLinux(rockylinux:9、almalinux:9),或者直接用 Debian/Ubuntu。老教程里那些 centos:5、centos:6 更是彻底不能碰。
常用中间件镜像实战
下面每个中间件我都给"现代正确姿势"——自定义网络 + 命名卷持久化 + 健康检查 + 非 root,而不是老教程那种一条 docker run 裸奔。
先创建网络
容器之间通信用自定义网络,别再用 --link:
docker network create app-net自定义网络的好处:容器之间可以直接用容器名当主机名互相访问(内置 DNS 解析),而且网络隔离,只有同一网络的容器能互通。
MySQL
docker run -d \ --name mysql \ --network app-net \ -e MYSQL_ROOT_PASSWORD=ChangeMe_2026 \ -e MYSQL_DATABASE=app \ -e MYSQL_USER=app \ -e MYSQL_PASSWORD=AppPw_2026 \ -e TZ=Asia/Shanghai \ -v mysql_data:/var/lib/mysql \ -p 3306:3306 \ --health-cmd="mysqladmin ping -h localhost -uroot -pChangeMe_2026" \ --health-interval=10s \ --health-timeout=5s \ --health-retries=10 \ mysql:8.4关键点:
- -v mysql_data:/var/lib/mysql:必须挂命名卷,否则容器删了数据就没了。MySQL 数据目录在 /var/lib/mysql
- TZ=Asia/Shanghai:不设时区,容器内默认 UTC,日志时间差 8 小时
- mysql:8.4 是当前 LTS 版本,别再用 mysql:5.7(已 EOL)
连接测试:
docker run -it --rm --network app-net mysql:8.4 \ mysql -hmysql -uapp -pAppPw_2026 app注意这里 -hmysql 直接用容器名,因为两个容器在同一个 app-net 网络里,DNS 能解析。这就是取代 --link 的现代写法。
PostgreSQL
docker run -d \ --name postgres \ --network app-net \ -e POSTGRES_PASSWORD=ChangeMe_2026 \ -e POSTGRES_DB=app \ -e TZ=Asia/Shanghai \ -v pg_data:/var/lib/postgresql/data \ -p 5432:5432 \ --health-cmd="pg_isready -U postgres" \ --health-interval=10s \ --health-retries=5 \ postgres:16PostgreSQL 的数据目录是 /var/lib/postgresql/data,别忘了挂卷。
Redis
docker run -d \ --name redis \ --network app-net \ -v redis_data:/data \ -p 6379:6379 \ --health-cmd="redis-cli ping" \ --health-interval=10s \ --health-retries=5 \ redis:7.4 redis-server --appendonly yes --requirepass ChangeMe_2026关键点:
- --appendonly yes:开启 AOF 持久化,默认 Redis 重启数据会丢
- --requirepass:设密码。老教程里 Redis 裸奔不设密码,是勒索病毒的头号目标,生产环境一定要设密码 + 不暴露公网端口
- -v redis_data:/data:Redis 数据目录是 /data
连接:
docker run -it --rm --network app-net redis:7.4 \ redis-cli -h redis -a ChangeMe_2026MongoDB
docker run -d \ --name mongo \ --network app-net \ -e MONGO_INITDB_ROOT_USERNAME=admin \ -e MONGO_INITDB_ROOT_PASSWORD=ChangeMe_2026 \ -v mongo_data:/data/db \ -p 27017:27017 \ mongo:8.0MongoDB 数据目录 /data/db。老教程里的 mongo:2.6、mongo:2.7 现在完全不能用了,当前稳定版是 8.0。新版客户端命令也从 mongo 改成了 mongosh:
docker run -it --rm --network app-net mongo:8.0 \ mongosh "mongodb://admin:ChangeMe_2026@mongo:27017"Nginx
docker run -d \ --name nginx \ --network app-net \ -v ./nginx.conf:/etc/nginx/nginx.conf:ro \ -v ./html:/usr/share/nginx/html:ro \ -v ./logs:/var/log/nginx \ -p 80:80 \ -p 443:443 \ nginx:1.27-alpine挂载说明:
- 配置文件 :ro 只读挂载,防止容器内进程改宿主机文件
- 静态页面目录 /usr/share/nginx/html
- 日志目录 /var/log/nginx,挂出来方便查看
自定义配置里如果要反向代理到别的容器,upstream 直接写容器名:
server { listen 80; location / { proxy_pass http://app:3000; # app 是同网络里另一个容器的名字 } }nginx:1.27-alpine 用的是 alpine 版,体积只有 ~20MB,Nginx 不涉及 glibc 兼容问题,用 alpine 很合适。
语言运行时镜像
Node.js
# 用 LTS 版本,2026 年当前 LTS 是 22.x FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production # ci 比 install 更适合生产(严格按 lock 文件装) COPY . . EXPOSE 3000 # 用 node 用户跑,别用 root USER node CMD ["node", "server.js"]老教程里的 node:0.10-onbuild 早就没了,-onbuild 这种变体现在也不推荐用(隐式行为太多,不好排查)。当前 Node LTS 是 22.x,选 node:22-alpine 或 node:22-slim。
Python
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "app.py"]--no-cache-dir 不缓存 pip 下载包,能减小镜像体积。Python 官方镜像的 slim 版基于 Debian,比完整版小一半。
Java(Eclipse Temurin)
FROM eclipse-temurin:17-jre-alpine # 或者用 debian 版避免 musl 兼容问题:eclipse-temurin:17-jre-jammy WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]Java 别再用 openjdk 官方镜像了——它在 Docker Hub 上已经标记 deprecated,官方推荐迁移到Eclipse Temurin(eclipse-temurin)、Amazon Corretto(amazoncorretto)或 Azul Zulu(azul/zulu)。生产环境用 -jre 变体(只含运行时,比 -jdk 小 200MB+)。
tag 策略:别再无脑用 latest
latest 是个误导性极强的标签,两个坑:
- 它不保证是最新版——latest 只是镜像维护者手动打的一个 tag,可能很久没更新,也可能指向某个具体版本,完全看维护者心情
- 不可复现——你今天 pull nginx:latest 拿到 1.27,下周可能变成 1.28,配置不兼容就直接炸
正确做法:
- 生产环境锁定具体版本号:mysql:8.4.2 而不是 mysql:8 或 mysql:latest
- 想跟大版本可以用 mysql:8.4(minor 版本),但要在 CI 里定期测试
- 追求绝对确定性用digest:mysql@sha256:abc123...,digest 是内容哈希,永远不变
# 查看某个 tag 对应的 digest docker inspect --format='{{index .RepoDigests 0}}' mysql:8.4 # 用 digest 拉取(内容锁定) docker pull mysql@sha256:xxxxxxxxxxxx镜像瘦身:多阶段构建
编译型语言(Go/Rust/Java/前端)镜像容易臃肿,因为构建工具链占大头。多阶段构建能把构建环境和运行环境分离:
# ---- 构建阶段 ---- FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B # 单独一层,利用缓存 COPY src ./src RUN mvn package -DskipTests -B # ---- 运行阶段 ---- FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]最终镜像只包含运行阶段的内容,Maven 和源码都不在里面。一个 Spring Boot 应用用这种方式能从 ~800MB 压到 ~200MB。
前端项目同理:
# 构建阶段用 node FROM node:22-alpine AS builder WORKDIR /build COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段用 nginx 托管静态文件 FROM nginx:1.27-alpine COPY --from=builder /build/dist /usr/share/nginx/html EXPOSE 80最终镜像就是个 nginx + 静态文件,node 和 node_modules 全部丢弃,体积从几百 MB 降到 ~25MB。
Docker Hub 拉取限制
2026 年绕不开的现实:Docker Hub 对匿名用户有拉取频率限制(大概每 6 小时 100 次,登录用户 200 次),CI 环境或者团队多人共用一个出口 IP 时很容易触发 toomanyrequests 错误。
三个应对办法:
- 配置国内镜像加速器(这块我上一篇专门写过,配置 /etc/docker/daemon.json 的 registry-mirrors)
- 搭私有 Registry 或用 Harbor:公司内部把常用镜像 pull 下来推到自建 Harbor,团队从内网拉,又快又不受限制
- 付费 Docker Hub 账号:Pro/Team 计划有更高限额
企业环境推荐第 2 种。Harbor 还能做镜像扫描(Trivy)、访问控制、镜像签名,比裸用 Docker Hub 安全得多。
把它们串起来:一份完整的 Compose 编排
前面每个中间件都是单独 docker run,实际项目里会用 Compose 统一管理。下面这份把 Nginx + 应用 + MySQL + Redis 串起来,是前面所有知识点的整合,可以直接改改用:
# compose.yml (Compose v2,不需要 version 字段) services: nginx: image: nginx:1.27-alpine container_name: nginx ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/html:/usr/share/nginx/html:ro depends_on: - app restart: unless-stopped networks: [frontend, backend] app: image: registry.company.com/team/myapp:1.4.2 # 从私有仓库拉,锁定版本号 container_name: app environment: DB_HOST: mysql REDIS_HOST: redis TZ: Asia/Shanghai depends_on: mysql: condition: service_healthy # 等 MySQL 真正就绪,不是只等启动 redis: condition: service_healthy restart: unless-stopped networks: [backend] mysql: image: mysql:8.4 container_name: mysql environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: app MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} TZ: Asia/Shanghai volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 10 start_period: 30s restart: unless-stopped networks: [backend] redis: image: redis:7.4-alpine container_name: redis command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"] volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: [backend] volumes: mysql_data: redis_data: networks: frontend: backend: internal: true # 后端网络不通外网,MySQL/Redis 不暴露公网配套 .env(不提交到 Git):
DB_ROOT_PASSWORD=ChangeMe_Root_2026 DB_USER=app DB_PASSWORD=ChangeMe_App_2026 REDIS_PASSWORD=ChangeMe_Redis_2026这份编排里几个设计值得单独说:
- 两个网络隔离:frontend 让 Nginx 对外,backend 设 internal: true,MySQL 和 Redis 只在内网,宿主机端口根本不暴露,安全性直接拉满
- condition: service_healthy:应用容器等 MySQL 健康检查通过才启动,避免"数据库还没初始化完应用就连过去报 Connection refused"
- 镜像地址走私有仓库 + 锁版本号:registry.company.com/team/myapp:1.4.2,可复现、可回滚
启动:
docker compose up -d # 后台启动全部 docker compose ps # 看状态 docker compose logs -f app # 看某个服务日志 docker compose down # 停止(保留数据卷)私有镜像仓库:团队怎么分发镜像
自己构建的镜像要分发给团队,不可能都从 Docker Hub 走(也不该把公司代码推到公网)。常见方案:
| 方案 | 适用 | 特点 |
|---|---|---|
| Docker Registry | 个人/小团队 | 官方开源,最轻量,docker run -d -p 5000:5000 registry:2 就能起 |
| Harbor | 中大型企业 | 带 Web UI、权限管理、镜像扫描(Trivy)、复制、签名,功能全 |
| 云厂商 ACR | 上云的团队 | 阿里云/腾讯云容器镜像服务,免运维,跟云生态集成好 |
推送流程(以 Harbor 为例):
docker login registry.company.com # 登录 docker tag myapp:1.4.2 registry.company.com/team/myapp:1.4.2 # 打仓库前缀 docker push registry.company.com/team/myapp:1.4.2 # 推送Harbor 相比裸 Registry 最大的价值是镜像安全扫描——推上去自动用 Trivy 扫一遍 CVE 漏洞,有问题直接拦住不让部署,这在有安全合规要求的团队里基本是刚需。
常见坑速查
容器一启动就退出:多半是主进程跑完就退了。Docker 容器生命周期绑定前台进程,CMD 里跑的服务必须在前台(比如 nginx 要 daemon off;,很多服务有 --foreground 参数)。用 docker logs 容器名 看退出原因。
数据改了重启就丢:没挂卷。写进容器可写层的数据在容器删除后就没了,重要数据必须挂 volume 或 bind mount。
端口冲突 port is already allocated:宿主机端口被占了,netstat -tlnp | grep 端口 或 docker ps 看谁占的。
容器之间 ping 不通:不在同一个网络。老教程用 --link,现在用 docker network create + --network 把它们放一起,然后用容器名互访。
镜像越拉越多占满磁盘:docker system df 看占用,docker system prune -a 清理无用镜像和停止的容器(注意 -a 会删所有未被使用的镜像,确认后再执行)。
拉取报 toomanyrequests:Docker Hub 限流,见上一节。
小结
用 Docker 镜像这几年,我的经验就三条:
- 优先官方镜像,锁定具体版本号,别碰 latest 和来路不明的第三方镜像
- 中间件一定挂命名卷 + 配健康检查 + 设密码,老教程那种裸奔的 docker run 在生产环境是灾难
- 自己写的镜像用多阶段构建瘦身,构建工具链不要带进运行镜像
网上很多 Docker 镜像教程是十年前的内容一路抄下来的,--link、centos:6、node:0.10 这些早就该进博物馆了。这篇文章里的命令都是按 2026 年当前的 Docker 24+ / Compose v2 写的,可以直接抄。
有具体镜像使用上的问题,评论区聊。