容器技术基础:Docker核心概念与实战
大家好,我是你们的老朋友。今天咱们聊聊Docker,这个在云原生和边缘计算领域绕不开的基础设施。说实话,我最早接触Docker是在2015年,那时候它还是个新鲜玩意儿。现在呢?几乎成了容器化部署的标配。你想想看,没有Docker,边缘计算设备上的应用分发、环境隔离、资源限制,得有多头疼。
这一章,我会带你从三个核心概念入手:镜像、容器、仓库。然后手把手教你写Dockerfile,最后用Docker Compose搞定多容器编排。嗯,都是干货,咱们直接开干。
1. Docker核心概念:镜像、容器、仓库
先别急着敲命令,咱们把概念理清楚。这三个东西,说白了就是“模板、实例、超市”的关系。
- 镜像(Image):一个只读的模板,包含运行应用所需的一切——代码、运行时、库、环境变量、配置文件。你可以把它理解成操作系统的ISO文件,或者一个类的定义。
- 容器(Container):镜像的运行实例。它是可写的,有生命周期。你可以启动、停止、删除它。就像从类实例化出来的对象。
- 仓库(Registry):存放镜像的地方。Docker Hub是最大的公共仓库,你也可以搭建私有仓库。就像GitHub存代码,仓库存镜像。
重要提醒:镜像分层是Docker的精髓。每个Dockerfile指令都会生成一个只读层,容器启动时在最上层加一个可写层。这种设计让镜像复用、缓存、分发变得极其高效。我在项目中遇到过,有人把多个应用塞进一个镜像,结果镜像体积飙到2GB,拉取慢得要命。后来拆成多个小镜像,问题迎刃而解。
为什么会这样?因为Docker的联合文件系统(UnionFS)只记录差异。你修改一个文件,不会复制整个镜像,只记录变化。嗯,这里要注意:如果你在容器里写日志,日志文件会留在可写层。容器删除后,数据就丢了。所以,持久化数据一定要用卷(Volume)或绑定挂载。
2. Dockerfile编写:从入门到避坑
Dockerfile就是构建镜像的“菜谱”。我个人的习惯是,每个项目根目录放一个Dockerfile,尽量保持简洁。咱们看一个典型的Node.js应用Dockerfile:
# 基础镜像,尽量选alpine版本,体积小
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 先复制package.json,利用缓存
COPY package*.json ./
# 安装依赖
RUN npm install --production
# 复制源代码
COPY . .
# 暴露端口
EXPOSE 3000
# 启动命令
CMD ["node", "app.js"]
你看,我特意把COPY package*.json ./放在COPY . .前面。为什么?因为Docker构建时会缓存每一层。如果package.json没变,npm install这层就不会重新执行,能省下大量时间。我曾经见过有人把所有文件一股脑复制进去,每次改一行代码都要重新装依赖,气得直拍桌子。
个人经验:写Dockerfile时,尽量把“变化频率低”的指令放在前面,“变化频率高”的放在后面。这样能最大化利用缓存。另外,RUN指令尽量合并,减少层数。比如RUN apt-get update && apt-get install -y curl,别拆成两个RUN。
再来说说多阶段构建。这是减少镜像体积的利器。比如你编译Go程序,第一阶段装Go编译器编译,第二阶段只复制编译好的二进制文件。最终镜像里没有编译器,体积能小一个数量级。
# 第一阶段:编译
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 第二阶段:运行
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
嗯,这里要注意:多阶段构建时,第一阶段的基础镜像可以很大,反正最后不会留在最终镜像里。我有个项目,第一阶段用golang:1.20(约800MB),第二阶段用alpine(约5MB),最终镜像只有15MB。部署到边缘设备上,爽得不行。
3. Docker Compose多容器编排
单容器好办,但现实中的应用往往需要多个服务:前端、后端、数据库、缓存、消息队列……一个个手动启动?太原始了。Docker Compose就是干这个的。
它用YAML文件定义一组容器,一条命令就能全部启动。咱们看一个典型的Web应用例子:
version: '3.8'
services:
web:
build: ./web
ports:
- "3000:3000"
depends_on:
- api
api:
build: ./api
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=5432
depends_on:
- db
db:
image: postgres:15-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
volumes:
pgdata:
你看,depends_on控制启动顺序,volumes持久化数据,environment传递配置。我个人习惯把敏感信息(比如密码)放在.env文件里,而不是直接写在YAML中。你想想看,如果把密码写死在Compose文件里,传到Git仓库,那可就“社死”了。
避坑指南:我曾经在生产环境遇到过一个问题:Compose文件里没指定restart: always,结果某个服务挂了之后,整个应用链式崩溃。后来我养成了习惯,关键服务一定要加restart: unless-stopped。另外,depends_on只保证启动顺序,不保证服务就绪。比如数据库启动后,可能还需要几秒才能接受连接。这时候需要配合健康检查(healthcheck)或等待脚本。
Docker Compose还有一个杀手锏:docker-compose scale(旧版)或--scale参数(新版)。你可以轻松扩展某个服务的实例数。比如docker-compose up --scale api=3,就能启动3个API实例,配合负载均衡,实现水平扩展。当然,在边缘计算场景下,资源有限,别盲目扩展,要根据设备性能来。
知识体系结构图
下面这张SVG图,帮你理清本章的知识脉络。从Docker核心概念出发,到Dockerfile构建镜像,再到Compose编排多容器,层层递进。
好了,这一章的内容就到这里。镜像、容器、仓库是基础,Dockerfile是构建工具,Compose是编排利器。把这些吃透了,后面的Kubernetes、边缘节点部署才能游刃有余。记住,动手实践是最好的学习方式——找个项目,写个Dockerfile,用Compose跑起来,遇到问题再回来翻翻这一章。