Prerequisite
很久之前写过 Docker 的博客,但那时也只是随意了解一下,现在打算重新学习(但我依然没有实际场景需要用到,因此以后可能还会再写一次)
参考文章:廖雪峰【一文读懂Docker原理】
参考文章:Install Docker Engine on Ubuntu
参考文章:浅析 Dockerfile 中 RUN、CMD 以及 ENTRYPOINT 指令的异同
参考文章:Docker 常用命令
参考视频:Docker技术从0到1全覆盖(对应笔记)
参考报错:failed to solve with frontend dockerfile.v0: failed to create LLB definition
使用镜像源:清华大学开源软件镜像站
Docker 原理
Docker 的容器是一种对进程进行隔离的运行环境(进程是 Linux 操作系统执行任务的最小单元)
Docker 本质就是帮助我们设置好隔离环境后,启动容器中不同隔离的进程(Linux 系统本身负责隔离)
隔离的方法有三种:
- 第一种就是进程之间看不到彼此,这是由 Linux 的 Cgroup 机制实现的。进程隔离的结果就是以隔离方式启动的进程看到的自身进程 ID 总是 1,且看不到系统的其他进程。
- 第二种就是隔离系统真实的文件系统。Docker 利用 Linux 的 mount 机制,给每个隔离进程挂载了一个虚拟的文件系统,使得一个隔离进程只能访问这个虚拟的文件系统,无法看到系统真实的文件系统。至于这个虚拟的文件系统应该长什么样,这就是制作 Docker 镜像要考虑的问题。比如我们的 Python 程序要正常运行,需要一个 Python3 解释器,需要把用到的第三方库如 psutil 引入进来,这些复杂的工作被简化为一个 Dockerfile,再由 Docker 把这些运行时的依赖打包,就形成了 Docker 镜像。我们可以把一个 Docker 镜像看作一个 zip 包,每启动一个进程,Docker 都会自动解压 zip 包,把它变成一个虚拟的文件系统。
- 第三种就是网络协议栈的隔离(以下面要运行 ZooKeeper 和 Kafka 举例)
# 将 zookeeper 进程的端口号映射到宿主机,从而在宿主机上访问 zookeeper
# 但这只是 zookeeper 进程的端口映射,如果需要映射多个进程,需要利用 Docker Compose
docker run -p 2181:2181 zookeeper:latest
因为 ZooKeeper 和 Kafka 都有监听的端口,但 Linux 可以为进程隔离网络,Docker 默认启动的 ZooKeeper 和 Kafka 进程拥有自己的网络名字空间,与宿主机不同。利用 Docker Compose,把 ZooKeeper 和 Kafka 运行在同一个网络名字空间里,并通过 zookeeper:2181 来访问 ZooKeeper 端口,让 Docker 自动把 zookeeper 名字解析为动态分配的 IP 地址
Docker Linux 下载
$ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Docker 用法
基础命令
# 登入 Docker Hub(用户名为:courserli)
docker login -u courserli
# 从 Docker Hub 下载镜像(名字为:courserli 的 hello-docker)
docker pull courserli/hello-docker
# 分享镜像到 Docker Hub(名字为:courserli 的 hello-docker,默认 tag 为:latest)
docker push courserli/hello-docker
# 清除缓存(会停止全部容器的运行)
docker system prune
镜像命令
# 查看全部 docker 镜像
docker image ls
# 删除 docker 镜像
docker image rm 镜像名称
# 构建本地镜像(由当前目录下的 Dockerfile 文件构建,构建镜像名为 tuanzi/test)
docker build . -t tuanzi/test
进程命令
# 启动 docker 容器(-it 是交互式操作终端,sagemath/sagemath 是镜像名,--rm 表示在容器退出时自动清理容器内部的文件系统)
docker run -it --rm sagemath/sagemath
# 启动 docker 容器(-d 后台运行,-p 端口映射,前者为容器内部端口,后者为映射本地端口,如果镜像名在本地找不到,会自动联网 pull)
docker run -d -p 80:80 docker/getting-started
# 启动 docker 容器(-v 是挂载路径,后接挂载本地路径和容器路径)
docker run -it --rm -v 路径:/share/data tuanzi/test
# 在运行的 docker 容器中执行命令(ls 就是执行的命令)
docker exec -it nginx ls
# 进入正在运行的 docker 容器(/bin/bash 命令相当于弹出 shell 来,可以看作是进入了)
docker exec -it nginx /bin/bash
# 查看正在运行的 docker 容器进程
docker ps
# 查看已经停止的 docker 容器进程
docker ps -a
# 停止 docker 容器进程(参数是 CONTAINER ID)
docker rm -f 042c94072180
Docker file
这里以一个实际的例子回顾,这是我的本地的目录
- test
- db
- data
- Dockerfile
- run.sh
- db
执行命令的路径与 Dockerfile 同级
- Dockerfile 文件:
FROM ubuntu:16.04
COPY run.sh /run.sh
VOLUME /share/data
RUN useradd nss
RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list && \
sed -i s@/security.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN ["apt-get", "update", "-y"]
WORKDIR /home/nss
RUN chmod +x /run.sh
ENV FLAG=NSSCTF{test_flag}
EXPOSE 80/tcp
CMD [ "helloworld" ]
USER nss
ENTRYPOINT [ "/run.sh" ]
- run.sh 文件:
#! /bin/bash
echo "Run with user `whoami`"
echo "Run with directory `pwd`"
echo "Run with environment $FLAG"
echo "Run with parameter $1"
tail -f /dev/null
read v
while [ "$v" == "n" ];
do
cat /share/data/data
read v
done;
关于内容的具体细节可以看这篇文章,我来特别说一下:
- 如何挂载?
一般构建的命令是 docker build . -t tuanzi/test
,然后运行是 docker run -it --rm tuanzi/test
;正如 Dockerfile 文件中的 VOLUME /share/data
所描述,我需要在命令行中添加挂载路径,即 docker run -it --rm -v 路径:/share/data tuanzi/test
,此时容器内 data 文件的内容会随着本地 data 文件而改变
- 如何直接进入容器中(而不是终端交互)?
去掉 Dockerfile 文件中的 RUN、CMD、ENTRYPOINT
的命令,或者执行 docker run -it --rm tuanzi/test /bin/sh
命令,用于覆盖掉 Dockerfile 文件中的 ENTRYPOINT
针对 RUN、CMD 和 ENTRYPOINT,大佬给出了总结:
- RUN、CMD 和 ENTRYPOINT 指令都可以用来执行具体的命令
- RUN 指令是在 Docker 镜像构建时发挥作用, 可以使用多个该命令, 且执行结果会记录到镜像中
- CMD 和 ENTYPOINT 指令是在容器启动时自动执行, 均只有最后一个该指令有效, 且均可以在 docker run 中被覆盖
- ENTRYPOINT 指令和 CMD 的区别在于使用 ENTRYPOINT 时 CMD 指令会被作为其默认参数, 而用户也可以在启动容器时通过覆盖 CMD 指令来输入参数; 此外, 这也意味着 ENTRYPOINT 指令的内容不易被用户命令覆盖
Docker Compose
Docker Compose 的意义在于结合多个 Docker 容器,比如搭建一个网站(Linux + Nginx + Mysql + php)
这里再以一个实际的例子回顾,这是我的本地的目录
- test
- mqsql
- app.sql
- Dockerfile
- nginx
- default.conf
- Dockerfile
- php1
- src
- index.php
- Dockerfile
- src
- php2
- src
- Dockerfile
- docker-compose.yml
- mqsql
执行命令的路径与 docker-compose 同级
- docker-compose 文件:
version: '3'
# Linux + Nginx + Mysql + PHP
services:
nginx:
build: ./nginx
container_name: "nginx"
ports:
- 8080:80
restart: always
cap_add:
- SYS_ADMIN
depends_on:
- php1
- php2
networks:
default:
my_net:
ipv4_address: 172.2.0.3
php1:
build: ./php1
container_name: nssctf
php2:
build: ./php2
container_name: xenny
mysql:
build: ./mysql
container_name: mysql
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=app
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --sql-mode='' --max-execution-time=1000
networks:
my_net:
driver: bridge
internal: true
ipam:
config:
- subnet: 172.2.0.0/16
关于内容的具体细节依然可以看这篇文章,我就不赘述了