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 详细信息
绑定挂载
绑定挂载对于开发环境来说是一个很好的选择。
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
命令所产生的文件。
这样,只要依赖没有发生改变,那么在重新构建时就不需要重新安装依赖,从而减少 build
、push
、pull
、以及更新镜像所需的时间。
多阶段构建
使用多阶段构建,可以帮助我们:
- 将
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 镜像
-
在 GitHub 上创建一个 Personal access token (classic),需要选中
write:packages
和delete:packages
权限。 -
在 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 用户名 -
标记镜像并推送到 GitHub Container Registry(ghcr.io)
docker tag <image>:<tag> ghcr.io/<username>/<image>:<tag> docker push ghcr.io/<username>/<image>:<tag>
将 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 容器中,需要开启目录共享功能。
进入设置:Resources > File sharing,添加要共享的目录。
如果没有该设置项,检查 General 下的 Use the WSL 2 based engine
,确保取消勾选。
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.