为什么要容器化
同一套代码在开发机跑得好好的,上线后环境不一样就出问题,这是后端最熟悉的痛。容器把应用连同运行环境一起打包成标准制品,镜像到哪都能跑,部署从折腾环境变成简单替换镜像。Docker 是容器化的事实标准,配合编排工具还能实现弹性伸缩与滚动发布。
- 镜像应用加环境的只读模板。
- 容器镜像运行起来的实例。
- 卷把数据持久化到容器外。
- Compose用声明文件编排多服务。
Dockerfile 与镜像构建
Dockerfile 是构建镜像的说明书:FROM 指定基础镜像,COPY 拷贝代码,RUN 执行安装命令,EXPOSE 声明端口,CMD 定义启动命令。每一行指令都会产生一个镜像层,写得越精简,构建越快、镜像越小。
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
把依赖安装放在代码拷贝之前,能充分利用层缓存:只要 package.json 没变,依赖层就不会重建,开发时改代码秒级出镜像。
镜像分层与仓库
镜像由一层层只读层叠加而成,推送到镜像仓库时只传输新增层,拉取时也能并发拉层,这是镜像比整机快的原因。基础镜像尽量选官方精简版,例如 alpine,体积小、攻击面小;不要图省事装一堆用不到的包。
docker build -t registry.example.com/app:v1.2.3 .
docker push registry.example.com/app:v1.2.3
版本号建议带上 git 短提交或流水线序号,保证可追溯;latest 标签只用于开发,生产部署永远指向明确版本,回滚才有依据。
容器运行与资源限制
容器不是无限制地吃资源,生产上必须显式限制 CPU 与内存,防止单个容器把整台机器拖垮。内存超限容器会被 OOM 杀掉,所以限制值要留足余量,并与 JVM 等运行时参数匹配。
docker run -d --name app \
--memory 512m --cpus 1 \
-p 8080:3000 app:v1.2.3
容器内应用要感知资源限制:Java 用容器感知参数,Node 设置合理的堆上限,日志要打到标准输出并由容器收集,不要往容器内写大文件。
容器网络
默认每个容器有独立网络命名空间。Docker 提供 bridge、host、none 等网络模式,生产常用自定义 bridge 网络,让容器之间用服务名互相访问,不用记 IP。对外服务用端口映射暴露,内部组件不映射端口,减少暴露面。
docker network create app-net
docker run --network app-net --name api api:v1
多个服务放在同一个自定义网络里,api 容器可以直接通过 http://db:3306 访问数据库容器,比维护 IP 列表可靠得多。
数据持久化与卷
容器删除后写入层的文件就没了,数据库、上传文件这类数据必须放卷里。Docker 卷由 Docker 管理,适合数据库;bind mount 把宿主目录映射进容器,适合配置文件与开发热更新。
docker run -d --name mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret mysql:8
用卷后容器重建数据不丢,备份也直接备份卷目录。注意权限:容器内用户与宿主用户 uid 不一致会引发读写问题,尽量保持基础镜像默认用户并显式管理权限。
Compose 多服务编排
一套后端系统往往有应用、数据库、缓存、队列多个组件,Compose 用一个 docker-compose.yml 声明全部服务,一条命令拉起整栈,开发与测试环境最常用。服务之间用服务名互联,环境变量统一管理。
services:
api:
build: .
ports: ["8080:3000"]
depends_on: [db, redis]
db:
image: mysql:8
volumes: [mysql-data:/var/lib/mysql]
volumes:
mysql-data:
Compose 适合单机多容器,生产多机编排要交给 Swarm 或 Kubernetes,但容器镜像与网络模型是同一套,迁移成本很低。
落地清单
- 基础镜像用官方精简版,减少体积与攻击面。
- 依赖安装放代码拷贝前,吃满层缓存。
- 版本号带提交号,部署用明确版本不用 latest。
- 限制 CPU 与内存,运行时参数与限制匹配。
- 数据落卷,日志走标准输出。
- 生产用自定义网络,内部组件不映射公网端口。