Voocii博客
首页博客AI 热榜作品集读书友链工具关于
© 2026 Voocii. Built with Next.js & tRPC.
GitHubXEmailRSS


Docker: 从原理到部署

技术RickRick2025年12月15日·2 次阅读

简单总结下 Docker


一、Docker 到底解决了什么问题

在 Docker 之前,时不时会出现类似噩梦:dev环境明明很好,怎么上线就出问题。原因很简单:应用运行依赖操作系统版本、系统库、运行时版本、环境变量……任何一环不一致,行为就可能不同。

虚拟机(VM)曾经是标准答案,但 VM 需要模拟完整硬件、运行独立内核,一台机器开几个 VM 资源就吃紧,启动也要几十秒。

Docker 换了个思路:不虚拟化硬件,只隔离进程。容器和宿主机共享同一个内核,靠 Linux 内核的几项能力实现"看起来像独立系统"的效果:

  • Namespaces(命名空间):隔离进程看到的世界——PID、网络、挂载点、主机名等,每个容器都以为自己独占了整台机器。
  • Cgroups(控制组):限制并统计资源使用,比如给容器限制 CPU 和内存上限,防止一个容器把整台宿主机拖垮。
  • UnionFS(联合文件系统):把多层只读文件系统叠加成一个统一视图,这正是镜像分层机制的基础。

三者结合,容器的启动速度是毫秒到秒级,资源开销远低于 VM,这也是 Docker 能在过去十年里成为事实标准的根本原因。

docker-architecture.svg

二、Docker 的核心架构

Docker 采用 C/S(客户端-服务端)架构:

  • Docker Client:你敲的 docker build、docker run 等命令,本质是向 Daemon 发 REST API 请求。
  • Docker Daemon(dockerd):真正干活的后台服务,负责镜像构建、容器生命周期、网络和存储管理。它内部又拆成了 containerd(容器生命周期管理,CNCF 毕业项目)和 runc(真正调用 Linux 内核创建 namespace/cgroup 的 OCI 标准运行时)。
  • Registry:镜像仓库,Docker Hub 是默认的公共仓库,企业通常会搭建私有 Registry(如 Harbor)。

这套架构的好处是解耦:你可以在本地 Client 上操作远程服务器上的 Daemon(比如 CI/CD 流水线里常见的 DOCKER_HOST 配置),镜像可以在任意一台机器构建、推送,再在另一台机器上拉取运行。

三、镜像与容器:分层是关键

镜像(Image) 是一个只读模板,容器(Container) 是镜像的一个运行实例。这句话说烂了,但真正重要的是"分层"这件事。

docker-layers.svg

Dockerfile 里的每一条指令(FROM、RUN、COPY 等)都会生成一个新的只读层,层与层之间通过 UnionFS 叠加成完整的文件系统视图。容器启动时,Docker 会在最上面加一层可写层,容器内所有的文件修改都只发生在这一层,删除容器时可写层随之销毁,底层镜像不受影响。

这个机制带来两个直接的工程价值:

  1. 构建缓存:如果某一层的内容和上一次构建时相同,Docker 会直接复用缓存层,不重新执行。这也是为什么 Dockerfile 里"依赖安装"要写在"代码拷贝"之前——依赖变化频率低,放前面能最大化利用缓存。
  2. 镜像共享:多个容器基于同一个镜像启动时,共享底层的只读层,只有可写层是各自独立的,极大节省磁盘和内存。

四、写一个像样的 Dockerfile

以一个 Node.js 服务为例,一个新手写法和一个经过优化的写法对比:

❌ 常见的低效写法:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

问题:任何代码改动都会导致 COPY . . 之后的所有层缓存失效,npm install 每次都要重新跑;而且用的是完整版 node 镜像,体积臃肿;没有区分构建依赖和生产依赖;用 root 用户运行也不安全。

✅ 优化后的多阶段构建:

# ---- 构建阶段 ----
FROM node:20-alpine AS builder
WORKDIR /app

# 先只拷贝依赖描述文件,最大化利用缓存
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ---- 运行阶段 ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

这里用到几个关键实践:

  • 多阶段构建(multi-stage build):构建阶段装满 devDependencies、跑打包工具,最终运行阶段只拷贝构建产物,成品镜像可以小一个数量级。
  • Alpine 基础镜像:体积通常只有几 MB 到几十 MB,比默认的 Debian 系镜像小很多(但要注意 Alpine 用的是 musl libc,个别 native 依赖可能有兼容问题)。
  • 依赖文件先行拷贝:COPY package*.json 在 COPY . . 之前,只要依赖没变,npm ci 这层就能复用缓存。
  • 非 root 用户运行:避免容器内进程以 root 权限运行,是基本的安全实践。
  • .dockerignore:别忘了配置,排除 node_modules、.git、.env 等,既加快构建又避免敏感文件被打进镜像。

五、docker-compose:多容器编排的日常工具

真实项目很少只有一个容器——服务本身、数据库、缓存、反向代理,往往要一起启动、一起联调。docker-compose.yaml 就是描述"这一组容器该怎么协作"的配置文件。

一个典型的全栈应用示例:

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - app-net
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-net

  cache:
    image: redis:7-alpine
    networks:
      - app-net

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    networks:
      - app-net

networks:
  app-net:
    driver: bridge

volumes:
  db-data:

几个容易踩坑、也是理解 Compose 的关键点:

  • depends_on 不等于"等服务真正就绪":默认只保证容器按顺序启动,不保证里面的服务(比如 Postgres)已经能接受连接。要用 condition: service_healthy 配合 healthcheck,才能真正等到数据库就绪再启动应用,否则应用启动瞬间连库会报错。
  • 服务名即 DNS 名:Compose 自动创建的网络里,db、cache 这些 service 名可以直接当主机名用(如上面的 DATABASE_URL 里的 db:5432),不需要关心容器实际 IP。
  • volumes 用来持久化数据:容器本身是"一次性"的,数据库数据必须挂载到具名卷或宿主机目录,否则容器一删数据全丢。
  • .env 文件:Compose 会自动读取同目录下的 .env 文件填充变量,敏感信息(密码、密钥)不要硬编码在 yaml 里,用 ${DB_PASSWORD} 引用环境变量。
  • restart: unless-stopped:生产环境建议配置重启策略,容器异常退出后自动拉起,除非是人为手动停止的。

常用命令:

docker compose up -d          # 后台启动全部服务
docker compose logs -f app    # 跟踪某个服务的日志
docker compose exec app sh    # 进入运行中的容器
docker compose down -v        # 停止并删除容器、网络(-v 连数据卷一起删,谨慎使用)

六、部署到生产环境要注意什么

本地 docker compose up 跑通只是第一步,真正上线还有几件事必须考虑:

1. 镜像要打版本 tag,别用 latest。 生产环境用 latest 意味着你没法确定线上跑的到底是哪个版本的代码,回滚也无从谈起。用 Git commit hash 或语义化版本号打 tag,比如 myapp:v1.4.2 或 myapp:a1b2c3d。

2. 资源限制必须显式配置。 不加限制的容器可能因为内存泄漏拖垮整台宿主机。Compose 里可以这样写:

    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

3. 日志不要无限增长。 Docker 默认的 json-file 日志驱动不会自动轮转,长期运行的容器日志文件可能撑爆磁盘。配置日志轮转:

    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

4. 健康检查(healthcheck)是编排系统判断存活的依据。 无论是 Compose、Kubernetes 还是云厂商的容器服务,健康检查决定了流量能不能被正确路由到这个实例、故障实例能不能被及时替换。

5. 敏感信息不要打进镜像。 数据库密码、API Key 等要通过环境变量、Secret 管理(如 Docker Secrets、Kubernetes Secrets、Vault)注入,绝不能写进 Dockerfile 或提交到镜像仓库。

6. 单机 Compose vs 集群编排。 docker compose 适合单机部署、开发环境、小型项目。当你需要多机调度、自动扩缩容、滚动更新、服务发现,就需要 Kubernetes 或 Docker Swarm 这类编排系统了——但理解 Compose 里的这些概念(Service、Network、Volume)之后,再学 K8s 的对应概念会顺畅很多,它们本质上是同一套思路的不同实现规模。

七、几个容易被忽略但很重要的知识点

  • 容器不是虚拟机,重启会丢数据:除非显式挂载 volume,容器内产生的文件在容器删除后都会消失,这是新手最容易踩的坑。
  • docker exec vs docker attach:exec 在容器里新开一个进程(比如进入 shell 调试),退出不影响容器主进程;attach 是接到容器主进程的标准输入输出上,如果主进程被你 Ctrl+C 中断,容器可能直接退出。日常调试优先用 exec。
  • 镜像体积优化:除了多阶段构建,善用 .dockerignore、合并 RUN 指令减少层数(如 RUN apt-get update && apt-get install -y xxx && rm -rf /var/lib/apt/lists/*)、选择 slim/alpine 基础镜像,都能明显缩小体积、加快分发。
  • 网络模式的区别:bridge(默认,容器间通过虚拟网桥通信)、host(容器直接共用宿主机网络栈,性能好但隔离性差)、none(完全无网络),生产环境里选择哪种要结合安全与性能需求权衡。
  • 容器编号和镜像 ID 的区别:docker ps 看到的是容器实例,docker images 看到的是镜像本身,同一个镜像可以启动出任意多个容器实例,互不影响。

Docker 的核心思想其实很简洁:把"环境一致性"这个老大难问题,通过内核级的隔离机制和分层文件系统解决掉。理解了 Namespace、Cgroup、UnionFS 这三件事,再回头看 Dockerfile 和 Compose 的种种最佳实践,会发现它们都是在为这套底层机制服务,而不是一堆孤立的"记住就好"的规则。

评论 (0)

暂无评论,快来抢沙发吧!

发表评论

目录
  • 一、Docker 到底解决了什么问题
  • 二、Docker 的核心架构
  • 三、镜像与容器:分层是关键
  • 四、写一个像样的 Dockerfile
  • 五、docker-compose:多容器编排的日常工具
  • 六、部署到生产环境要注意什么
  • 七、几个容易被忽略但很重要的知识点