Docker 学习笔记

参考:

Practice

保持容器运行的小技巧

使用 tail 跟踪 /dev/null 文件:

docker run -d ubuntu bash -c "tail -f /dev/null"

参考:Persisting data | Docker Docs

image

管理

docker search hello-world  # 搜索 image

docker pull hello-world
# docker image pull library/hello-word
docker image ls  # 列出所有 image
docker images

docker image rm <img>  # 删除 image
docker rmi <img>

image 可以在同一 CPU 架构的任何操作系统上运行

构建

docker build -t <name> .  # . 表示 Dockerfile 所在的目录
# docker image build -t <name>:<tag> .

docker history <image>  # 查看镜像中创建镜像层的命令

-t: tag,相当于一个指向镜像的指针

每一个 image 都是由一系列的 layer 组成的。Dockerfile 中的每条命令都会创建一个新的 layer

docker history <image> 命令可以查看 image 中用于创建每个 layer 的命令。在命令的输出中,最底层的是 base image,最顶层的是 top layer。通过这个命令也可以看到每个 layer 的大小,帮助我们诊断大体积的 image

Dockerfile

FROM <image>   # 基础镜像
RUN <command>  # 在基础镜像上运行命令
COPY <src> <dest>  # 将本地文件复制到镜像中
EXPOSE <port>  # 暴露端口
CMD <command>  # 容器启动时运行的命令

每次 RUN 的执行都是在一个新的镜像层 layer 上进行的,所以,如果你的镜像中有多个 RUN 命令,那么每个 RUN 都会创建一个新的镜像层,这样会导致镜像体积变大。

常用基础镜像:ubuntu, alpine, node:14, python, golang, openjdk:11-jdk-slim, nginx:1.21.0-alpine

发布

# 给 image 打 tag (创建一个新的 tag 并将该 image id 与 tag 关联)
docker tag old:0.0.1 username/new:0.0.1
# docker image tag old:0.0.1 username/new:0.0.1

# 发布
docker push username/new:0.0.1
# docker image push username/new:0.0.1

container

管理

docker ps -a # 列出所有 container,包括终止运行的
# docker container ls -a # 同上

docker rm <container>  # <container> 可以是 tag 或者 id
# docker container rm <container>

常用命令:

docker rm -f <container>  # 强制删除一个容器(正在运行的容器也可以删除)

运行

docker run hello-world  # 若 image 不存在则自动执行 docker pull 命令
# docker container run hello-world

其他运行选项:

docker run -it ubuntu  # -i: interactive -t: 分配一个伪 TTY
docker run -d ubuntu  # -d: 后台运行
docker run -p 80:80 nginx  # -p: 端口映射,将主机的 80 端口映射到容器的 80 端口
docker run -v /path/to/host:/path/to/container nginx  # -v: 挂载卷
docker run --name <name> nginx  # --name: 指定容器名
docker run --rm nginx  # --rm: 运行结束后自动删除容器

常用示例:

  • -p 127.0.0.2:80:80: 将主机的 127.0.0.2:80 映射到容器的 80 端口
  • -v "$(pwd)":/app: 将当前目录挂载到容器的 /app 目录

停止

docker stop <container>  # 停止运行
docker kill <container>  # 强制停止

其他

docker start <container>             # 启动已经停止的 container

docker exec -it <container> bash     # 进入运行中的 container 的 shell

docker logs -f <container>           # 查看 container 的日志,-f: 实时查看

docker cp <container>:<path> <path>  # 从 container 中复制文件到主机

docker inspect <container>           # 查看 container 的详细信息

Advanced Topics

卷挂载

当你需要某个持久的地方来存储程序的数据时,卷挂载是一个很好的选择。(数据库)

# 创建一个 volume
docker volume create todo-db

# 挂载 volume
docker run -dp 3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started
docker volume ls
docker volume inspect todo-db  # 查看 volume 详细信息

Persist the DB | Docker Docs

绑定挂载

绑定挂载对于开发环境来说是一个很好的选择。

docker run -it --mount type=bind,src="$(pwd)",target=/src ubuntu bash

也可以使用 -v--volume 选项(不推荐):

docker run -it -v "$(pwd)":/src ubuntu bash

参考:Use bind mounts | Docker Docs

在开发容器中运行 app

docker run \
    -dp 3000:3000 \
    -w /app \
    --mount 'type=bind,src="$(pwd)",target=/app' \
    node:18-alpine \
    sh -c "yarn install && yarn run dev"

-w: Working Directory

相当于直接使用一个安装了 Node.js 的机器运行这个 node 项目。

当你完成对项目的修改,这时再使用 build 命令构造出最终的镜像:

docker build -t getting-started .

参考:Run your app in a development container | Docker Docs

了解更多高级存储概念,查阅 Manage data in Docker | Docker Docs

Docker Compose

docker network create todo-app  # 创建一个网络

# 启动 mysql
docker run -d \
     --network todo-app --network-alias mysql \  # 连接到网络并指定网络别名(在网络中的主机名)
     -v todo-mysql-data:/var/lib/mysql \         # 卷挂载,自动创建名为 todo-mysql-data 的 volume
     -e MYSQL_ROOT_PASSWORD=secret \             # 设置环境变量,ROOT 密码
     -e MYSQL_DATABASE=todos \                   # 设置环境变量,数据库名
     mysql:8.0

# 启动网络调试工具 netshoot
docker run -it --network todo-app nicolaka/netshoot
$ dig mysql

# 启动开发就绪容器
docker run -dp 3000:3000 \
   -w /app -v "$(pwd):/app" \  # 绑定挂载
   --network todo-app \        # 连接到网络
   -e MYSQL_HOST=mysql \       # 设置环境变量,数据库 IP 或主机名
   -e MYSQL_USER=root \        # 设置环境变量,数据库用户名
   -e MYSQL_PASSWORD=secret \  # 设置环境变量,数据库密码
   -e MYSQL_DB=todos \         # 设置环境变量,数据库名
   node:18-alpine \
   sh -c "yarn install && yarn run dev"  # 启动命令
⚠️ 安全提示 在生产中不建议通过环境变量来配置连接设置。在大多数情况下,secrets 被以文件的形式加载到运行的容器中,比如说 MySQL 容器支持通过以 `_FILE` 为后缀的环境变量来指向含有环境变量值的文件,如 `MYSQL_PASSWORD_FILE` 会让 app 使用文件中的值作为连接密码。

Why you shouldn't use ENV variables for secret data - Diogo Mónica

参考:Use Docker Compose | Docker Docs

compose.yaml

services:
  app:
    image: node:18-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app  # Docker Compose 中可以直接使用相对路径
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:  # 定义的服务名会自动成为其网络别名
    image: mysql:8.0
    # docker run 的 -v 选项会自动创建不存在的 volume,
    # 但是在 compose 中需要先在顶层 volumes section
    # 中声明 volume,然后再在 service config 中指定
    # 挂载点
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:  # volumes 节, 声明卷
  todo-mysql-data:  # key: value 形式,如果使用默认 volume 引擎的话不用给出 value
  • 相对路径以 compose 文件所在的目录为基
  • 相对路径必须以 ... 开头
  • Docker Compose 会自动为你的应用程序栈创建一临时网络,因此你不用在 compose 文件中定义网络

运行应用程序栈:

docker compose up -d        # 启动,-d 选项表示后台运行
docker compose down         # 停止

docker compose logs -f      # 查看日志,-f 选项表示实时输出
docker compose logs -f app  # 只查看 app 服务的日志

在运行 docker compose down 命令时,默认不会删除 compose 文件中的命名 volume,如果想要删除它们,可以加上 --volumes 选项。

构建多平台镜像

docker build --platform linux/amd64 -t undefined443/course:amd64 .
docker build --platform linux/arm64 -t undefined443/course:arm64 .

参考:Multi-platform builds | Docker Docs

# 查看现有的 builder
docker buildx ls

# 使用 docker-container 驱动创建一个 builder
# docker-container 提供了多平台 build 支持
docker buildx create --name mybuilder --driver docker-container --bootstrap

# 切换到刚刚创建的 builder
docker buildx use mybuilder

# 构建多平台镜像
docker buildx build --platform 'linux/amd64,linux/arm64' -t '<username>/<image>:latest' --push .

# 检视镜像
docker buildx imagetools inspect <username>/<image>:latest

# 使用 SHA256 摘要指定一个镜像变体
docker run --rm <username>/<image>@sha256:2b77acdfea5dc5baa489ffab2a0b4a387666d1d526490e31845eb64e3e73ed20 uname -m

构建镜像的最佳实践

参见:Image-building best practices | Docker Docs

Layer Caching

当镜像层发生改变时,所有下游镜像层都要重新构建。

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

如果我们这样写 Dockerfile,那么每次我们修改源代码并重新构建镜像时都要重新安装依赖,因为源代码的改变会导致 COPY . . 层发生改变,从而导致其下的所有层都要重新构建。

为了能够缓存依赖,我们需要重新组织 Dockerfile 的结构:

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package.json yarn.lock ./  # 先将安装依赖所需的文件拷贝进来
RUN yarn install --production   # 安装依赖
COPY . .                        # 最后再将其他内容拷贝进来
CMD ["node", "src/index.js"]

在 Dockerfile 所在的目录下创建 .dockerignore 文件,并填入以下内容:

node_modules

参考:.dockerignore file | Docker Docs

在第二个 COPY 命令执行时应该忽略 node_modules 文件夹,否则它会覆盖 RUN 命令所产生的文件。

这样,只要依赖没有发生改变,那么在重新构建时就不需要重新安装依赖,从而减少 buildpushpull、以及更新镜像所需的时间。

多阶段构建

使用多阶段构建,可以帮助我们:

  • build-time 依赖与 runtime 依赖分离
  • 通过只传送 app 需要运行的内容来减小整个 image 的体积

Maven/Tomcat 示例:

# syntax=docker/dockerfile:1
# build 阶段,使用 Maven 构建 Java 项目
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

# runtime 阶段,使用 Tomcat 运行 Java 项目
FROM tomcat
# 将 build 阶段构建的文件拷贝进来
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps

最终的镜像只包含最后创建的阶段(可以使用 --target 标记覆盖)

只构建需要的镜像
DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .

通过 --target 选项指定要构建的阶段。

通过设置 DOCKER_BUILDKIT=1,可以只构建 target 阶段以及 target 依赖的阶段。

在 GitHub 发布 Docker 镜像

  1. 在 GitHub 上创建一个 Personal access token (classic),需要选中 write:packagesdelete:packages 权限。

  2. 在 Docker CLI 中登录到 GitHub Container Registry

    export CR_PAT=<your_token>
    echo $CR_PAT | docker login ghcr.io -u <username> --password-stdin
    

    <your_token><username> 替换为你自己的 GitHub Token 和 GitHub 用户名

  3. 标记镜像并推送到 GitHub Container Registry(ghcr.io)

    docker tag <image>:<tag> ghcr.io/<username>/<image>:<tag>
    docker push ghcr.io/<username>/<image>:<tag>
    

参考:使用容器注册表 | GitHub Docs

将 Docker 镜像连接到 GitHub 仓库

在 GitHub 的 Profile 中,选择 Packages,找到刚刚推送的镜像,点击 Connect to a repository

或者,编辑镜像的 Dockerfile,加入以下内容:

LABEL org.opencontainers.image.source https://github.com/<username>/<repo>

<repo> 替换为你要推送的库

Troubleshooting

alpine apk 换源

临时

apk add --repository http://mirrors.aliyun.com/alpine/v3.14/main/ --allow-untrusted --no-cache bash

永久

sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

Dockerfile 中有些命令写在一个 RUN 命令中会导致奇怪问题发生。

在 Alpine 中使用 Python 安装依赖时,在安装 cffi 时报错:

fatal error: Python.h: No such file or directory

解决方法:

RUN apk add --no-cache --update --virtual .build-deps \
    --repository http://mirrors.aliyun.com/alpine/v3.14/main/ \
    python3-dev \
    py3-pip \
    make \
    g++ \
    gcc \
    libc-dev \
    linux-headers \
    libffi-dev \
    openssl-dev

python:alpine 安装了 python3-dev 后,就可以正常安装 cffi 了。

参考:fatal error: Python.h: No such file or directory | Stack Overflow

What is .build-deps for apk add --virtual command | Stack Overflow

将 build 镜像中的 .venv 拷贝到 runtime 后找不到 .venv/bin/python

原因是 build 镜像中 .venv/bin/python -> /bin/local/python,而 runtime 镜像的 Python 在 /usr/bin/python3.10

需要在构建虚拟环境时指定 Python 安装位置:

RUN PIPENV_VENV_IN_PROJECT=1 pipenv --python /usr/bin/python3.10 && pipenv install

A perfect way to Dockerize your Pipenv Python application

Windows 目录挂载问题

如果要将 Windows 目录挂载到 Linux 容器中,需要开启目录共享功能。

Shared Drives | Docker Docs

进入设置:Resources > File sharing,添加要共享的目录。

如果没有该设置项,检查 General 下的 Use the WSL 2 based engine,确保取消勾选。

image

host 网络模式

巨坑!host 网络模式只支持 Linux。不支持 Windows 和 macOS。

Networking using the host network: The host networking driver only works on Linux hosts, and is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.

image

posted @ 2024-06-06 02:23  Undefined443  阅读(23)  评论(0编辑  收藏  举报