Skip to content

01.Docker 指南

NOTE

此文档为较早版本,有些地方可能不是非常明确,待修改。

Docker 核心三件组成

Dockerfile

Dockerfile 用于构建image,是构建image的蓝图。

Dockerfile 指令

Dockerfile 的指令按照在构建镜像和容器运行,分为两类。

即,有些指令只在构建镜像时才会被执行,在容器运行时不会执行;反之亦然。

构建镜像时执行
FROM

作用

指定基础镜像,是所有 Dockerfile 必须的第一条指令。后续的指令都将在此基础上执行。

示例

dockerfile
FROM python:3.13-slim

这表示使用官方的 Python 3.13 轻量级镜像作为基础。

RUN

区分:CMD和ENTRYPOINT

作用:

执行指令,并将结果保存到镜像中。常用于安装软件包、配置环境等。

每个 RUN 指令执行时会创建一个新的镜像层(Layer),并在该层上执行指令。新层不是上一层的完整副本,而是基于上层的增量更改,并记录本次 RUN 操作带来的变化。这种设计可以避免镜像不断变大,但不断创建新层仍会带来一定的额外空间开销。

执行 RUN 后的文件系统更改会被永久保存在镜像中。在容器运行时,Docker 会把这些层组合成一个联合文件系统(例如 OverlayFS),展现为一个完整的文件系统,供容器使用。从镜像结构来看,层依然是分开的,不会被“解散”。

示例

dockerfile
RUN apt-get update && apt-get install -y vim

这条指令会在镜像中安装 Vim 编辑器。

COPY

区分:ADD

**作用:**将文件或目录从构建上下文(本地文件系统)复制到镜像中的指定位置。

示例

dockerfile
COPY . /app

将当前目录(宿主机目录)下的所有文件复制到镜像的 /app 目录。

ADD

区分:COPY

**作用:**类似于 COPY,但具有额外功能,如自动解压归档文件或从 URL 下载文件。

示例

dockerfile
ADD https://example.com/file.tar.gz /app/

这条指令会下载指定的 tar.gz 文件并解压到镜像的 /app/ 目录。

相较于 COPYCOPY 仅复制本地文件和目录,行为更可预测,推荐优先使用 COPY,除非需要 ADD 的额外功能。

WORKDIR

作用:

设置后续指令的工作目录。如果目录不存在,Docker 会自动创建。

示例

dockerfile
WORKDIR /app

设置 /app 为当前工作目录。

LABEL

(使用案例待补充)

作用:为镜像添加元数据(如作者信息、版本等),通常用于描述镜像。

示例

dockerfile
LABEL maintainer="yourname@example.com"

设置镜像的维护者信息。

ARG

对比ENV

**作用:**定义构建时的变量。

示例

dockerfile
ARG APP_VERSION=1.0
RUN echo "Version: $APP_VERSION"

在构建时可以通过 --build-arg 参数覆盖 APP_VERSION

容器运行时执行
CMD

区分:RUNENTRYPOINT

作用:指定容器启动时的默认命令。如果 docker run 时指定了其他命令,则 CMD 会被覆盖。

示例

dockerfile
CMD ["python", "app.py"]

默认在容器启动时运行 python app.py

NOTE

CMD 指令有两种主要的格式:

Exec 格式(推荐)

dockerfile
CMD ["executable", "param1", "param2"]

docker 会将每一个元素看作一个整体执行,如使用CMD ["flask run"]无法达到预期效果,因为docker会将"flask run"看作一个可执行文件。可能会报以下错误:

cmd
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "flask run": executable file not found in $PATH: unknown.

正确的使用方法应该是:

dockerfile
CMD ["flask", "run"]

或者后文的 Shell 格式:

dockerfile
CMD flask run

Shell 格式

dockerfile
CMD executable param1 param2

这种格式会通过 /bin/sh -c 来执行命令,因此可以使用 shell 特性。

ENTRYPOINT

区分:RUNCMD

作用:为容器设置一个固定的入口点,指定不可更改的命令,容器启动时总是执行该命令,运行容器时指定参数无法覆盖该指令。可以与 CMD 结合使用,可以传递默认参数。

示例

dockerfile
ENTRYPOINT ["python"]
CMD ["app.py"]

容器启动时将执行 python app.pyENTRYPOINT 设置了 python 作为固定命令,CMD 设置了默认参数 app.py

对比CMDCMD 设置的是默认参数,容易被 docker run 时的命令覆盖。

EXPOSE

作用:声明容器将要监听的网络端口。它是一个文档化的功能,不会实际映射端口。

示例

dockerfile
EXPOSE 5000

声明容器内部的 5000 端口用于监听。

VOLUME

作用:创建一个挂载点,允许在容器和宿主机之间共享数据。用于持久化数据或在多个容器之间共享数据。

示例

dockerfile
VOLUME ["/data"]

声明 /data 为一个挂载点。容器内部的/data目录将会和docker指定一个默认的宿主机上的目录共享,当下一次容器启动时,仍可加载/data目录中的文件(除非在宿主机上删除),当容器被删除时,宿主机上的目录及其中的文件会保留,不会因为容器删除而自动清除,可以手动在宿主机上删除该目录或其中的文件。

USER

作用:指定运行容器时的用户,默认是 root

示例

dockerfile
USER appuser

指定以 appuser 用户身份运行容器。

镜像构建和容器运行时都执行
ENV

对比ARG

设置环境变量,这些变量在构建时和容器运行时都可用。

示例:

dockerfile
# 使用官方的 Python 3.13 轻量级镜像作为基础镜像
FROM python:3.13-slim

# 设置环境变量
ENV APP_HOME=/app

# 使用环境变量设置工作目录
WORKDIR $APP_HOME

# 复制当前目录内容到容器中的工作目录
COPY . $APP_HOME

# 设置另一个环境变量
ENV PYTHONUNBUFFERED=1

# 创建虚拟环境并安装依赖
RUN python3 -m venv venv && \
    ./venv/bin/pip install --no-cache-dir -r requirements.txt
    
# 暴露5000端口
EXPOSE 5000

# 设置容器启动时执行的命令
CMD ["./venv/bin/python", "app.py"]

ENV APP_HOME=/app 设置了一个名为 APP_HOME 的环境变量。

WORKDIR $APP_HOME 使用了前面设置的 APP_HOME 环境变量,设置工作目录为 /app

ENV PYTHONUNBUFFERED=1 设置了 PYTHONUNBUFFERED 环境变量,以确保 Python 输出被实时刷新。

在构建镜像时这些环境变量可以被dockerfile后续的指令识别,在使用镜像运行容器时,容器内的应用程序,如flask,也可以获得该环境变量,如通过 os.environ.get('APP_HOME') 获取 APP_HOME 的值。

image

image是创建container的镜像,container从image中创建。

container

container是容器,容器内部执行相关操作。

docker 高级功能

运行指定版本的容器

使用 docker run ubuntu 默认运行一个版本为 latestubuntu 镜像的容器,如果想要指定 ubuntu 镜像的版本:

  1. 去 docker hub 上查看你需要镜像的版本号
  2. docker run ubuntu 基础上显式指定版本号,使用冒号开始:如 docker run ubuntu:17.10

在后台运行 docker 容器

docker 默认使用前台运行的方式运行容器,如使用 docker run ubuntu sleep 200 将在前台显式的执行 sleep 200 的指令,在此期间控制台将“无法返回”,直到 200 秒结束。(您仍然可以通过新建一个控制台连接到 docker,并进一步停止该容器的执行)

使用 -d 参数指定以后台运行容器: docker run -d ubuntu sleep 200

使用 -d 使容器在后台执行后,使用 attach 重新链接到该容器,使其回到前台:

  1. 使用 docker ps 查看当前运行的容器的 hash
  2. 使用 docker attach <hash> 链接到该容器

查看容器的详细信息

使用 docker inspect <hash> 查看容器的更多详细信息。

链接端口号

将 docker 容器的端口号和宿主机端口号建立映射,使用 -p 参数: docker run -p 5000:8080 jenkins

其中,第一个 5000 是宿主机的端口号,第二个 8080 是 docker 容器的端口号。

建立端口映射后,宿主机流入 5000 的流量都将被路由到 docker 8080 端口,从而能够访问到容器内部。

建立映射卷

使容器内部数据持久化。

默认情况下,当容器停止时,容器内部所生成的所有数据都将被销毁,下次启动该容器时,容器内部的文件将不存在。为了持久化产生的数据,使得容器下一次重新运行时还能读取这些文件,需要和宿主机建立文件映射,将容器产生的文件实际上保存到宿主机上而不是容器内。

使用 -v 参数建立容器和宿主机上的文件映射: docker run -p 5000:8080 -v your/root/host/path:docker/container/path jenkins ,这样,容器内产生的文件在容器内部显示保存到了 docker/container/path ,但实际上是保存到了宿主机上的 your/root/host/path 目录中。

下一次再次运行该容器时,如果仍运行 docker run -p 5000:8080 -v your/root/host/path:docker/container/path jenkins ,那么数据将会显示为保存的效果,因为容器会从 docker/container/path 下寻找文件,且可以找到实际上存储在宿主机上 your/root/host/path 的文件。

案例

在 docker 中运行 flask 应用

步骤

  1. 创建虚拟环境,建立flask应用

    详细的环境配置和代码在后文,首先关注 docker 和 flask 应用。

    目录树如下(未列出虚拟环境目录):

    cmd
    DOCKER-FLASK
    |-----------
    |-- .dockerignore
    |-- .flaskenv
    |-- app.py
    |-- Dockerfile
    |-- requirements.txt
    |-- templates
    |-- |-- login.html
    • .flaskenv中指定FLASK_RUN_HOST=0.0.0.0FLASK_RUN_PORT=5000
  2. 编写dockerfile文件

    Dockerfile:

    dockerfile
    FROM python:3.13-slim
    
    WORKDIR /app
    
    COPY . /app
    
    RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
    
    EXPOSE 5000
    
    CMD ["flask", "run"]

    .dockerignore:

    dockerfile
    .docker-flask_venv/  # 虚拟环境目录
    __pycache__/
  3. 构建image:

    在cmd中运行:

    cmd
    docker build -t docker-flask-app .
  4. 启动容器

    在cmd中运行:

    dockerfile
    docker run -p 8888:5000 docker-flask-app
  5. 在宿主机浏览器中访问http://localhost:8888/

    效果如下:

    image-20241110170848250

环境配置和代码

环境配置

python版本:

采用python3.12版本,使用conda创建虚拟环境。

代码

app.py

python
from flask import Flask, render_template, request, redirect, url_for, session
import os

app = Flask(__name__)
app.secret_key = os.urandom(24)

# Sample user data
dummy_user = {"username": "user", "password": "password"}


@app.route("/")
def home():
    if "username" in session:
        return f"<h1>Welcome {session['username']}! You are logged in.</h1><br><a href='/logout'>Logout</a>"
    return redirect(url_for("login"))


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]

        if username == dummy_user["username"] and password == dummy_user["password"]:
            session["username"] = username
            return redirect(url_for("home"))
        else:
            return "<h1>Invalid credentials. Please try again.</h1>"
    return render_template("login.html")


@app.route("/logout")
def logout():
    session.pop("username", None)
    return redirect(url_for("login"))

requirements.txt

blinker==1.8.2
click==8.1.7
colorama==0.4.6
Flask==3.0.3
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
python-dotenv==1.0.1
Werkzeug==3.1.2

.flaskenv

FLASK_APP=app.py
FLASK_ENV=development
FLASK_RUN_HOST=0.0.0.0
FLASK_RUN_PORT=5000

login.html

html
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>

<body>
    <h2>Login Page</h2>
    <form method="POST">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required><br><br>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required><br><br>
        <input type="submit" value="Login">
    </form>
</body>

</html>

指令

删除镜像

强制删除

shell
podman rmi --force <image_hash>

Docker 指令附录

记录 Docker 基本指令。

Dockerfile 相关

根据 Dockerfile 构建镜像

设当前目录下有一个 Dockerfile:

bash
docker build .

-t 参数用于指定构建镜像的名称和标签

bash
docker build . -t "custom-name-image:latest"

这样,使用 docker images 检查的 REPOSITORYTAG 分别是 custom-name-imagelatest ,如果不指定标签,如 docker build . -t "custom-name-image" ,则默认 TAGlatest

如果不使用 -t 参数,则构建的镜像名称为一个 ID 值(哈希值),之后需要通过这个 ID 值操作镜像。

容器

启动容器

bash
docker run demo-image

这将 demo-image 的镜像运行为一个容器。

-p 参数

-p 参数指定映射容器和宿主机的端口,格式:<宿主机端口>:<容器端口> (注意参数的位置,在 run 之后,镜像名称之前)

bash
docker run -p 5000:80 demo-image

这将将宿主机上流入 5000 端口的流量全部路由到容器内的 80 端口,从而访问容器内的 80 端口的内容。

--name 参数

指定容器运行时的名称:

bash
docker run --name=redis1 redis

表示运行 redis 镜像为容器,其中容器的名称为 redis1

-d 参数

指定后台运行容器:

bash
docker run -d redis1

此时运行名称为 redis1 的容器,将在后台执行。

启动一个停止的容器

设一个停止的容器名称为 redis1

bash
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS                      PORTS     NAMES
2ceb686a9a83   redis     "docker-entrypoint.s…"   11 minutes ago   Exited (0) 11 minutes ago             redis1

启动这个容器:

bash
docker start redis1

同样可以指定后台运行,使用 -d 参数:

bash
docker start -d redis1

删除容器

删除所有容器(无论是否正在运行):

bash
docker rm -af
  • rm :删除容器
  • -a (--all) :删除所有容器(包括正在运行和已停止的)
  • -f (--force) :强制删除,即使容器正在运行

容器间交互

使用 –-link 参数使得一个容器可以通过容器的名称访问另一个容器:

bash
docker run --link redis:redis voting-app

其中,第一个 redis 表示之前已经启动好的一个容器的名称,第二个 redis 表示在这个新启动的容器中,要访问之前的 reids 容器,使用的别名,这里,新的容器内部可以通过使用 redis 来访问之前启动好的容器 redis 。如果修改为 –link redis:redis1 ,则现在可以通过 redis1 访问之前启动好的容器 redis

限制 CPU 和内存使用

默认情况下,分配给容器的宿主机 CPU 和内存没有限制,可以在启动容器时显式指定最大允许的 CPU 或内存,使用 --cpus--memory 选项:

bash
docker run --cpus=.5 ubuntu

表示最大允许使用宿主机 CPU 的 50% 。

bash
docker run --memory=100m ubuntu

表示最大允许使用宿主机内存的 100MB 。