文章目录
- image
- 获取
- Dockerfile
- 基础镜像
- RUN
- 文件复制
- 变量和环境变量
- CMD
- Demo
- 缓存
- .dockerignore
- 多阶段构建
- 非root
- 存储
- Volume
- Bind Mount
- 共享
image
- 镜像的创建和管理
获取
- 镜像的获取有三种方式
- 从公有或私有的仓库拉取
- 用Dockerfile自己build
- 从文件导入(离线)
- commit
- 公有仓库,最常用的有两个
- Docker Hub
- 和红帽推出的Quay,注册一下
- 镜像命令
docker pull nginx:1.20.0 # 拉取特定版本 docker pull quay.io/bitnami/nginx # bitnami是所属组 docker image ls docker image inspect imageID # 查看镜像的详细信息,可以看到OS layer等 # 这块命令都要带上image,不带默认是container,不过建议使用docker container ... docker image rm # 没有容器使用才可
- 离线获取镜像
- 找个有镜像的机器,让它给你打包一份,命令:
docker image save nginx:latest -o nginx.image
,名称+tag也可以定位一个image - 然后发送给你,再load:
docker image load -i ./nginx.image
- 找个有镜像的机器,让它给你打包一份,命令:
- 通过container的commit功能获取
- 如果我们在container中修改了某些功能,希望保留下来,存成新的镜像以后使用(Create a new image from a container’s changes)
- 进入容器,修改内容(例如NGINX的index),再使用:
docker commit nginx_d1 roykun/nginx:1.0
,相当于创建新的分支,保存成镜像,原镜像不变(不是container不变) - 关于容器内没有vim等命令
# 进入容器 apt-get update apt-get upgrade apt-get install vim # 或者使用echo直接写入文件 echo "<h1>Here is NGINX</h1>" > index.html more index.html
- 通过Dockerfile构建
- 这个比较复杂了单独说
Dockerfile
- 是一个文本文件,存的是指令,用来构建image,有特定的语法规则
- 看个例子:
FROM ubuntu:21.04 # 基础镜像(Ubuntu环境) RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y python3.9 python3-pip python3.9-dev # 安装python环境 ADD hello.py / # 要执行的文件 CMD ["python3", "/hello.py"] # 执行文件
- 使用
docker image build -t hello .
构建,生成一个名为hello:latest的镜像
- 可以用
-t
指定具体的镜像名称,方便发布到dockerhub自己的私有仓库 - 例如我的注册名称是roykun,可以命名为:
roykun/hello:1.0
- 因为注册名是唯一的,所以能直接定位你的repo推过去?并不是
- 需要在本地login:
- 也可以复制并重命名镜像,使用:
docker image tag hello roykun/hello:1.0
- 它们的ID相同,所以rm只能通过名称和tag啦,推到仓库:
docker image push roykun/hello:1.0
- 推上去后:
docker pull roykun/hello
是不行的,tag默认是latest
- 可以用
- 接下来详细讲讲语法和相关技巧
基础镜像
- 纠正一下上一篇的概念,image也有可能包含基础的操作系统,例如可以在centos宿主机上选择Ubuntu基础镜像(视具体需求而定)
- 当然还是依赖宿主机的进程,但容器内又包含了完整的底层操作系统和上层应用(天内天)
- 基础镜像的选择
- 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
- 固定版本tag而不是每次都使用latest
- 尽量选择体积小的镜像
- 一般run容器的时候会带
-t
参数表示进入终端,如果我们使用了基础镜像(例如Ubuntu),会指定进入bash- 如果不使用,需要定义CMD进入bash,或者参数中动态定义CMD
- 基础镜像不一定是操作系统镜像,FROM指定的都是
RUN
- 做image时执行命令,安装软件下载文件等
FROM ubuntu:21.04 RUN apt-get update RUN apt-get install -y wget RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz RUN tar zxf ipinfo_2.0.1_linux_amd64.tar.gz RUN mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo RUN rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
- 每个
RUN
产生一个layer(层),一个RUN可以包含多条命令# 可以发现,体积会小一些 FROM ubuntu:21.04 RUN apt-get update && \ apt-get install -y wget && \ wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \ tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \ mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \ rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
- 可以使用
docker image history ID
查看layer,实际上一个docker命令就会产生一个layer,只不过有些不占大小,例如WORKDIR
文件复制
- 可以使用
COPY
和ADD
命令复制文件到镜像,但ADD会对文件自动解压缩FROM python:3.9.5-alpine3.13 WORKDIR /app/ ADD hello.tar.gz /app/
- 常和
WORKDIR
命令结合使用,相当于cd
,而且还会自动创建不存在的目录
变量和环境变量
- 既然是语法,怎么可能没有变量?
ARG
和ENV
都可以设置变量FROM ubuntu:21.04 ENV VERSION=2.0.1 # ARG VERSION=2.0.1 RUN apt-get update && \ apt-get install -y wget && \ wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \ tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \ mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \ rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
- 区别是作用域不同
- ENV可以理解成设置在image中的环境变量
ARG
就不行了,一个临时的传参而已
- 使用ARG还可以在build是可以动态指定,覆盖Dockerfile里的变量
- 命令:
docker image build -f .\Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .
- 命令:
CMD
CMD
设置容器启动时的命令,如果定义了多个CMD,只有最后一个会被执行- 如果docker container run创建容器时指定了其它命令,则CMD命令会被忽略
docker run -idt ipinfo:latest ipinfo
,最后这个ipinfo是CMD命令,直接覆盖掉Dockerfile里的- 插一句:
docker image prune -a
删除目前没在使用的镜像 docker container prune -a
删除退出(stop)的容器
- 一般创建容器后会进入shell,是因为在Ubuntu的基础镜像里有定义CMD
- 使用history可以看到:
/bin/sh -c #(nop) CMD ["/bin/bash"]
- 使用history可以看到:
- 如果写成
CMD []
,会覆盖掉基础镜像里的CMD,这会报错 ENTRYPOINT
常和CMD一起使用,区别是它所设置的命令是一定会被执行的# Dockerfile-entrypoint FROM ubuntu:21.04 ENTRYPOINT ["echo", "hello docker"]
docker image build -f Dockerfile-entrypoint -t demo-entrypoint .
docker run -it --rm demo-entrypoint echo "hello fuck"
,rm表示容器退出(Exited)后自动删除- 创建容器会发现既输出docker又fuck
FROM ubuntu:21.04 ENTRYPOINT ["echo"] # run时传入的内容会原样输出 CMD [] # 虽然为空但不报错
- Shell和Exec
- 一般CMD都是用Exec格式的,就是中括号这个:
CMD ["echo", "hello docker"]
- 也可以使用Shell格式:
ENTRYPOINT echo "hello docker"
- 因为我们要执行的不一定都是Shell命令,所以推荐Exec
# 区别是有的,还是在执行shell命令时,如果传参 FROM ubuntu:21.04 ENV NAME=docker CMD echo "hello $NAME" CMD ["sh", "-c", "echo hello $NAME"] # 必须加sh -c,相当于换成shell格式
- 一般CMD都是用Exec格式的,就是中括号这个:
Demo
- flask大家应该都接触过(我用过),做个运行flask demo的image
- 要运行的文件
from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, World!'
- Dockerfile
FROM python:3.9.5-slim COPY app.py /src/app.py # 这个必须精确到文件名,不然就会把src当做文件名而不是目录 RUN pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple/ # 工作目录!所有工作的出发点 WORKDIR /src # 切换到src下,进入容器后,包括后面的环境变量,都是从这下面开始 ENV FLASK_APP=app.py # 这个是环境变量,Linux中可以用export设置,是flask run要寻找的执行文件 EXPOSE 5000 # 后面学,暴露端口 CMD ["flask", "run", "-h", "0.0.0.0"]
- 执行命令:
docker image build -t flask-demo .
,docker run -idt --name flask_dc -p 5000:5000 flask-demo
,docker exec -it flask_dc /bin/sh
- 在浏览器查看即可
缓存
- build镜像的时候,如果之前biu过,会有cache,下次biu就很快
- 但如果我们在
cache
前的部分修改代码,例如上面把app.py改了改,这会让下面缓存的部分全部失效
- 那怎么整呢?把可能经常修改的这部分内容放到后面去,尽量使用cache
FROM python:3.9.5-slim RUN pip install flask WORKDIR /src ENV FLASK_APP=app.py COPY app.py /src/app.py # 放到需要cache的后面 EXPOSE 5000 CMD ["flask", "run", "-h", "0.0.0.0"]
.dockerignore
- 看看前面的截图,build镜像时有个提示:Sending build context to docker daemon xxxKB
- 最后那个
.
代表将当前目录的所有文件发到image里作为context - 很多是不需要的呀卧槽,类似git的ignore
# 就在要做context的那个目录新建 .dockerignore文件 ./vscode env/ # 啥不想拿进去,就写啥
- 当然,也会影响COPY
多阶段构建
- 例如我们要编译C代码得到二进制可执行文件,需要GCC镜像
// hello.c #include <stdio.h> void main(int argc, char *argv[]) { printf("hello %s\n", argv[argc - 1]); }
- build:
docker build -t hello .
# Dockerfile FROM gcc:9.4 # 这个东西很大,一个多G COPY hello.c /src/hello.c WORKDIR /src RUN gcc --static -o hello hello.c ENTRYPOINT [ "/src/hello" ] CMD [] Successfully tagged hello:latest $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE hello latest 7cfa0cbe4e2a 2 hours ago 1.14GB gcc 9.4 be1d0d9ce039 9 days ago 1.14GB
- hello.c编译完以后,并不需要这样一个大的GCC环境,拿了二进制文件就跑
# Dockerfile-new FROM gcc:9.4 AS builder # 起个别名,这个阶段就是得到hello,其他不需要 COPY hello.c /src/hello.c WORKDIR /src RUN gcc --static -o hello hello.c FROM alpine:3.13.5 COPY --from=builder /src/hello /src/hello # 把二进制文件拿过来 ENTRYPOINT [ "/src/hello" ] CMD []
docker build -t hello-apline -f Dockerfile-new .
- 创建container试试 ,艺谋一样,这种情况就叫多阶段构建
非root
- docker安装完毕之后,默认需要root用户的权限才能够启动和使用,这其实不好
- 虽然用户可以sudo,但和root用户还是有区别的,用的是自己的ENV
- 不安全主要是创建容器时使用
-v
参数导致,可能会对宿主机误操作,但目录映射一般无法避免 - 如果容器内直接进root,对映射进去目录的操作权限就也是root
- 但是它自动创建了
docker
用户组,可以将当前用户添加到这个组解决此问题# 如果当前用户不能sudo,需要到root编辑/etc/sudoers chmod u+w /etc/sudoers sudo groupadd docker # 可以看看到底有没 'docker' already exists # gpasswd 从组中添加/删除用户,-a:添加用户到组;-d:从组删除用户; sudo gpasswd -a roy docker # roy是你当前使用的用户,可以查看 /etc/group sudo systemctl restart docker su # 再切回来 su roy # 设置docker容器开机启动 systemctl list-unit-files |grep enable # 查看enable的服务 systemctl enable docker.service docker run xxx --restart=always # 设置xxx容器开机启动
- 这种方法可以在宿主机非root操作docker,但是创建的容器还是直接到了root用户
- 可以在build时为镜像创建用户/用户组,使用
USER
切换过去,这样创建的container也就是非root了FROM python:3.9.5-slim RUN pip install flask && \ # -r 创建系统工作组 # -r 建立系统帐号 -g 指定群组 groupadd -r flask && useradd -r -g flask flask && \ mkdir /src && \ chown -R flask:flask /src # 设置/src的拥有者 USER flask # 切换到flask用户 COPY app.py /src/app.py WORKDIR /src ENV FLASK_APP=app.py EXPOSE 5000 CMD ["flask", "run", "-h", "0.0.0.0"]
- 这里创建的用户和用户组只会在容器中体现,我们也不能使用宿主机中的用户!例如
roy:docker
,只能挂载宿主机目录进去
- 这里创建的用户和用户组只会在容器中体现,我们也不能使用宿主机中的用户!例如
存储
- 默认情况下,在运行中的容器里创建的文件,被保存在一个可写的容器层
- 如果没有目录映射,创建的文件在容器删除后也会消失
- 可写的容器层是和特定的容器绑定的,不方便的和其它容器共享
- 这就需要数据持久化,一般有两种方式
- Data Volume, 由Docker管理,(/var/lib/docker/volumes/ Linux), 持久化数据的最好方式
- Bind Mount,由用户指定存储的数据具体mount在系统什么位置
Volume
- Dockerfile
FROM alpine:latest RUN apk update RUN apk --no-cache add curl ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.1.12/supercronic-linux-amd64 \ SUPERCRONIC=supercronic-linux-amd64 \ SUPERCRONIC_SHA1SUM=048b95b48b708983effb2e5c935a1ef8483d9e3e RUN curl -fsSLO "$SUPERCRONIC_URL" \ && echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - \ && chmod +x "$SUPERCRONIC" \ && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \ && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic COPY cron-date /app/my-cron WORKDIR /app VOLUME ["/app"] # 注意看这里,定义一个持久化数据的目录 # RUN cron job CMD ["/usr/local/bin/supercronic", "/app/my-cron"]
- 使用
VOLUME
持久化数据
- 使用
- cron-date,一个定时执行的任务
*/1 * * * * date >> /app/test.txt
- 构建:
docker image build -t cron-date .
,启动:docker run -d my-cron
- 此时在/app下已经开始执行定时任务了,会看到输出文件
docker volume ls
可以看到创建的本地卷,docker volume inspect ID
,查看详细信息
- 还可以build时不指定VOLUME,而是创建容器时使用
-v
参数动态指定docker run -d -v cron-data:/app cron-date
,这里指定了宿主机中volume名称为cron-data(只能指定名称),但文件还在/var,:/app
代表对容器中这app下面的data做持久化- 这就的-v,直接指定了Volume名字,和下面的Bind Mount不一样,卷的位置是系统指定的
- 可以使用
docker volume prune -f
删除所有本地卷,慎用
Bind Mount
- 这里的
-v
参数就是我们常用的目录映射了(最常用),将volume可以放在宿主机的指定位置,所以Windows用户也能查看了 - 用之前的Dockerfile整一个:
docker run -d --name bind-mount -v /home/roy/tmps:/app my-cron
,也可以用${pwd}
- 此时在指定目录就能看到持久化的文件了,这个操作的本质是挂载,所以用不着
docker volume
- 使用mount还可以帮我们搭建开发环境(借鸡生蛋)
- 例如我们有一个gcc的镜像,如果要编译一个C文件,可以:
docker run -d -v ${pwd}:/root gcc:9.4
- 直接将我们要编译的文件挂载到gcc容器的root目录,进去编译!直接能映射出来
- 类似的,在vscode中有个插件:
Remote-Container
,可以借助远程的container搭建需要的开发环境,自行探索!
- 例如我们有一个gcc的镜像,如果要编译一个C文件,可以:
- 这两种方式有啥区别呢?
- Volume可以远程,一般是备份吧,不在本地写
- Mount就是里外来回造~,适合本地(宿主机)
共享
-
容器之间共享数据,首先要创建个Volume放在公共机器上,大家都用这个卷存数据,就行
-
这里使用手动创建Volume的方式,需要安装Volume的driver
- 准备两台机器试一下,一台装上driver(roy01),另一台放公共Volume做备份(roy)
- 在roy01,驱动用
docker plugin
装,驱动:sshfs
,可以用其他的(默认driver是local
)
docker plugin install --grant-all-permissions vieux/sshfs # 这个192.168.109.3就是roy,放volume docker volume create --driver vieux/sshfs \ -o sshcmd=roy@192.168.109.3:/home/roy\ -o password=123456 \ sshvolume # 创建容器并使用这个volume docker run -idt -v sshvolume:/app busybox sh
-
在roy01的容器写东西,/home/roy 本地就能看到,OK