Dockerfile 和 Docker Compose 手把手教学

看完还不会写,回来找我。

一、Dockerfile — 给程序"打包"的配方

Dockerfile 本质上就是一个文本文件,告诉 Docker 怎么把你的程序装进一个干净的"微型电脑"。这台微型电脑里有你指定的操作系统、运行环境、你的代码,只干一件事。

核心指令速查表

指令作用类比
FROM指定基础镜像买一台预装了系统的电脑
WORKDIR设置工作目录cd 到某个文件夹
COPY从宿主机复制文件进镜像插 U 盘拷文件
RUN在镜像构建时执行命令装软件
CMD容器启动时默认执行的命令开机自启动程序
EXPOSE声明容器监听端口在墙上开个洞
ENV设置环境变量配全局参数

一个最简示例

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ota_server.py .
CMD ["python", "ota_server.py"]

一步步解释:

FROM python:3.11-slim      # 基于已装了 Python 3.11 的精简 Linux
WORKDIR /app                # 进到 /app 目录
COPY requirements.txt .     # 把本地的 requirements.txt 拷进来
RUN pip install -r requirements.txt   # 装依赖
COPY ota_server.py .        # 把主程序拷进来
CMD ["python", "ota_server.py"]      # 启动时跑这个命令

几个关键细节

1. COPY 为什么要分两次?

# ✅ 正确:先拷依赖文件,再装依赖,最后拷代码
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ota_server.py .

# ❌ 不好:先全拷进去再装,改一行代码就要重装依赖
COPY . .
RUN pip install -r requirements.txt

Docker 是分层缓存的。requirements.txt 没变 → 依赖层用缓存,秒级构建。改了代码 → 只重新执行 COPY ota_server.py 这一层。

2. RUN 和 CMD 的区别

RUN pip install flask     # 构建时执行,结果写进镜像
CMD ["python", "app.py"]  # 运行时执行,每次启动容器才跑
  • RUN:做一次就固化的事(装软件、创建目录)
  • CMD:容器每次启动都要跑的事(启动程序)

3. FROM 怎么选?

镜像大小适用
python:3.11~1GB不推荐,太大
python:3.11-slim~150MB推荐,日常够用
python:3.11-alpine~50MB极致精简,但某些 C 扩展可能踩坑

一般选 slim 最稳。


二、docker-compose.yml — 一键编排多个容器

单独用 Dockerfile 构建镜像后,还要手动 docker run 带一堆参数启动,容易忘记。

docker-compose.yml 就是把这些参数写进一个文件,一条命令(或一个按钮)搞定构建 + 启动

最简单的 compose 文件

services:
  ota-server:
    build: .
    ports:
      - "8092:8092"
    volumes:
      - /opt/ota_data:/app/firmware
    restart: unless-stopped

拆解:

services:                  # 定义一组"服务"(一个服务 = 一个容器)
  ota-server:              # 服务名,随便起
    build: .               # 从当前目录的 Dockerfile 构建镜像
    ports:                 # 端口映射
      - "8092:8092"        #   宿主机端口:容器内端口
    volumes:               # 数据卷挂载
      - /opt/ota_data:/app/firmware   #   宿主机路径:容器内路径
    restart: unless-stopped  # 重启策略:除非手动停,否则一直自动重启

多服务示例

一个 Wordpress 站,需要 WordPress + MySQL 两个容器:

services:
  db:
    image: mysql:8.0             # 不需要 build,直接用现成镜像
    environment:                  # 环境变量
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: wordpress
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:latest
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db       # 这里写服务名 db,compose 自动解析
      WORDPRESS_DB_USER: root
      WORDPRESS_DB_PASSWORD: 123456
    depends_on:                   # 依赖:先启动 db,再启动 wordpress
      - db

volumes:                          # 声明具名数据卷
  db_data:

要点:

  • build: . → 自己构建
  • image: xxx → 用 Docker Hub 上的现成镜像
  • 服务间用服务名通信,比如 WORDPRESS_DB_HOST: db,compose 自动做 DNS

常用配置项速查

services:
  myapp:
    build: .                    # 构建目录
    image: myapp:v1             # 给构建出来的镜像打标签
    container_name: myapp       # 固定容器名(不写则自动生成)
    ports:
      - "8080:80"               # 宿主机:容器
    volumes:
      - ./data:/app/data        # 相对路径(相对于 compose 文件)
      - /absolute/path:/data    # 绝对路径
    environment:                # 环境变量
      - DEBUG=true
      - PORT=8080
    restart: always             # 挂了就重启(always / unless-stopped / on-failure)
    depends_on:                 # 启动顺序
      - db

build 和 image 的区别

# 自己构建
services:
  app:
    build: .                    # 找当前目录的 Dockerfile

# 拉现成的
services:
  nginx:
    image: nginx:1.25           # 从 Docker Hub 拉

# 两个都写 → 构建后打标签
services:
  app:
    build: .
    image: myapp:latest         # 构建的镜像命名为 myapp:latest
:如果两个都写,Docker 有时会先去 Hub 上找 myapp:latest,找不到才构建。纯自建项目建议只写 build: .,不写 image

三、实战:从零写出 OTA 服务器的两个文件

回到 OTA 项目,这两个文件是怎么一步步写出来的:

Dockerfile

我需要什么?
1. Python 环境          → FROM python:3.11-slim
2. 进工作目录            → WORKDIR /app
3. 装上 flask, werkzeug → COPY requirements.txt + RUN pip install
4. 拷入代码              → COPY ota_server.py
5. 固件数据目录          → RUN mkdir -p /app/firmware
6. 暴露端口              → EXPOSE 8092
7. 启动命令              → CMD ["python", "ota_server.py"]

写出来就是:

FROM python:3.11-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY ota_server.py .
RUN mkdir -p /app/firmware

EXPOSE 8092
ENV OTA_HOST=0.0.0.0
ENV OTA_PORT=8092

CMD ["python", "ota_server.py"]

docker-compose.yml

我需要什么?
1. 从当前目录构建          → build: .
2. 映射端口 8092           → ports: "8092:8092"
3. 固件数据持久化到宿主机   → volumes: /opt/ota_data:/app/firmware
4. 服务器重启自动拉起来     → restart: unless-stopped

写出来就是:

services:
  ota-server:
    build: .
    container_name: ota-server
    restart: unless-stopped
    ports:
      - "8092:8092"
    volumes:
      - /opt/ota_data:/app/firmware

四、常见坑

1. build 和 image 一起写导致尝试 pull

# ❌ 可能先去 Docker Hub 找 esp32-ota:latest
services:
  app:
    build: .
    image: esp32-ota:latest

修法:自建镜像不写 image:

2. version 已经废弃

# ❌ Docker Compose v2 会报警告
version: "3.8"

修法:删掉 version 那一行,现在 compose 默认就是 v2 行为。

3. 忘记映射端口

# ❌ 外面访问不到
services:
  app:
    build: .

修法ports: - "宿主机端口:容器内端口"

4. COPY . . 导致每改一行就重装依赖

分层顺序错了,依赖缓存白费。按上一节的顺序来:先拷 requirements.txt,再 pip install,最后拷代码。


总结:记住这个模板就行

Dockerfile 模板:

FROM 语言:版本-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt    # 或 npm install / go build 等
COPY . .
CMD ["启动命令"]

docker-compose.yml 模板:

services:
  服务名:
    build: .
    ports:
      - "端口:端口"
    volumes:
      - 宿主机路径:容器内路径
    restart: unless-stopped

改改参数,90% 的项目就这么写。


🤖 声明:本文由 AI 辅助生成,经过人工校验与修改。

作者:Wangbeihong / 2026年6月