第四章:容器化入门——Docker核心概念与实战

好,咱们进入容器化这个环节。说实话,容器化是我个人认为从传统IT到云架构转型过程中,最让人「眼前一亮」的技术。我记得第一次在项目里用上Docker时,那种「环境不一致」的噩梦终于结束了。今天我就带你彻底搞懂Docker的核心概念,以及怎么把它用起来。

4.1 容器与VM的区别:别再傻傻分不清

很多刚接触容器的朋友会问:「容器不就是轻量级的虚拟机吗?」嗯,这个说法对了一半。咱们先看个对比表,你就明白了。

对比维度 虚拟机(VM) 容器(Container)
启动速度 分钟级(需要启动完整OS) 秒级(共享宿主机内核)
资源占用 每个VM有独立OS,占用GB级内存 共享内核,占用MB级内存
隔离级别 硬件级虚拟化,强隔离 进程级隔离(namespace+cgroups)
镜像大小 通常几个GB 通常几十到几百MB
移植性 依赖Hypervisor,迁移较复杂 一次构建,到处运行

说白了,虚拟机是在硬件层面做虚拟化,每个VM都有一套完整的操作系统。而容器呢?它只是把应用程序及其依赖打包在一起,共享宿主机的操作系统内核。我刚开始用容器时也犯过糊涂——以为容器能像VM一样跑Windows应用,结果发现Linux容器只能跑Linux应用。这个坑,你记住就好。

核心区别一句话: 容器是进程级别的隔离,VM是操作系统级别的隔离。容器更轻、更快,但隔离性不如VM。

4.2 Docker三大核心概念:镜像、容器、仓库

这三个概念,你搞懂了,Docker就算入门了。我习惯用一个类比来解释:镜像就是「菜谱」,容器就是「做好的菜」,仓库就是「菜谱市场」

4.2.1 镜像(Image)

镜像是只读的模板,包含了运行应用所需的一切:代码、运行时、库、环境变量、配置文件。你可以把它理解成一个「快照」。我在项目中经常遇到有人问:「为什么我改了容器里的文件,重启后又变回去了?」嗯,因为容器是基于镜像启动的,容器内的修改不会影响镜像本身。要持久化修改,你得重新构建镜像。

4.2.2 容器(Container)

容器是镜像的运行实例。你可以启动、停止、删除容器。每个容器都是隔离的、安全的。我曾经在生产环境遇到过一个问题:一个容器里的日志把磁盘写满了,导致其他容器也受影响。后来我学会了给每个容器设置资源限制——docker run --memory=512m --cpus=1,这样就不会互相干扰了。

4.2.3 仓库(Registry)

仓库用来存储和分发镜像。最常用的是Docker Hub,但企业里一般用私有仓库,比如Harbor。我记得有一次团队里有人从Docker Hub拉了个镜像,结果里面带了漏洞。从那以后,我们规定所有镜像必须经过安全扫描才能上线。

小技巧:docker image ls 查看本地镜像,用 docker ps -a 查看所有容器(包括已停止的)。

4.3 Dockerfile编写:从零构建你的第一个镜像

Dockerfile就是构建镜像的「配方」。你想想看,如果没有Dockerfile,每次部署都要手动配置环境,那得多痛苦?下面我写一个最简单的Python Web应用的Dockerfile,你感受一下。

# 使用官方Python基础镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt .

# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 5000

# 启动命令
CMD ["python", "app.py"]

这里有几个关键点我要强调一下:

  • FROM:基础镜像的选择很重要。我建议用官方镜像,比如 python:3.9-slimpython:3.9 小很多,因为去掉了不必要的工具。
  • RUN:每个RUN指令都会创建一个新的镜像层。所以尽量把多个命令合并成一个RUN,比如 RUN apt-get update && apt-get install -y xxx,这样可以减少镜像层数。
  • CMD vs ENTRYPOINT:CMD可以被覆盖,ENTRYPOINT不行。我一般用CMD,因为更灵活。
避坑指南: 我曾经在Dockerfile里写了 COPY . /app,结果把本地的一大堆临时文件也复制进去了。后来我学会了用 .dockerignore 文件来排除不需要的文件,比如 node_modules.git 等。

4.4 Docker Compose编排:多容器应用不再头疼

单个容器好办,但实际项目里往往是多个服务配合:Web应用、数据库、缓存、消息队列……这时候手动启动每个容器就太累了。Docker Compose就是用来解决这个问题的。

来看一个典型的Web应用 + Redis + MySQL的编排文件:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - redis
      - db
    environment:
      - REDIS_HOST=redis
      - DB_HOST=db

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: myapp
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

你只需要一个 docker-compose up -d 命令,所有服务就都起来了。我个人习惯把Compose文件放在项目根目录,这样团队成员拉下代码后,直接就能跑起来。

这里有个细节:depends_on 只控制启动顺序,不等待服务真正就绪。比如MySQL启动需要几秒钟,但Compose不会等它完全可用。我曾经踩过这个坑——Web应用启动时连不上数据库,直接崩溃了。解决方案是在应用代码里加个重试机制,或者用 wait-for-it.sh 脚本。

常用Compose命令:
docker-compose up -d 启动所有服务(后台运行)
docker-compose down 停止并删除所有容器
docker-compose logs -f 查看实时日志
docker-compose ps 查看服务状态

4.5 实战经验总结

最后,我分享几个在实际项目中积累的经验:

  • 镜像要小:尽量用Alpine版本的基础镜像,能省不少磁盘空间和网络带宽。
  • 容器要轻:一个容器只跑一个进程,别把SSH、systemd塞进去。
  • 数据要持久:数据库等有状态服务,一定要用volume挂载数据目录,否则容器删了数据就没了。
  • 日志要集中:别把日志写在容器里,用 docker logs 或者挂载到宿主机目录。

嗯,容器化这部分内容就到这里。你可能会觉得信息量有点大,但没关系,动手实践几次就熟了。下一章我们会聊聊容器编排的进阶话题——Kubernetes,那才是真正的大规模容器管理利器。