Docker学习笔记

Docker / Docker Swarm · 2022-09-01 · 3021 人浏览
Docker学习笔记

Docker

第一章 Docker的安装和介绍

容器技术的介绍

  • 容器引领基础架构

    • 90年代 PC
    • 00年代 虚拟化
    • 10年代 cloud
    • 11年代 container
  • 容器是一种快速打包技术

    • 标准化
    • 轻量级
    • 易移植
Linux container容器技术的诞生(2008年)解决了IT世界里“集装箱运输”的问题。Linux container(LXC)是一种内核轻量级的操作系统层虚拟化技术。LXC主要由namespaceCgroup两大机制来保证实现。
  • namespace资源隔离
  • Cgroup资源管理,如CPU/MEM的限制

容器的快速发展与普及

  • 2020年,全球>50%公司在生产环境中使用container

    ——Analysis By Gartner

容器的标准化

  • docker != container
  • 2015年,Google、Docker、Redhat等厂商联合发起OCI((Open Container Initiative)组织,致力于容器的标准化。

容器关乎速度

  • 软件开发
  • 编译构建
  • 测试
  • 部署
  • 更新
  • 故障恢复

Linux上安装Docker

sudo systemctl start docker
sudo docker version

出现client、server的信息即为安装成功。

第二章 容器快速上手

认识docker命令行

  • 非root账户下需要在docker命令前加sudo
docker version
docker info
docker container ps
docker container ps -a
docker image ls
docker image rm nginx

docker的基本操作

  • 新旧格式命令均兼容
  • 建议学习新格式命令
新格式命令旧格式命令
docker container run nginxdocker run nginx
docker container stop nginxdocker stop 1a6fc
docker container lsdocker ps
docker container ls -adocker ps -a
docker container rm 1a6fcdocker rm 1a6fc

命令小技巧之批量操作

例如存在多个container时:

docker container af 87 c2 4c

当要操作的容器变多时,上面的命令不再方便,可以如下操作:

docker container ps -aq

# 批量停止
docker container stop $(docker container ps -aq)
# 批量删除
docker container rm $(docker container ps -aq)
  • 不能删除一个正在运行的container

container的attached和detached模式

  • attached模式(前台运行模式)
docker container run -p 80:80 nginx

执行的结果直接输出到命令行里,按ctrl+c即可中断。Windows系统上的attached模式并非完整attached模式。

  • detached模式(后台运行模式)
    docker run --detached ...简写为docker run -d ...
docker run -d -p 80:80 nginx
  • detach进入attach模式

docker attach b2

容器的交互模式

docker container run -d -p 80:80 nginx
docker container ps
# 查看log
docker container logs d02
# 动态查看log
docker container -f logs d02
  • 交互式运行模式(-it)
docker container run -it ubuntu sh
  • detached模式容器进入交互式模式
# 执行shell
docker exec -it dc3 sh
  • 使用busybox镜像熟悉交互模式
docker container run -it busybox sh

Windows是如何运行docker engine的

  • hyper-v manage

    • Windows系统使用基于hyper-v的虚拟机
  • WSL2 based engine

    • 也可以使用WSL2替代hyper-v
    • 在Windows10中安装WSL2(Ubuntu),在WSL2安装docker

image-20220413143516967

容器和虚拟机的区别

docker container run -it创建一个容器并进入交互式模式

docker container run -it busybox sh

docker container exec -it 在一个已经运行的容器里执行一个额外的command

docker container run -d nginx
docker container exec -it 33d sh

容器和虚拟机 Container vs VM

image-20220413150200904

容器不是Mini虚拟机

  • 容器其实是进程containers are just processes
  • 容器中的进程被限制了对CPU内存等资源的访问
  • 当进程停止后,容器就退出了

实验

docker container run -d nginx
docker container ps
# 查看容器内的进程
docker container top fcf

Note

可以使用pstree命令h直观的看出进程之间的创建附属关系。在Ubuntu20.04中,pstree命令需额外安装,可以使用yum install psmisc或者sudo apt-get install psmisc安装。

image-20220413151953413

docker container run背后发生了什么

docker container run -d publish 80:80 --name webhost nginx

  1. 在本地查找是否有nginx这个image镜像,如果没发现,会执行2、3步
  2. 去远程的image register查找nginx镜像(默认的register是Docker Hub)
  3. 下载最新版本的nginx镜像(nginx:latest默认)
  4. 基于nginx镜像来创建一个新的容器,并准备运行
  5. docker engine分配给这个容器一个虚拟IP地址
  6. 在宿主机上打开80端口并把容器的80端口转发到宿主机的80端口上
  7. 启动容器,运行指定命令(这里是一个shell脚本去启动nginx)

第三章 镜像的创建管理和发布

image-20220414092147771

镜像的获取

  • pull from registry(online)从registry拉取

    • public(公有)
    • private(私有)
  • build from Dockerfile(online)从Dockerfile构建
  • load from file(offline)文件导入(离线)

镜像的registry

  • Docker Hub
  • Quay.io(Redhat)

image-20220413160719822

image-20220413160919344

image-20220413160752883

镜像的获取查看和删除

docker iamge

image-20220413161623686

# 拉取官方nginx(默认最新)
docker pull nginx
# 查看镜像列表
docker image ls
# 拉取指定版本的nginx
docker image pull nginx:1.20.0
# 查看镜像列表
docker image ls

image-20220413164127477

# 从quay.io拉取镜像
docker image pull quay.io/bitnami/nginx
docker image ls

image-20220413164634076

# 获取镜像的信息,参数是镜像的ID
docker image inspect f0b8a9a54136
  • Architecture架构,如amd64

image-20220413165057556

  • Layers分层,即镜像分层,每一层对应的哈希值

image-20220413165144429

# 删除镜像,参数为镜像ID
docker image rm 092
如果要删除的镜像,有一些正在使用的容器,将无法删除。先要把对应container删掉,才能删除镜像。

镜像的导入导出

image-20220413165641141

docker image save nginx:1.20.0 -o nginx.image

image-20220413165748369

# 导入镜像(离线)
docker image load -i ./nginx.image

Dockerfile介绍

  • Dockerfile是用于构建docker镜像的文件
  • Dockerfile里包含了构建镜像所需的“指令”
  • Dockerfile有其特定的语法规则

举例:执行一个python程序

容器即进程,所以镜像就是一个运行这个进程所需要的环境。

加入我们要在一台Ubuntu20.04上运行hello.py的python程序

hello.py的文件内容

print("hello docker")

第一步,准备python环境

apt update && \
apt install software-properties-common && \
add-apt-repository ppa:deadsnakes/ppa && \
apt install python3.9

第二步,运行hello.py

$ python3 hello.py
hello docker

<hr/>

一个Dockerfile的基本结构

  • FROM
  • RUN
  • ADD
  • CMD

Dockerfile

FROM ubuntu:20.04
RUN apt update && \
    apt install software-properties-common && \
    add-apt-repository ppa:deadsnakes/ppa && \
    apt install python3.9
# 将当前目录下的hello.py添加到容器内的根目录下
ADD hello.py /
CMD ["python3", "/hello.py"]

镜像的构架和分享

  • hello.py
  • Dockerfile

image-20220414085715360

image-20220414085742939

  • hello.py与Dockerfile在同一个目录里

image-20220414090021520

# 如果不加版本号,默认为latest
docker image build -t hello ./
# 带版本号
docker image build -t hello:1.0 ./
  • 之前做过一次构建,二次构建会很快

image-20220414090303482

  • 清除本地构建的镜像缓存,再构建

image-20220414090513396

image-20220414090643809

docker run -it hello

image-20220414090727028

  • 执行之后,进程就停下来了,container也就停止了
  • 把镜像push到Docker Hub上,这样任何人都可以拉取
  • 如果push到Docker Hub上,镜像名称需要遵循ID/NAME:version的命名规范,比如
docker image build -t billaday/hello:1.0 ./
  • 但是如果已经构建出来了,仅仅是名称不符合规范,这是根据已经存在的image进行操作
# 给image打一个新标签billaday/hello:1.0
docker image tag hello billaday/hello:1.0
  • 推送镜像到Docker Hub上
# 先登录,输入账户密码,出现Login Succeeded即登陆成功
docker login
Username: billaday
Password:
Login Succeeded
docker image push billaday/hello:1.0
  • 验证推送的镜像
docker pull billaday/hello:1.0
docker image ls
docker run -it billaday/hello:1.0

通过commit创建镜像

image-20220414092147771

docker container run -d -p 80:80 nginx
docker container ls
# 进入刚刚创建的nginx容器的交互式模式
docker container exec -it 07e sh
# 修改index.html文件
cd /usr/share/nginx/html
echo "<h1>hello docker</h1>" > index.html
# 提交修改生成新镜像
docker container commit 07e billaday/nginx
# 查看新的image
docker image ls
创建一台Ubuntu21.04的机器,并安装python3.9环境,运行一个hello.py程序
docker container run -it ubuntu:21.04 sh
# 进入容器shell,使用ppa安装python3.9
apt update && \
    apt install software-properties-common && \
    add-apt-repository ppa:deadsnakes/ppa && \
    apt install python3.9
ls
echo "print('hello docker')"
python3 hello.py
# 退出容器
exit
# 查看容器列表
docker container ls -a
docker container commit 4a2 billaday/python-demo
# 运行新创建的镜像,会直接打印出hello docker
docker container run -it billaday/python-demo python3 /hello.py

scratch镜像

  • scratch是一个空的镜像,可以基于他创建一个自己的镜像
  • 先将下面的hello.cpp编译成二进制文件hello
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("hello\n");
    return 0;
}
  • 编译时一定要加上--static参数
# 使用gcc
gcc --satic -o hello hello.cpp
# 或g++进行编译
g++ --satic -o hello hello.cpp
  • 再在当前目录下新建并编写Dockerfile文件
FROM scratch
ADD hello /
CMD ["/hello"]
  • 构建docker image
docker build -t hello ./
  • 查看docker image
docker image ls

image-20220415084256891

  • 运行基于hello镜像的容器
docker container run -it hello
  • 通过docker image history命令查看分层
docker iamge history hello
  • 可以看到二进制文件hello的大小与容器的大小相同,均为872KB

image-20220415084216343

image-20220415084140728

第四章 Dockerfile完全指南

基础镜像的选择

  • 官方镜像优于非官方镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
  • 固定版本tag而不是每次都是用latest
  • 尽量选择体积小的镜像

    ## 通过RUN执行指令

  • 可以安装、下载软件
  • 配置预置环境
  • 以安装ipconfiggitrepo为例
  • Ubuntu21.04上需要执行以下指令
apt-get update
apt-get insatll net-tools
apt-get install git
mkdir ~/bin  
curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo 
chmod a+x ~/bin/repo
echo "export PATH=~/bin:$PATH" >> ~/.bashrc
source  ~/.bashrc
  • Dockerfile
FROM ubuntu:21.04
RUN apt-get update
RUN apt-get insatll net-tools
RUN apt-get install git
RUN mkdir ~/bin  
RUN curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo 
RUN chmod a+x ~/bin/repo
RUN echo "export PATH=~/bin:$PATH" >> ~/.bashrc
RUN source  ~/.bashrc
  • 不推荐频繁执行RUN命令,因为每执行一次RUN,都会产生一层image layer,而且镜像可能打出来体积会更大,建议合并成一个指令
  • 改进版Dockerfile
FROM ubuntu:21.04
RUN apt-get update && \
    apt-get insatll net-tools -y && \
    apt-get install git -y && \
    mkdir ~/bin && \
    curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo && \
    chmod a+x ~/bin/repo && \
    echo "export PATH=~/bin:$PATH" >> ~/.bashrc && \
    source  ~/.bashrc

文件复制和目录操作

  • 文件复制有两种方式,COPYADD
  • COPYADD都可以把local的一个文件复制到镜像里,如果目标目录不存在,则会自动创建

复制普通文件

FROM python:3.9.5-alpine3.13
COPY hello.py /app/hello.py

比如把local的hello.py复制到容器的/app目录下,如果app目录不存在,则会自动创建

复制压缩文件

ADDCOPY高级一点的地方就是,如果复制的是一个gzip等压缩文件,ADD会帮助我们自动解压缩文件。

FROM python:3.9.5-alpine3.13
ADD hello.tar.gz /app/

目录切换

WORKDIR,当目录不存在时会自动创建

FROM python:3.9.5-alpine3.13
WORKDIR /app
COPY hello.py hello.py

构建参数和环境变量(ARG vs ENV)

  • 使用ARGDockerfile
  • 使用ENVDockerfile

image-20220418104816412

  • 使用arg创建的变量,变量只能存在于构建当中,不会保存在image中
  • 使用env创建的变量,不仅在build中可以使用,也可以作为image的环境变量,在容器中可获取
  • 区别图示:

image-20220418105634832

# 通过--build-arg动态指定arg的VERSION=2.0.0
docker iamge build -f ./Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0
  • 上面这个是arg比env方便的地方

CMD容器启动命令

CMD可以用来设置容器启动时默认执行的命令

  • 容器启动时默认执行的命令
  • 如果docker container run自动容器时指定了其他命令,则CMD会被忽略
  • 如果定义多个CMD,只有最后一个会执行
  • 例如 如下Dockerfile

image-20220418145644458

# 把已经退出的容器全部删除
docker system prune -f
# 把没有使用的镜像全部删除
docker image prune -a
# build镜像
docker image build -t ipinfo .
# 创建容器
docker container run -it ipinfo
  • 我们会发现run之后容器会自动进入shell,但我们Dockerfile没有设置CMD。这是因为Ubuntu基础镜像里有CMD

image-20220418150153125

  • 使用--rm,容器运行结束后会自动删除
docker container run --rm -it ipinfo ipinfo 8.8.8.8

容器启动命令ENTRYPOINT

ENTRYPOINT也可以设置容器启动时要执行的命令,但是和CMD是由去别的。

  • CMD设置的命令,可以在docker container run时传入其他命令,覆盖掉CMD命令,但是ENTERPOINT所设置的命令是一定会被执行的。
  • ENTRYPOINTCMD可以联合使用,ENTRYPOINT设置命令,CMD传递参数
实验
  • Dockerfile-cmd
FROM ubuntu:21.04
CMD ["echo", "hello docker"]
  • Dockerfile-entrypoint
FROM ubuntu:21.04
ENTRYPOINT ["echo", "hello docker"]
  • Dockerfile
FROM ubuntu:21.04
ENTRYPOINT ["echo"]
CMD []

image-20220418151823520

Shell格式和Exec格式

CMD和ENTRYPOINT同时支持shell格式和exec格式。

Shell格式

CMD echo "hello docker"
ENTRYPOINT echo "hello docker"

Exec格式

CMD ["echo", "hello docker"]
ENTRYPOINT ["echo", "hello docker"]

注意shell脚本的问题

FROM ubuntu:21.04
ENV NAME=docker
CMD echo "hello $NAME"

上面的写法是可以的,但是如果把shell写法改成Exec格式,下面的写法是不行的:

FROM ubuntu:21.04
ENV NAME=docker
CMD ["echo", "hello $NAME"]

这样会打印出hello $NAME,而不是hello docker,需要以shell脚本的方式去执行

FROM ubuntu:21.04
ENV NAME=docker
CMD ["sh", "-c", "echo hello $NAME"]

构建一个Python Flask镜像

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'hello, world!'
# 如果没安装虚拟环境,先安装vnev
pip3 install --user virtualenv
# 在当前目录下创建python虚拟环境
python3 -m venv env
# 使虚拟环境生效
./env/bin/activate
# 安装flask
pip3 install flask
# 设置flask环境变量
export FLASK_APP=app.py
# 运行app.py
flask run

image-20220419085957021

本地运行成功,下面编写Dockerfile

FROM python:3.9.5-slim

# 不要写/src,要写/src/,不然会识别成文件
# COPY app.py /src   ×
# COPY app.py /src/  √
# 推荐
COPY app.py /src/app.py

RUN pip install flask

WORKDIR /src
ENV FLASK_APP=app.py

# 暴露5000端口
EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

构建镜像

docker image build -t flask-demo ./

image-20220419091629091

测试运行

docker container run -d -p 5000:5000 flask-demo

image-20220419091639734

image-20220419091731502

部署成功!

Dockerfile最佳实践

合理使用缓存

image-20220421110956422

  • 这里使用了缓存,可以加速构建
  • 一旦某一层命令发生变化,则从这条命令开始往后构建docker镜像都不使用缓存
  • 制作镜像时,应当把固定的、不经常变化的,放在Docekrfile的前面,从而使用缓存加速构架

dockerignore

  • Dockerfile构建image的路径,使用.dockerignore忽略掉镜像不需要的文件(夹),例如下图所示,其中.vscodeenv均不需要打包到镜像中,如果没有忽略,镜像体积会冗余。.dockerignore也可以保护本地的数据不传输到server、不包含到镜像中。

image-20220421112250021

  • 创建.dockerignore,其使用方法与.gitignore基本相同
.vscode/
env/
  • 再次打包后,会发现image镜像体积减少

多阶段构建

  • hello.c
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("hello\n");
    return 0;
}
  • Dockerfile
FROM gcc:9.4

COPY hello.c /src/hello.c

WORKDIR /src

RUN gcc --static -o hello hello.c

ENTRYPOINT ["/src/hello"]

CMD []
  • build image
docker build -t hello-gcc -f Dockerfile .

image-20220421150650768

  • 镜像有1.14GB,很大,基础镜像gcc占用了很大空间。
  • 实际需求是仅仅需要运行编译好的hello二进制文件,这里选择gcc镜像,是因为要用到gcc进行编译,但是编译之后,就不需要gcc了。
  • 这时就要引入docker的多阶段构建

    • 先使用gcc基础镜像,对hello.c进行编译
    • 在选用alpine:3.13.5基础镜像运行hello二进制文件
  • 新的Dockerfile文件
FROM gcc:9.4 AS builder

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 []
  • 最后构建的镜像大小只有6.55MB

image-20220427171941543

尽量使用非root用户

原文链接

root的危险性

docker的root权限一直是其遭受诟病的地方,docker的root权限有那么危险么?我们举个例子。

假如我们有一个用户,叫demo,它本身不具有sudo的权限,所以就有很多文件无法进行读写操作,比如/root目录它是无法查看的。

[demo@docker-host ~]$ sudo ls /root
[sudo] password for demo:
demo is not in the sudoers file.  This incident will be reported.
[demo@docker-host ~]$

但是这个用户有执行docker的权限,也就是它在docker这个group里。

[demo@docker-host ~]$ groups
demo docker
[demo@docker-host ~]$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
busybox      latest    a9d583973f65   2 days ago   1.23MB
[demo@docker-host ~]$

这时,我们就可以通过Docker做很多越权的事情了,比如,我们可以把这个无法查看的/root目录映射到docker container里,你就可以自由进行查看了。

[demo@docker-host vagrant]$ docker run -it -v /root/:/root/tmp busybox sh
/ # cd /root/tmp
~/tmp # ls
anaconda-ks.cfg  original-ks.cfg
~/tmp # ls -l
total 16
-rw-------    1 root     root          5570 Apr 30  2020 anaconda-ks.cfg
-rw-------    1 root     root          5300 Apr 30  2020 original-ks.cfg
~/tmp #

更甚至我们可以给我们自己加sudo权限。我们现在没有sudo权限

[demo@docker-host ~]$ sudo vim /etc/sudoers
[sudo] password for demo:
demo is not in the sudoers file.  This incident will be reported.
[demo@docker-host ~]$

但是我可以给自己添加。

[demo@docker-host ~]$ docker run -it -v /etc/sudoers:/root/sudoers busybox sh
/ # echo "demo    ALL=(ALL)       ALL" >> /root/sudoers
/ # more /root/sudoers | grep demo
demo    ALL=(ALL)       ALL

然后退出container,bingo,我们有sudo权限了。

[demo@docker-host ~]$ sudo more /etc/sudoers | grep demo
demo    ALL=(ALL)       ALL
[demo@docker-host ~]$

如何使用非root用户

  • 以运行一个flask-hello-world项目为例
  • 准备两个Dockerfile文件,前者不指定用户运行flask(即默认是root用户运行),后者指定flask用户(用户组)运行flask
  • 第一个Dockerfile如下
FROM python:3.9.5-slim

RUN pip install flask

COPY app.py /src/app.py

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

假设构建的镜像名称为flask-demo

  • 第二个Dockerfile,使用非root用户构建镜像,镜像名称为flask-no-root

    • 通过groupadd和useradd创建一个flask用户及用户组
    • 通过USER指定后面的命令要以flask用户运行
FROM python:3.9.5-slim

RUN pip install flask && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src
ENV FLASK_APP=app.py

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

image-20220428091504159

第五章 Docker的存储

默认情况下,在运行中的容器里创建的文件,被保存在一个可写的容器层:

  • 如果容器被删除了,则数据也没有了
  • 这个可写的容器是和特定的容器绑定的,也就是这些数据无法方便的和其他容器共享

Docker主要提供了两种方式做数据的持久化

  • Data Volume,由Docker管理,(/var/lib/volumes/Linux),持久化数据的最好方式
  • Bind Mount,由用户指定存储的数据具体mount在系统什么位置

docker-volume

Data Volume

示例:通过Docker container执行计划任务

环境准备

  • 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 my-cron /app/my-cron
WORKDIR /app

# RUN cron job
CMD ["/usr/local/bin/supercronic", "/app/my-cron"]
  • my-cron脚本
*/1 * * * * date >> /app/test.txt
  • 构建镜像
docker image build -t my-cron .
  • Could not resolve host xxx 错误解决

在构建镜像的时候,可能会报如下错误:

Could not resolve host github.com,这是因为docker容器没有配置DNS服务器的,导致域名无法解析的缘故,我们通过查看宿主机的DNS服务器,调整docker的DNS配置相同即可。

首先在宿主机执行

nmcli dev show | grep 'IP4.DNS'

出现

IP4.DNS[1]:                             192.168.4.146
IP4.DNS[2]:                             192.168.4.147

记住上面查询到的DNS服务器地址,编辑/etc/docker/daemon.json文件,在里面添加DNS地址(没有daemon.json创建)

## 此处的dns是填写你前面查询出来的
## 有几个写几个

{                                                                          
    "dns": ["192.168.4.146", "192.168.4.147"]                                                                           
}

#示例文件如下

{
 "registry-mirrors": ["https://nrbewqda.mirror.aliyuncs.com"],
  "dns": ["208.116.3.201"]
}

而后重启docker,再编译即可

service docker restart
  • 编译成功

image-20220505110817305

  • 运行容器并进入到容器中
docker container run -d my-cron
docker container ls
docker container exec -it 087 sh
  • 容器内
/app # ls
my-cron   test.txt
/app # cat test.txt
Tue May 10 06:36:00 UTC 2022
Tue May 10 06:37:00 UTC 2022
  • 停止容器后再启动容器,进入到容器内数据仍然在
root@bill-lixiang-a:~# docker container stop 087
087
root@bill-lixiang-a:~# docker container start 087
087
root@bill-lixiang-a:~# docker container exec -it 087 sh
/app # ls
my-cron   test.txt
/app # more test.txt
Tue May 10 06:36:00 UTC 2022
Tue May 10 06:37:00 UTC 2022
Tue May 10 06:38:00 UTC 2022
Tue May 10 06:39:00 UTC 2022
  • 但是删除容器后,数据就会丢失。如何把数据持久化的存到磁盘上,使用DataVolume实现数据持久化。在Dockerfile内添加VOLUME ["/app"]语句
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 my-cron /app/my-cron
WORKDIR /app

VOLUME ["/app"]

# RUN cron job
CMD ["/usr/local/bin/supercronic", "/app/my-cron"]
  • 重新构建image镜像并运行容器
docker image build -t my-cron .
docker container run -d my-cron
docker container ls
docker container exec -it 792 sh
# =============================
/app # ls
my-cron   test.txt
/app # more test.txt
Tue May 10 06:36:00 UTC 2022
  • 查看volume
docker volume ls
# 查看某个volume详细信息
docker volume inspect f9d264e01253a153c16f3701cff3c7fe6c27c857489a99e83a7a12d834e41659
[
    {
        "CreatedAt": "2022-05-10T14:36:00+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/f9d264e01253a153c16f3701cff3c7fe6c27c857489a99e83a7a12d834e41659/_data",
        "Name": "f9d264e01253a153c16f3701cff3c7fe6c27c857489a99e83a7a12d834e41659",
        "Options": null,
        "Scope": "local"
    }
]
  • 查看挂载点文件夹内容
$ ls /var/lib/docker/volumes/f9d264e01253a153c16f3701cff3c7fe6c27c857489a99e83a7a12d834e41659/_data
my-cron  test.txt

​ 可以看到挂载点在物理机上面存储了,实现了数据的持久化。此时即使删除container,volume也不会被删除。但是如果删除了容器,再次创建个新的容器,也会重新创建一个新的volume。而在实际的场景中,往往我们需要使用原来的volume内的数据,这样虽然实现了数据持久化,但还不是很方便使用。

​ 我们可以给volume起个名字,每次创建新的容器,使用相同的volume名字即可。

# 删除没有使用的volume
$ docker volume prune
# 创建容器的时候给volume起个名
$ docker container run -d -v cron-data:/app my-cron
# 可以看到volume列表有一个cron-data的卷
$ docker volume ls
DRIVER    VOLUME NAME
local     cron-data
# 查看volume详细信息
$ docker volume inspect cron-data
[
    {
        "CreatedAt": "2022-05-10T14:57:59+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/cron-data/_data",
        "Name": "cron-data",
        "Options": null,
        "Scope": "local"
    }
]
# 删除容器,可以看到volume数据还在
$ docker container rm -f 43a
$ more /var/lib/docker/volumes/cron-data/_data/test.txt
Tue May 10 06:58:00 UTC 2022
Tue May 10 06:59:00 UTC 2022
Tue May 10 07:00:00 UTC 2022
  • 此时我们可以再去创建一个container,并挂载先前的volume
$ docker container run -d -v cron-data:/app my-cron
$ docker container exec -it f79 sh
/app # more test.txt
Tue May 10 06:58:00 UTC 2022
Tue May 10 06:59:00 UTC 2022
Tue May 10 07:00:00 UTC 2022
Tue May 10 07:05:00 UTC 2022

可以看到先前volume的数据被挂载上来,后面新的log会在之前log的基础上追加。

注:只能在Linux上直接查看volume的数据,Windows上也可以查看,但是比较麻烦,可以网上搜索。上面实验的命令均在Linux环境下进行。

Data Volume 练习MySQL

准备镜像

$ docker pull mysql:5.7
$ docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
mysql         5.7       8aa4b5ffb001   12 days ago   462MB

创建容器

$ docker container run --name some-mysql -e MYSQL_ROOT_PASSWORD=123456 -d -v mysql-data:/var/lib/mysql mysql:5.7
ae04887aa2787ed9663fc54bf0ebca131ab71d39df4790b4bf0fe1ec0381ea6c
$ docker container ls
CONTAINER ID   IMAGE       COMMAND                  CREATED          STATUS          PORTS                 NAMES
ae04887aa278   mysql:5.7   "docker-entrypoint.s…"   20 seconds ago   Up 19 seconds   3306/tcp, 33060/tcp   some-mysql
$ docker volume inspect mysql-data
[
    {
        "CreatedAt": "2022-05-10T15:37:28+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
        "Name": "mysql-data",
        "Options": null,
        "Scope": "local"
    }
]
# 进入到MySQL容器
$ docker container exec -it ae0 sh

数据库写入数据

  • MySQL容器
mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.38 MySQL Community Server (GPL)

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

mysql> create database demo;
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| demo               |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql> exit
Bye
  • 退出MySQL容器
exit
  • 查看mysql-data volume内的数据
$ ls /var/lib/docker/volumes/mysql-data/_data
auto.cnf         demo            ibtmp1              server-cert.pem
ca-key.pem       ib_buffer_pool  mysql               server-key.pem
ca.pem           ibdata1         performance_schema  sys
client-cert.pem  ib_logfile0     private_key.pem
client-key.pem   ib_logfile1     public_key.pem

重新创建容器

  • 删除MySQL容器,删除container并不会删除volume
docker container rm -f ae0
  • 创建新的MySQL容器并挂载先前的volume,进入容器,查看database
$ docker container run --name some-mysql -e MYSQL_ROOT_PASSWORD=123456 -d -v mysql-data:/var/lib/mysql mysql:5.7
6a2e3ae691052e19d6cd8169496f107574a7f6ce1c1a32a5066a5cf03772c06e
$ docker container exec -it 6a2 sh
  • MySQL容器
mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.38 MySQL Community Server (GPL)

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| demo               |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql>

可以看到之前的demo数据库已经在新的MySQL容器中了。

数据持久化之Bind Mount

  • 还是使用之前my-cron的Dockerfile
  • 不在Dockerfile文件里指定VOLUME ["/app"]
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 my-cron /app/my-cron
WORKDIR /app

# RUN cron job
CMD ["/usr/local/bin/supercronic", "/app/my-cron"]
  • 编译镜像
docker image build -t my-cron .

​ 下面由于我们将/root/app目录映射到容器里的/app目录,需要把my-cron脚本也复制到物理机的/root/app目录下,否则运行会提示open /app/my-cron: no such file or directory

  • 使用Bind Mount运行容器
cp /home/bill/data/studyDocker/mycron/my-cron /root/app/
# 直接绑定路径即可(-v /root/app:/app)
$ docker container run -d -v /root/app:/app my-cron
$ ls /root/app
my-cron  test.txt
$ more /root/app/test.txt
Wed May 11 01:36:00 UTC 2022

​ 可以看到容器中的运行结果输出到了物理机上。

Bind Mount练习 - Docker开发环境

场景:本地需要C语言的开发环境来编译C语言的代码,但是本地没有C语言开发环境。

此时可以使用Docker的Bind Mount

  • 待编译调试文件hello.c
  • 文件路径在/home/bill/data/studyDocker/mycpp
void main(int argc, char *argv[]) {
    printf("hello %s\n", argv[argc - 1]);
}
  • 拉取gcc9.4镜像,运行容器
docker image pull gcc:9.4
# 这样运行,默认执行命令为bash,运行一下会退出,需要用交互式运行容器
docker container run -d -v /home/bill/data/studyDocker/mycpp:/root gcc:9.4
# -it运行容器
docker container run -it -v /home/bill/data/studyDocker/mycpp:/root gcc:9.4
  • 进入到容器
root@1bd22d6de49f:/# cd ~
root@1bd22d6de49f:~# ls
hello.c
root@1bd22d6de49f:~# gcc -o hello hello.c
hello.c: In function 'main':
hello.c:2:5: warning: implicit declaration of function 'printf' [-Wimplicit-function-declaration]
    2 |     printf("hello %s\n", argv[argc - 1]);
      |     ^~~~~~
hello.c:2:5: warning: incompatible implicit declaration of built-in function 'printf'
hello.c:1:1: note: include '<stdio.h>' or provide a declaration of 'printf'
  +++ |+#include <stdio.h>
    1 | void main(int argc, char *argv[]) {
root@1bd22d6de49f:~# ls
hello  hello.c
root@1bd22d6de49f:~# ./hello docker
hello docker

多个机器之间的容器共享数据

image-20220523081310508

Docker的volume支持多种driver。默认创建的volume driver都是local

本节我们使用sshfs(ssh file system)的driver,如何让docker使用不在同一台机器上的文件系统做volume

环境准备

准备三台Linux机器,之间可以通过SSH通信。

hostnameipssh usernamessh password
docker-host1192.168.200.10vagrantvagrant
docker-host2192.168.200.11vagrantvagrant
docker-host3192.168.200.12vagrantvagrant

安装sshfs-plugin

在其中两台机器上安装一个plugin vieux/sshfs

docker plugin install --grant-all-permissions vieux/sshfs

创建volume(名字为sshvolume)

docker volume create --driver vieux/sshfs \
    -o sshcmd=vagrant@192.168.200.12:/home/vagrant \
    -o password=vagrant \
    sshvolume

### 查看volume

$ docker volume ls
DRIVER               VOLUME NAME
vieux/sshfs:latest   sshvolume
$ docker volume inspect sshvolume
[
    {
        "CreatedAt": "0001-01-01T00:00:00Z",
        "Driver": "vieux/sshfs:latest",
        "Labels": {},
        "Mountpoint": "/mnt/volumes/f59e848643f73d73a21b881486d55b33",
        "Name": "sshvolume",
        "Options": {
            "password": "vagrant",
            "sshcmd": "vagrant@192.168.200.12:/home/vagrant"
        },
        "Scope": "local"
    }
]

创建容器挂载volume

docker run -it -v sshvolume:/app busybox sh
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
b71f96345d44: Pull complete
Digest: sha256:930490f97e5b921535c153e0e7110d251134cc4b72bbb8133c6a5065cc68580d
Status: Downloaded newer image for busybox:latest
/ #
/ # ls
app   bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # cd /app
/app # ls
/app # echo "this is ssh volume"> test.txt
/app # ls
test.txt
/app # more test.txt
this is ssh volume
/app #
/app #

test.txt文件我们可以在docker-host3上看到

第六章 Docker的网络

网络基础知识回顾

当我们访问一个WEB站点时,会触发以下流程:

https://www.homenethowto.com/advanced-topics/traffic-example-the-full-picture/

网络常用命令

IP地址的查看

Windows

ipconfig

Linux

ifconfig

ip addr

网络连通性测试

ping

测试IP是否可达

ping 127.0.0.1

telnet

测试端口的连通性

telnet www.baidu.com 80

traceroute

路径探测跟踪

Linux使用tracepath

tracepath www.baidu.com

Windows使用tracert

tracert www.baidu.com

curl

请求WEB服务

http://www.ruanyifeng.com/blog/2019/09/curl-reference.html

Docker bridge 网络

image-20220817140032096

bill@bill-lixiang-a:~$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
774b5e0089ac   bridge    bridge    local
f6fe9fa807b4   host      host      local
165075680a9c   net1      bridge    local
1d0b2dcf23fc   net2      bridge    local
fb02fb964d7a   none      null      local
bill@bill-lixiang-a:~$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "774b5e0089ac3c9726ffa5c2a7cb33ce07294d325a24ddcedb6dacdf0724c172",
        "Created": "2022-08-18T09:20:17.847442277+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]
  • docker0: bridge
  • brctl 使用前需要安装, 对于CentOS, 可以通过 sudo yum install -y bridge-utils 安装. 对于Ubuntu, 可以通过 sudo apt-get install -y bridge-utils
bill@bill-lixiang-a:~$ brctl show
bridge name     bridge id               STP enabled     interfaces
br-165075680a9c         8000.02422a4ad8c0       no              vetha63ea58
                                                        vethccd3776
br-1d0b2dcf23fc         8000.024209dfbea4       no              veth38d5113
                                                        veth73c221d
                                                        vethb57f9a8
                                                        vethe7eb813
                                                        vetheea0225
                                                        vetheef23fc
docker0         8000.024236471856       no

容器对外通信

  • 查看路由表
bill@bill-lixiang-a:~$ ip route
default via 10.248.195.254 dev enp3s0 proto dhcp metric 100
10.248.192.0/22 dev enp3s0 proto kernel scope link src 10.248.192.137 metric 100
169.254.0.0/16 dev enp3s0 scope link metric 1000
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
172.18.0.0/24 dev br-165075680a9c proto kernel scope link src 172.18.0.1
172.19.0.0/16 dev br-1d0b2dcf23fc proto kernel scope link src 172.19.0.1
  • iptables转发规则
bill@bill-lixiang-a:~$ sudo iptables --list -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !localhost/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.19.0.0/16        anywhere
MASQUERADE  all  --  172.18.0.0/24        anywhere
MASQUERADE  all  --  172.17.0.0/16        anywhere
MASQUERADE  tcp  --  172.18.0.7           172.18.0.7           tcp dpt:8888
MASQUERADE  tcp  --  172.18.0.7           172.18.0.7           tcp dpt:mysql
MASQUERADE  tcp  --  172.18.0.2           172.18.0.2           tcp dpt:mysql
MASQUERADE  tcp  --  172.19.0.2           172.19.0.2           tcp dpt:6379
MASQUERADE  tcp  --  172.19.0.3           172.19.0.3           tcp dpt:6379
MASQUERADE  tcp  --  172.19.0.4           172.19.0.4           tcp dpt:6379
MASQUERADE  tcp  --  172.19.0.5           172.19.0.5           tcp dpt:6379
MASQUERADE  tcp  --  172.19.0.6           172.19.0.6           tcp dpt:6379
MASQUERADE  tcp  --  172.19.0.7           172.19.0.7           tcp dpt:6379

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere
DNAT       tcp  --  anywhere             anywhere             tcp dpt:4001 to:172.18.0.7:8888
DNAT       tcp  --  anywhere             anywhere             tcp dpt:4002 to:172.18.0.7:3306
DNAT       tcp  --  anywhere             anywhere             tcp dpt:mysql to:172.18.0.2:3306
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5001 to:172.19.0.2:6379
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5002 to:172.19.0.3:6379
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5003 to:172.19.0.4:6379
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5004 to:172.19.0.5:6379
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5005 to:172.19.0.6:6379
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5006 to:172.19.0.7:6379

更多资料:s1-firewall-ipt-fwd.html

NAT技术

image-20220824192349188

创建和使用自定义Bridge

bill@bill-lixiang-a:~$ docker network create -d bridge mybridge
54415915c9f9277d4bf5114f0b1e055f72002cd0d49c80b4f16a505344721cd3
bill@bill-lixiang-a:~$ docker network ls
NETWORK ID     NAME       DRIVER    SCOPE
774b5e0089ac   bridge     bridge    local
f6fe9fa807b4   host       host      local
54415915c9f9   mybridge   bridge    local
165075680a9c   net1       bridge    local
1d0b2dcf23fc   net2       bridge    local
fb02fb964d7a   none       null      local
  • -d指定driver为bridge
bill@bill-lixiang-a:~$ docker network inspect mybridge
[
    {
        "Name": "mybridge",
        "Id": "54415915c9f9277d4bf5114f0b1e055f72002cd0d49c80b4f16a505344721cd3",
        "Created": "2022-08-24T19:25:54.947586129+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]
  • 创建容器并加入到mybridge网络
docker container run -d --rm --name box1 --network=mybridge busybox /bin/sh -c "while true; do sleep 3600; done"
  • inspect box1
bill@bill-lixiang-a:~$ docker container inspect box1
[
    {
        "Id": "ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450",
        "Created": "2022-08-24T11:30:54.203225644Z",
        "Path": "/bin/sh",
        "Args": [
            "-c",
            "while true; do sleep 3600; done"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 909008,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2022-08-24T11:30:54.546915117Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:7a80323521ccd4c2b4b423fa6e38e5cea156600f40cd855e464cc52a321a24dd",
        "ResolvConfPath": "/var/snap/docker/common/var-lib-docker/containers/ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450/resolv.conf",
        "HostnamePath": "/var/snap/docker/common/var-lib-docker/containers/ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450/hostname",
        "HostsPath": "/var/snap/docker/common/var-lib-docker/containers/ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450/hosts",
        "LogPath": "/var/snap/docker/common/var-lib-docker/containers/ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450/ed3d8c95d32ed3ce29abcced597a4ad5225bdfbb92d9223ec371ea7935322450-json.log",
        "Name": "/box1",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "docker-default",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "mybridge",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": true,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "CgroupnsMode": "host",
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": false,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/snap/docker/common/var-lib-docker/overlay2/f5d046130a18e596a5b80686f82e792152180ef3c7e85ee398df5d15961f8aa0-init/diff:/var/snap/docker/common/var-lib-docker/overlay2/0c139c1fe59d74c09444d322839d9428d88fa2f7f36f17d57afff5ae9f4f56d5/diff",
                "MergedDir": "/var/snap/docker/common/var-lib-docker/overlay2/f5d046130a18e596a5b80686f82e792152180ef3c7e85ee398df5d15961f8aa0/merged",
                "UpperDir": "/var/snap/docker/common/var-lib-docker/overlay2/f5d046130a18e596a5b80686f82e792152180ef3c7e85ee398df5d15961f8aa0/diff",
                "WorkDir": "/var/snap/docker/common/var-lib-docker/overlay2/f5d046130a18e596a5b80686f82e792152180ef3c7e85ee398df5d15961f8aa0/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [],
        "Config": {
            "Hostname": "ed3d8c95d32e",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/bin/sh",
                "-c",
                "while true; do sleep 3600; done"
            ],
            "Image": "busybox",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "b0b56b6232d4a82240f77d8af17da76f9ca5cf827bcbe2cd558c48de10ac6a64",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {},
            "SandboxKey": "/run/snap.docker/netns/b0b56b6232d4",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "",
            "Gateway": "",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "",
            "IPPrefixLen": 0,
            "IPv6Gateway": "",
            "MacAddress": "",
            "Networks": {
                "mybridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": [
                        "ed3d8c95d32e"
                    ],
                    "NetworkID": "54415915c9f9277d4bf5114f0b1e055f72002cd0d49c80b4f16a505344721cd3",
                    "EndpointID": "bdeee9ee6a0c4d80a963975e8e33e95e52de2b22580bc13581990cc36867e463",
                    "Gateway": "172.20.0.1",
                    "IPAddress": "172.20.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:14:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]
  • 让box1容器同时也连接bridge网络
docker network connect bridge box1
  • inspect box1的network部分
"Networks": {
    "bridge": {
        "IPAMConfig": {},
        "Links": null,
        "Aliases": [],
        "NetworkID": "774b5e0089ac3c9726ffa5c2a7cb33ce07294d325a24ddcedb6dacdf0724c172",
        "EndpointID": "cf6d2b1a8cc6d62b1f19605f73399ae43b37a867039f688510ca8badb312c179",
        "Gateway": "172.17.0.1",
        "IPAddress": "172.17.0.2",
        "IPPrefixLen": 16,
        "IPv6Gateway": "",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "MacAddress": "02:42:ac:11:00:02",
        "DriverOpts": {}
    },
    "mybridge": {
        "IPAMConfig": null,
        "Links": null,
        "Aliases": [
            "ed3d8c95d32e"
        ],
        "NetworkID": "54415915c9f9277d4bf5114f0b1e055f72002cd0d49c80b4f16a505344721cd3",
        "EndpointID": "bdeee9ee6a0c4d80a963975e8e33e95e52de2b22580bc13581990cc36867e463",
        "Gateway": "172.20.0.1",
        "IPAddress": "172.20.0.2",
        "IPPrefixLen": 16,
        "IPv6Gateway": "",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "MacAddress": "02:42:ac:14:00:02",
        "DriverOpts": null
    }
}

image-20220824193718842

  • 让box1容器取消连接bridge网络
docker network disconnect bridge box1
  • 自定义的bridge可以提供容器名称到容器IP地址的解析服务,例如可以直接ping box1
bill@bill-lixiang-a:~$ docker container run -d --rm --name box2 --network=mybridge busybox /bin/sh -c "while true; do sleep 3600; done"
bill@bill-lixiang-a:~$ docker exec -it box2 ping box1
PING box1 (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.061 ms
64 bytes from 172.20.0.2: seq=1 ttl=64 time=0.141 ms
64 bytes from 172.20.0.2: seq=2 ttl=64 time=0.138 ms
64 bytes from 172.20.0.2: seq=3 ttl=64 time=0.176 ms
^C
--- box1 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.061/0.129/0.176 ms
  • 创建网络时指定子网掩码和网关
docker network create -d bridge --gateway=172.200.0.1 --subnet=172.200.0.0/16 demo
  • 查看demo bridge信息
bill@bill-lixiang-a:~$ docker network inspect demo
[
    {
        "Name": "demo",
        "Id": "e5f71518d5114a504eacc16ad39f895f7e6849323fb531abf8334d5fd3668f25",
        "Created": "2022-08-24T19:48:12.210521782+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.200.0.0/16",
                    "Gateway": "172.200.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

容器的端口转发

  • 查看容器的IP地址
docker container inspect --format '{{.NetworkSettings.IPAddress}}' box1
  • Dockerfile内不写EXPOSE 端口号,也可以在容器创建/运行的时候,使用-p参数进行端口转发。EXPOSE更多的作用是给Docerfile的使用者以提示(a type of documentation)

Docker host 网络详解

# 删掉之前的实验box
docker container rm -f box1 box2
# 新建box使用host网络
docker container run -d --rm --name box1 --network=host busybox /bin/sh -c "while true; do sleep 3600; done"
  • host模式下建两个nginx container
docker container run -d --name web1 --network=host nginx
docker container run -d --name web2 --network=host nginx
  • web1启动成功,但web2启动失败,因为web1和web2均绑定了宿主机的80端口,web1已经使用了80端口,因此web2绑定失败

Docker null网络

image-20220825081400417

docker container rm -f box1
# 新建box使用none网络
docker container run -d --rm --name box1 --network=none busybox /bin/sh -c "while true; do sleep 3600; done"
docker container exec -it box1 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
/ #
  • 只有lo本地网络,与外界无法通信

Linux网络命名空间(未理解)

Linux的Namespace(命名空间)技术是一种隔离技术,常用的Namespace有 user namespace, process namespace, network namespace等

在Docker容器中,不同的容器通过Network namespace进行了隔离,也就是不同的容器有各自的IP地址,路由表等,互不影响。

实验

准备一台Linux机器,这一节会用到一个叫 brtcl 的命令,这个命令需要安装,如果是Ubuntu的系统,可以通过 apt-get install bridge-utils 安装;如果是Centos系统,可以通过 sudo yum install bridge-utils 来安装

image-20220825082349168

创建bridge

bill@bill-lixiang-a:~$ sudo brctl addbr mydocker0
[sudo] bill 的密码:
bill@bill-lixiang-a:~$ brctl show
bridge name     bridge id               STP enabled     interfaces
br-165075680a9c         8000.02422a4ad8c0       no              vetha63ea58
                                                        vethccd3776
br-1d0b2dcf23fc         8000.024209dfbea4       no              veth38d5113
                                                        veth73c221d
                                                        vethb57f9a8
                                                        vethe7eb813
                                                        vetheea0225
                                                        vetheef23fc
br-54415915c9f9         8000.0242cdd97887       no
br-e5f71518d511         8000.024276bbb280       no
docker0         8000.024236471856       no              veth76eb360
mydocker0               8000.000000000000       no

准备一个shell脚本

  • add-ns-to-br.sh
#!/bin/bash

bridge=$1
namespace=$2
addr=$3

vethA=veth-$namespace
vethB=eth00-$namespace

sudo ip netns add $namespace
sudo ip link add $vethA type veth peer name $vethB

sudo ip link set $vethB netns $namespace
sudo ip netns exec $namespace ip addr add $addr dev $vethB
sudo ip netns exec $namespace ip link set $vethB up

sudo ip link set $vethA up

sudo brctl addif $bridge $vethA

脚本执行

sh add-ns-to-br.sh mydocker0 ns1 172.16.1.1/16
sh add-ns-to-br.sh mydocker0 ns2 172.16.1.2/16

把mydocker0这个bridge up起来

sudo ip link set dev mydocker0 up

验证

[vagrant@docker-host1 ~]$ sudo ip netns exec ns1 bash
[root@docker-host1 vagrant]# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
5: eth00@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether f2:59:19:34:73:70 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.16.1.1/16 scope global eth00
    valid_lft forever preferred_lft forever
    inet6 fe80::f059:19ff:fe34:7370/64 scope link
    valid_lft forever preferred_lft forever
[root@docker-host1 vagrant]# ping 172.16.1.2
PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.
64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.080 ms
^C
--- 172.16.1.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1000ms
rtt min/avg/max/mdev = 0.029/0.054/0.080/0.026 ms
[root@docker-host1 vagrant]#

对外通信

https://www.karlrupp.net/en/computer/nat_tutorial

多容器应用部署实验

Python Flask + Redis 练习

image-20220825160908840

  • 使用自定义bridge,这样可以通过容器的名字去通信(DNS的效果),而默认bridge无此功能
  • 准备一个flask程序
  • app.py
from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"
  • Dockerfile
FROM python:3.9.5-slim

RUN pip install flask redis && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK_APP=app.py REDIS_HOST=redis

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
  • 构建flask镜像,准备一个redis镜像
docker pull redis
docker build -t flask-demo .
  • 创建docker-bridge
docker network create -d bridge demo-network
  • 创建redis container、flask container
docker container run -d --name redis-server --network demo-network redis
docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo

打开浏览器访问 http://127.0.0.1:5000

应该能看到类似下面的内容,每次刷新页面,计数加1

image-20220825172418978

Hello Container World! I have been seen 3 times and my hostname is d7c13e2fbe5f.

  • 总结:如果把上面的步骤合并到一起,成为一个部署脚本
# prepare image
docker image pull redis
docker image build -t flask-demo .

# create network
docker network create -d bridge demo-network

# create container
docker container run -d --name redis-server --network demo-network redis
docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo

第七章 Docker Compose

什么是Docker Compose

  • 简化多容器部署

安装Docker Compose

  • Windows和Mac在默认安装了docker desktop以后,docker-compose随之自动安装
docker-compose --version
dnf install docker-compose # fedora
yum install docker-compose # CentOS 7/ RHEL7
apt-get install docker-compose # debian及其变种如Ubuntu
apk add docker-compose # alpine
pacman -S docker-compose # arch
  • 安装完毕后查看版本号
bill@bill-lixiang-a:~$ docker-compose --version
docker-compose version 1.25.0, build unknown

compose文件的结构和版本

基本语法结构

version: "3.8"

services: # 容器
  servicename: # 服务名字,这个名字也是内部 bridge网络可以使用的 DNS name
    image: # 镜像的名字
    command: # 可选,如果设置,则会覆盖默认镜像里的 CMD命令
    environment: # 可选,相当于 docker run里的 --env
    volumes: # 可选,相当于docker run里的 -v
    networks: # 可选,相当于 docker run里的 --network
    ports: # 可选,相当于 docker run里的 -p
  servicename2:

volumes: # 可选,相当于 docker volume create

networks: # 可选,相当于 docker network create

以上一章结尾的python+flask改造为例:

docker image pull redis
docker image build -t flask-demo .

# create network
docker network create -d bridge demo-network

# create container
docker container run -d --name redis-server --network demo-network redis
docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo

改造成dokcer-compose.yml:

version: "3.8"

services:
  flask-demo:
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - demo-network
    ports:
      - 8080:5000

  redis-server:
    image: redis:latest
    networks:
      - demo-network

networks:
  demo-network:
  • demo-network不指定driver的话默认为bridge

docker-compose 语法版本

向后兼容

https://docs.docker.com/compose/compose-file/

  • 在命令行使用docker-compose
docker-compose up
Creating network "flask_demo-network" with the default driver
Creating flask_redis-server_1 ... done
Creating flask_flask-demo_1   ... done
Attaching to flask_flask-demo_1, flask_redis-server_1
redis-server_1  | 1:C 26 Aug 2022 06:23:51.210 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis-server_1  | 1:C 26 Aug 2022 06:23:51.210 # Redis version=7.0.4, bits=64, commit=00000000, modified=0, pid=1, just started
redis-server_1  | 1:C 26 Aug 2022 06:23:51.210 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis-server_1  | 1:M 26 Aug 2022 06:23:51.210 * Increased maximum number of open files to 10032 (it was originally set to 1024).
redis-server_1  | 1:M 26 Aug 2022 06:23:51.210 * monotonic clock: POSIX clock_gettime
redis-server_1  | 1:M 26 Aug 2022 06:23:51.210 * Running mode=standalone, port=6379.
redis-server_1  | 1:M 26 Aug 2022 06:23:51.210 # Server initialized
redis-server_1  | 1:M 26 Aug 2022 06:23:51.210 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
redis-server_1  | 1:M 26 Aug 2022 06:23:51.211 * Ready to accept connections
flask-demo_1    |  * Serving Flask app 'app.py'
flask-demo_1    |  * Debug mode: off
flask-demo_1    | WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
flask-demo_1    |  * Running on all addresses (0.0.0.0)
flask-demo_1    |  * Running on http://127.0.0.1:5000
flask-demo_1    |  * Running on http://172.22.0.3:5000
flask-demo_1    | Press CTRL+C to quit

image-20220826142520586

image-20220826142940982

  • 上面的过程是在前台运行
  • 在后台运行使用-d参数即可
  • 查看log类似docker查看某个容器的log

    • docker-compose logs
    • docker-compose logs -f
docker-compose up -d

image-20220826143046313

docker-compose ps

image-20220826143129865

docker-compose stop

image-20220826143345253

docker-compose rm

image-20220826143432389

  • 通过docker container ls可以看到通过docker-compose创建的容器都有前缀
  • 可以通过指定容器前缀参数-p替换掉系统默认前缀
docker-compose -p myproject up -d

image-20220826143753986

  • 如果使用自定义前缀参数-p,在使用docker-compose进行其他操作时,都需要带上-p
docker-compose -p myproject ps

image-20220826143935820

docker-compose -p myproject stop
docker-compose -p myproject rm

image-20220826144109709

  • 容器名称指定
version: "3.8"

services:
  flask-demo:
    # 自定义容器名,但会影响到后面的scale操作
      container_name: my-flask-demo
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - demo-network
    ports:
      - 8080:5000

  redis-server:
    image: redis:latest
    networks:
      - demo-network

networks:
  demo-network:

docker-compose 镜像的拉取和构建

  • docker-compose引用的镜像在本地不存在时,会去Docker Hub上自动拉取
  • 在docker-compose.yml中加入build参数可以在本地进行构建,提前准备好构建目录flask,与docker-compose.yml在同一目录
  • 使用docker-compose build进行构建
.
├── docker-compose.yml
└── flask
    ├── app.py
    └── Dockerfile
  • docker-compose.yml
version: "3.8"

services:
  flask-demo:
    build: ./flask
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - demo-network
    ports:
      - 8080:5000

  redis-server:
    image: redis:latest
    networks:
      - demo-network

networks:
  demo-network:
  • docker-compose build
# 先删除本地的镜像
docker image rm flask-demo:latest
# build
docker-compose build
redis-server uses an image, skipping
Building flask-demo
Sending build context to Docker daemon  3.072kB
Step 1/8 : FROM python:3.9.5-slim
 ---> c71955050276
Step 2/8 : RUN pip install flask redis &&     groupadd -r flask && useradd -r -g flask flask &&     mkdir /src &&     chown -R flask:flask /src
 ---> Running in 34a10a3c414b
Collecting flask
  Downloading Flask-2.2.2-py3-none-any.whl (101 kB)
Collecting redis
  Downloading redis-4.3.4-py3-none-any.whl (246 kB)
Collecting Jinja2>=3.0
  Downloading Jinja2-3.1.2-py3-none-any.whl (133 kB)
Collecting Werkzeug>=2.2.2
  Downloading Werkzeug-2.2.2-py3-none-any.whl (232 kB)
Collecting click>=8.0
  Downloading click-8.1.3-py3-none-any.whl (96 kB)
Collecting itsdangerous>=2.0
  Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting importlib-metadata>=3.6.0
  Downloading importlib_metadata-4.12.0-py3-none-any.whl (21 kB)
Collecting zipp>=0.5
  Downloading zipp-3.8.1-py3-none-any.whl (5.6 kB)
Collecting MarkupSafe>=2.0
  Downloading MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Collecting deprecated>=1.2.3
  Downloading Deprecated-1.2.13-py2.py3-none-any.whl (9.6 kB)
Collecting packaging>=20.4
  Downloading packaging-21.3-py3-none-any.whl (40 kB)
Collecting async-timeout>=4.0.2
  Downloading async_timeout-4.0.2-py3-none-any.whl (5.8 kB)
Collecting wrapt<2,>=1.10
  Downloading wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (77 kB)
Collecting pyparsing!=3.0.5,>=2.0.2
  Downloading pyparsing-3.0.9-py3-none-any.whl (98 kB)
Installing collected packages: zipp, wrapt, pyparsing, MarkupSafe, Werkzeug, packaging, Jinja2, itsdangerous, importlib-metadata, deprecated, click, async-timeout, redis, flask
Successfully installed Jinja2-3.1.2 MarkupSafe-2.1.1 Werkzeug-2.2.2 async-timeout-4.0.2 click-8.1.3 deprecated-1.2.13 flask-2.2.2 importlib-metadata-4.12.0 itsdangerous-2.1.2 packaging-21.3 pyparsing-3.0.9 redis-4.3.4 wrapt-1.14.1 zipp-3.8.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
WARNING: You are using pip version 21.1.3; however, version 22.2.2 is available.
You should consider upgrading via the '/usr/local/bin/python -m pip install --upgrade pip' command.
Removing intermediate container 34a10a3c414b
 ---> a33a8f69c0d3
Step 3/8 : USER flask
 ---> Running in aac585018a53
Removing intermediate container aac585018a53
 ---> bf6ae47bf7ec
Step 4/8 : COPY app.py /src/app.py
 ---> 8a001223aa26
Step 5/8 : WORKDIR /src
 ---> Running in a48772836061
Removing intermediate container a48772836061
 ---> 97f70b13d3c6
Step 6/8 : ENV FLASK_APP=app.py REDIS_HOST=redis
 ---> Running in a357a0a187de
Removing intermediate container a357a0a187de
 ---> 4538f84aa7fa
Step 7/8 : EXPOSE 5000
 ---> Running in 43ccfcaa47d6
Removing intermediate container 43ccfcaa47d6
 ---> 931950029385
Step 8/8 : CMD ["flask", "run", "-h", "0.0.0.0"]
 ---> Running in c23cb1f2a107
Removing intermediate container c23cb1f2a107
 ---> 1a24120de6c1
Successfully built 1a24120de6c1
Successfully tagged flask-demo:latest
  • build的更多参数
  • 重命名Dockerfile
mv ./flask/Dockerfile ./flask/Dockerfile.dev
  • 设置build参数
version: "3.8"

services:
  flask-demo:
    build: 
      context: ./flask
      dockerfile: Dockerfile.dev
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - demo-network
    ports:
      - 8080:5000

  redis-server:
    image: redis:latest
    networks:
      - demo-network

networks:
  demo-network:
  • 构建
bill@bill-lixiang-a:~/flask$ docker-compose build
redis-server uses an image, skipping
Building flask-demo
Sending build context to Docker daemon  3.072kB
Step 1/8 : FROM python:3.9.5-slim
 ---> c71955050276
Step 2/8 : RUN pip install flask redis &&     groupadd -r flask && useradd -r -g flask flask &&     mkdir /src &&     chown -R flask:flask /src
 ---> Using cache
 ---> a33a8f69c0d3
Step 3/8 : USER flask
 ---> Using cache
 ---> bf6ae47bf7ec
Step 4/8 : COPY app.py /src/app.py
 ---> Using cache
 ---> 8a001223aa26
Step 5/8 : WORKDIR /src
 ---> Using cache
 ---> 97f70b13d3c6
Step 6/8 : ENV FLASK_APP=app.py REDIS_HOST=redis
 ---> Using cache
 ---> 4538f84aa7fa
Step 7/8 : EXPOSE 5000
 ---> Using cache
 ---> 931950029385
Step 8/8 : CMD ["flask", "run", "-h", "0.0.0.0"]
 ---> Using cache
 ---> 1a24120de6c1
Successfully built 1a24120de6c1
Successfully tagged flask-demo:latest
  • docker-compose pull
docker-compose pull

image-20220826152315226

  • 有的镜像需要在本地先去build

docker-compose 服务更新

  • 项目的文件有修改需要进行docker-compose服务更新
  • 使用--build参数,即可实现镜像更新
docker-compose up -d --build

image-20220826152941854

  • 总结

    • 使用--remove-orphans

      • 修改镜像文件,需要重新构建image
      • docker-compose up -d --build
    • 使用restart

docker-compose 网络

  • 自己创建的driver为bridge的网络,支持容器名=>IP地址解析(DNS解析)
  • docker-compose up后会自己建一个network(bridge)
  • 实验一、

image-20220830141628847

  • 实验二、
  • imap可以指定子网

image-20220830141948525

docker-compose 水平扩展和负载均衡

实验环境及文件

  • 目录机构

flask1
├── docker-compose.yml
└── flask

   ├── app.py
   └── Dockerfile
  • 文件源码

docker-compose.yml

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server

  redis-server:
    image: redis:latest

  client:
    image: xiaopeng163/net-box:latest
    command: sh -c "while true; do sleep 3600; done;"

flask/app.py

from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"

flask/Dockerfile

FROM python:3.9.5-slim

RUN pip install flask redis && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK=app.py REDIS_HOST=redis

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

启动及验证

docker-compose up -d
docker-compose ps

image-20220830142857971

  • 进入到client中,测试docker-compose新建的bridge网络的容器名=>IP解析功能
docker container ls | grep flask1

image-20220830143352653

docker container exec -it b504 sh
/omd # ping flask
/omd # ping redis-server

image-20220830143617221

可以ping通

  • 在client访问flask服务
/omd # curl flask:5000
Hello Container World! I have been seen 1 times and my hostname is 6efbbf18a04a.
/omd # curl flask:5000
Hello Container World! I have been seen 2 times and my hostname is 6efbbf18a04a.

flask服务及redis服务测试访问成功,验证通过。

flask服务水平扩展

docker-compose up -d --scale flask=3 # 扩展总数

image-20220830143933738

# 向下scale
docker-compose up -d --scale flask=1 # 扩展总数

image-20220830144056797

  • 水平扩展后,会自动分配IP地址,如果去ping flask,docker网络可以自动负载均衡

比如去扩展3个flask服务:

docker-compose up -d --scale flask=3
# 进入到client容器
docker container exec -it b504 sh
# 查看flask域名解析
/omd # nslookup flask
Server:         127.0.0.11
Address:        127.0.0.11#53

Non-authoritative answer:
Name:   flask
Address: 172.24.0.6
Name:   flask
Address: 172.24.0.5
Name:   flask
Address: 172.24.0.3

image-20220830144555192

可以看到flask对应三个address

  • 在client容器内访问flask服务也可以观察到负载均衡过程
/omd # curl flask:5000
Hello Container World! I have been seen 3 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 4 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 5 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 6 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 7 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 8 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 9 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 10 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 11 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 12 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 13 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 14 times and my hostname is 6efbbf18a04a.
/omd # curl flask:5000
Hello Container World! I have been seen 15 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 16 times and my hostname is 6efbbf18a04a.
/omd # curl flask:5000
Hello Container World! I have been seen 17 times and my hostname is bfaf0a35033c.
/omd # curl flask:5000
Hello Container World! I have been seen 18 times and my hostname is a28ccc40a7b4.
/omd # curl flask:5000
Hello Container World! I have been seen 19 times and my hostname is 6efbbf18a04a.

可以看到请求被负载到不同的容器中

整合Nginx负载均衡+Flask+Redis

  • 新的实验环境如下:

flask2
├── docker-compose.yml
├── flask
│ ├── app.py
│ └── Dockerfile
└── nginx

   └── nginx.conf
  • 文件源码
  • ngxin反向代理配置proxy_pass http://flask:5000;使用自建的bridge的域名解析服务,结合水平扩展,实现自动负载均衡

docker-compose.yml

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  # 连接flask与redis
  backend:
  # 连接nginx与flask
  frontend:

.dockerignore

.DS_Store
.idea
.git
.gitignore
.env
.dockerignore
Dockerfile
docker-compose.yaml
nginx.conf
var

flask/app.py

from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"

flask/Dockerfile

FROM python:3.9.5-slim

RUN pip install flask redis && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK=app.py REDIS_HOST=redis

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]

nginx/nginx.conf

server {
  listen  80 default_server;
  location / {
    proxy_pass http://flask:5000;
  }
}
  • 启动服务
docker-compose up -d

image-20220830151735804

访问:http://10.248.192.137:8000/

image-20220830151803537

  • 水平扩展3个flask服务
docker-compose up -d --scale flask=3
# 重启nginx服务
docker-compose restart nginx

image-20220830151922932

image-20220830152030957

image-20220830152152080

image-20220830152159588

image-20220830152207012

请求被负载均衡到3个不同的容器上

docker-compose 环境变量

实验环境

  • 基于上一节的整合Nginx负载均衡+Flask+Redis的实验源码
  • 修改docker-compose.yml,给redis增加连接密码
  • 修改app.py,增加redis连接密码

docker-compose.yml

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=abc123
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    command: redis-server --requirepass abc123
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  # 连接flask与redis
  backend:
  # 连接nginx与flask
  frontend:

app.py

from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'),
              port=6379, password=os.environ.get('REDIS_PASS'))


@app.route('/')
def hello():
    redis.incr('hits')
    return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"

重新构建镜像并启动服务

docker-compose build
docker-compose up -d

访问http://10.248.192.137:8000/

image-20220830153221316

连接redis成功

  • redis密码被固定在了docker-compose.yml中,不安全,不通用
  • 一般来说redis密码需要作为服务的启动参数传入

改造docker-compose.yml

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  # 连接flask与redis
  backend:
  # 连接nginx与flask
  frontend:
  • 将固定的redis密码abc123替换成${REDIS_PASSWORD}
  • 增加.env文件(与docker-compose.yml同级),并将.env加入到.dockerignore.gitignore中,保护配置密码
REDIS_PASSWORD=abc123
  • 校验配置
docker-compose config

image-20220830163412697

可以看到redis密码变量被替换成.env文件中的配置

  • 也可以自己指定.env的文件名,比如新建myenv文件:
REDIS_PASSWORD=ABC123
  • 在校验和启动时,带上--env-file参数
docker-compose --env-file=myenv config

image-20220830164323503

可以看到ABC123注入成功

  • 自定义配置文件启动服务
docker-compose --env-file=myenv up -d

image-20220830164445372

服务依赖和健康检查

服务依赖

  • 通过depends_on配置项
  • 例如上面的整合Nginx负载均衡+Flask+Redis示例

    • flask依赖于redis-server
    • nginx依赖于flask
    • 首先启动redis、再启动flask、最后启动nginx
  • 光有服务依赖,还需要进行健康检查

健康检查

  • 仍然使用上节课使用的flask程序
  • 先从Dockerfile的健康检查入手

flask/Dockerfile

FROM python:3.9.5-slim

RUN pip install flask redis && \
    apt-get update && \
    apt-get install -y curl && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK=app.py REDIS_HOST=redis

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=30s \
    CMD curl -f http://localhost:5000/ || exit 1

CMD ["flask", "run", "-h", "0.0.0.0"]
  • 加入HEALTHCHECK配置项
  • 容器安装curl

进到flask目录,构建新镜像

docker image build -t flask-demo .
# 创建网络(这里没有用到docker-compose故需要自己创建bridge网络)
docker network create mybridge
docker container run -d --network mybridge --env REDIS_PASS=abc123 flask-demo
docker container ls

可以看到刚启动时,健康状态为health: starting,过了3分钟,显示如下:

image-20220921111457604

健康状态为:unhealthy,这是因为没有打开Redis的原因。

docker container run -d --network mybridge --name redis redis:latest redis-server --requirepass abc123

过30s,可以看到容器健康检测状态为healthy

image-20220921112417211

接下来通过修改docker-compose来实现服务的健康检查和依赖关系
  • 删除手动创建的flask、redis容器
docker container rm -f 9c15 0842
  • 还原flask的Dockerfile,仅增加curl安装命令

flask/Dockerfile

FROM python:3.9.5-slim

RUN pip install flask redis && \
    apt-get update && \
    apt-get install -y curl && \
    groupadd -r flask && useradd -r -g flask flask && \
    mkdir /src && \
    chown -R flask:flask /src

USER flask

COPY app.py /src/app.py

WORKDIR /src

ENV FLASK=app.py REDIS_HOST=redis

EXPOSE 5000

CMD ["flask", "run", "-h", "0.0.0.0"]
  • docker-compose.yml中加入健康检测配置
version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s
    depends_on:
      redis-server:
        condition: service_healthy
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 1s
      timeout: 3s
      retries: 30
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      flask:
        condition: service_healthy
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  backend:
  frontend:
  • 运行
docker-compose up -d

image-20220921113454586

可以看到启动等待顺序为redis->flask->nginx。

docker-compose ps

image-20220921113606446

可以看到flask和redis的健康状态均为healthy

docker compose 投票 app 练习

源码地址: https://github.com/dockersamples/example-voting-app

image-20220921134408372

git clone https://github.com/dockersamples/example-voting-app
cd example-voting-app
docker-compose up -d

image-20220921134449048

启动镜像后,查看运行的镜像列表:

docker-compose ps

image-20220921134537408

  • 投票端访问5000端口
  • 结果端访问5001端口

第八章 Docker Swarm

Docker Swarm介绍

为什么不建议在生产环境中使用docker-Compose
  • 多机器如何管理?
  • 如果跨机器做scale横向扩展?
  • 容器失败退出时如何新建容器确保服务正常运行?
  • 如何确保零宕机时间?
  • 如何管理密码,Key等敏感数据?
  • 其它
容器编排 swarm

docker-swarm-intro

Swarm的基本架构

docker-swarm-arch

swarm单节点快速上手

  • 激活单节点swarm环境
# 初始化swarm集群,当前节点作为manager
docker swarm init
# 查看集群节点
docker node ls

image-20220921135338605

sdf@cluster-System-Product-Name:~$ docker swarm init
Swarm initialized: current node (zyhqfys9fw53hy3c8svszu9b0) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-1klc3amj6w2fdd9hygqxgpkjfsi1aa6cqyqveq08q8ngsd7p0p-3cqe5wnu5kbtx213728dti4ar 192.168.3.76:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

sdf@cluster-System-Product-Name:~$ docker node ls
ID                            HOSTNAME                      STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
zyhqfys9fw53hy3c8svszu9b0 *   cluster-System-Product-Name   Ready     Active         Leader           20.10.16
  • 当服务器存在多个网卡时,例如我的VMware环境有双网卡,一个网卡用于固定IP,另一个网卡用于桥接主机网络连通外网。此时需要在docker swarm init时指定IP地址,例如在我的VMware虚拟机上:
[bill@localhost ~]$ docker swarm init
Error response from daemon: could not choose an IP address to advertise since this system has multiple addresses on different interfaces (192.168.72.131 on ens33 and 192.168.73.5 on ens36) - specify one with --advertise-addr
  • 此时需要使用--advertise-addr参数指定地址来初始化swarm
[bill@localhost ~]$ docker swarm init --advertise-addr=192.168.73.5
Swarm initialized: current node (ddx18riw19jn17u01ruuv23w2) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-33vl10uefiij94z6xmcupz42m83x6u3d8t6hsg38yfwd048of7-4dobqmik5z0ijbs3su21asdwa 192.168.73.5:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

docker swarm init 背后发生了什么

主要是PKI和安全相关的自动化

  • 创建swarm集群的根证书
  • manager节点的证书
  • 其它节点加入集群需要的tokens

创建Raft数据库用于存储证书,配置,密码等数据

RAFT相关资料

离开集群

docker swarm leave [--force]

sdf@cluster-System-Product-Name:~$ docker swarm

Usage:  docker swarm COMMAND

Manage Swarm

Commands:
  ca          Display and rotate the root CA
  init        Initialize a swarm
  join        Join a swarm as a node and/or manager
  join-token  Manage join tokens
  leave       Leave the swarm
  unlock      Unlock swarm
  unlock-key  Manage the unlock key
  update      Update the swarm

Run 'docker swarm COMMAND --help' for more information on a command.

swarm单节点service

Usage: docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]

  • 例如
docker service create nginx:latest

image-20220921164907882

[bill@localhost ~]$ docker service create nginx:latest
lh433gd5z8ten0qp9hif9uijr
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged
[bill@localhost ~]$ docker service ls
ID             NAME              MODE         REPLICAS   IMAGE          PORTS
lh433gd5z8te   elegant_meitner   replicated   1/1        nginx:latest
[bill@localhost ~]$ docker service ps lh43
ID             NAME                IMAGE          NODE                    DESIRED STATE   CURRENT STATE           ERROR     PO     RTS
5tjxnpl2p07q   elegant_meitner.1   nginx:latest   localhost.localdomain   Running         Running 4 minutes ago
  • lh433gd5z8ten0qp9hif9uijr是服务的ID,非容器的ID
  • 服务横向扩展
[bill@localhost ~]$ docker service update lh43 --replicas 3
lh43
overall progress: 3 out of 3 tasks
1/3: running   [==================================================>]
2/3: running   [==================================================>]
3/3: running   [==================================================>]
verify: Service converged
[bill@localhost ~]$ docker service ps lh43
ID             NAME                IMAGE          NODE                    DESIRED STATE   CURRENT STATE            ERROR     PORTS
5tjxnpl2p07q   elegant_meitner.1   nginx:latest   localhost.localdomain   Running         Running 7 minutes ago
p5ekd0bhhj2t   elegant_meitner.2   nginx:latest   localhost.localdomain   Running         Running 44 seconds ago
ze7jhtsoctkq   elegant_meitner.3   nginx:latest   localhost.localdomain   Running         Running 44 seconds ago
[bill@localhost ~]$ docker service ls
ID             NAME              MODE         REPLICAS   IMAGE          PORTS
lh433gd5z8te   elegant_meitner   replicated   3/3        nginx:latest
  • docker service ps serviceID 是展示容器层面的信息,而docker service ls展示的是服务层面的信息
  • 删除服务
docker service rm lh43

swarm 三节点集群搭建

下面的swarm集群实验均使用我的VMware虚拟机实验,虚拟机IP及配置如下
固定IP配置名称
192.168.73.52C(i5-11300H @ 3.10GHz)4Gnode1(manager)
192.168.73.62C(i5-11300H @ 3.10GHz)4Gnode2(worker1)
192.168.73.72C(i5-11300H @ 3.10GHz)4Gnode3(worker2)
  • 加入实验集群
To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-33vl10uefiij94z6xmcupz42m83x6u3d8t6hsg38yfwd048of7-4dobqmik5z0ijbs3su21asdwa 192.168.73.5:2377
  • 多节点的环境涉及到机器之间的通信需求,所以防火墙和网络安全策略组是大家一定要考虑的问题,特别是在云上使用云主机的情况,下面这些端口记得打开 防火墙 以及 设置安全策略组

    • TCP port 2376
    • TCP port 2377
    • TCP and UDP port 7946
    • UDP port 4789
    • TCP port 4567 pxc cluster 相互通讯端口

为了简化,以上所有端口都允许节点之间自由访问就行。我的实验环境为centos7,故防火墙需要使用firewall,在三个节点上都要执行以下命令开放端口:

sudo su -
firewall-cmd --zone=public --add-port=2376/tcp --permanent
firewall-cmd --zone=public --add-port=2377/tcp --permanent
firewall-cmd --zone=public --add-port=7946/tcp --permanent
firewall-cmd --zone=public --add-port=4789/tcp --permanent
firewall-cmd --zone=public --add-port=4567/tcp --permanent
firewall-cmd --reload
exit
  • 在node1、node2上执行docker swarm join
docker swarm join --token SWMTKN-1-33vl10uefiij94z6xmcupz42m83x6u3d8t6hsg38yfwd048of7-4dobqmik5z0ijbs3su21asdwa 192.168.73.5:2377

image-20220921171520967

  • 回到manager节点上,查看节点
docker node ls

image-20220921172201638

由于两台虚拟机是由第一台虚拟机复制出来的,只修改了网卡的配置信息,hostname均相同,不利于分辨,故依次修改hostname,方便区分

hostnamectl set-hostname manager
hostnamectl set-hostname worker1
hostnamectl set-hostname worker2

再次docker node ls,结构名称比较清晰

image-20220921173510095

swarm的overlay 网络

创建使用overlay网络

image-20220921195935755

创建overlay网络

docker network create -d overlay mynet

到其他节点去查看网络

docker network ls

image-20220921200802230

可以看到overlay网络也同步到了其他节点

创建service并使用overlay网络

docker service create --network mynet --name test --replicas 2 busybox ping 8.8.8.8

查看service运行情况

[bill@manager ~]$ docker service ps test
ID             NAME      IMAGE            NODE      DESIRED STATE   CURRENT STATE            ERROR     PORTS
nx20if592mxm   test.1    busybox:latest   manager   Running         Running 26 seconds ago
6e30rufza9sg   test.2    busybox:latest   worker1   Running         Running 25 seconds ago

可以看到容器运行在两个不同的服务器上:managerworker1

进到worker1的busybox容器,查看IP和路由信息

[bill@worker1 ~]$ docker container exec -it e61e sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
    link/ether 02:42:0a:00:01:03 brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.3/24 brd 10.0.1.255 scope global eth0
       valid_lft forever preferred_lft forever
18: eth1@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.3/16 brd 172.19.255.255 scope global eth1
       valid_lft forever preferred_lft forever
/ # ip route
default via 172.19.0.1 dev eth1
10.0.1.0/24 dev eth0 scope link  src 10.0.1.3
172.19.0.0/16 dev eth1 scope link  src 172.19.0.3

swarm的overlay网络详解

  • 外部如何访问部署运行在swarm集群内的服务,即入方向流量,在swarm通过ingress来解决
  • 部署在swarm集群内的服务,如何对外进行访问,分为两块

    • 东西向流量,不同的swam节点上容器之间进行通信,swarm通过overlay网络解决
    • 南北向流量,swarm集群的容器如何对外访问,比如互联网,是通过Linux bridgeiptables NAT来解决的

  • 数据包从10.0.1.8发送到10.0.1.9时,会通过VXLAN协议进行封装,封装成从192.168.200.10发送到192.168.200.11的数据包

swarm的ingress网络

实验

创建一个service,指定网络时overlay的mynet,通过-p映射端口

使用containous/whoami是一个简单的web服务,能返回服务器的hostname,和基本的网络信息,比如IP地址

docker service create --name web --network mynet -p 8080:80 --replicas 2 containous/whoami

通过外部访问8080端口服务,会有一个负载均衡的效果

[bill@manager ~]$ curl 192.168.73.5:8080
Hostname: e8090d83ebc7
IP: 127.0.0.1
IP: 10.0.0.20
IP: 172.19.0.5
IP: 10.0.1.30
RemoteAddr: 10.0.0.2:60914
GET / HTTP/1.1
Host: 192.168.73.5:8080
User-Agent: curl/7.29.0
Accept: */*

[bill@manager ~]$ curl 192.168.73.5:8080
Hostname: 9296b889668a
IP: 127.0.0.1
IP: 10.0.1.14
IP: 172.19.0.4
IP: 10.0.0.9
RemoteAddr: 10.0.0.2:60916
GET / HTTP/1.1
Host: 192.168.73.5:8080
User-Agent: curl/7.29.0
Accept: */*
  • 通过ipvs做了负载均衡

关于这里的负载均衡

  • 这是一个stateless load balancing
  • 这是三层的负载均衡,不是四层的 LB is at OSI Layer 3 (TCP), not Layer 4 (DNS)
  • 以上两个限制可以通过Nginx或者HAProxy LB proxy解决 (https://docs.docker.com/engine/swarm/ingress/

swarm内部负载均衡和 VIP

实验环境
  • 创建mynet的overlay网络,创建一个service
docker service create --name web --network mynet --replicas 2 containous/whoami
  • 创建一个client的服务
docker service create --name client --network mynet xiaopeng163/net-box:latest ping 8.8.8.8

https://dockertips.readthedocs.io/en/latest/docker-swarm/internal_lb.html

部署多service应用

  • docker-compose用于本地开发测试
  • stack用于生产环境

手动部署

swarm stack部署多service应用

  • 先在swarm manager节点上安装一下docker-compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
  • 模拟开发环境,编写代码,构建镜像
git clone https://github.com/xiaopeng163/flask-redis
cd flask-redis
  • 环境清理
docker system prune -a -f
  • 编辑docker-compose.yml
  • image修改为我的dockerhub id: billaday
version: "3.8"

services:
  flask:
    build:
      context: ./
      dockerfile: Dockerfile
    image: billaday/flask-redis:1.0
    ports:
      - "8080:5000"
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}


  redis-server:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
  • 构建和查看镜像
docker-compose build
docker image ls

image-20220922154957153

  • 提交镜像到dockerhub
  • 先登录dockerhub
[bill@manager flask-redis]$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: billaday
Password:
Error response from daemon: Get "https://registry-1.docker.io/v2/": unauthorized: incorrect username or password
[bill@manager flask-redis]$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: billaday
Password:
WARNING! Your password will be stored unencrypted in /home/bill/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded
  • 再推送镜像
[bill@manager flask-redis]$ docker-compose push
WARNING: The REDIS_PASSWORD variable is not set. Defaulting to a blank string.
Pushing flask (billaday/flask-redis:1.0)...
The push refers to repository [docker.io/billaday/flask-redis]
ec90a017419f: Pushed
ec184b69a655: Pushed
95557c4c07af: Mounted from library/python
873602908422: Mounted from library/python
e4abe883350c: Mounted from library/python
95a02847aa85: Mounted from library/python
b45078e74ec9: Mounted from library/python
1.0: digest: sha256:7d843ab99f01f0a6fec327e35371c0dc24111370db6a59d628b9db3e8b50af3d size: 1788

image-20220922155229174

可以看到镜像已经成功推送到dockerhub上

通过stack启动服务(模拟生产环境)

env REDIS_PASSWORD=ABC123 docker stack deploy --compose-file docker-compose.yml flask-demo

使用--compose-file参数指定dockers-compose.yml文件

[bill@manager flask-redis]$ docker stack ls
NAME         SERVICES   ORCHESTRATOR
flask-demo   2          Swarm

参考资料

  1. Docker Tips (Docker笔记)


Docker Docker Swarm
Theme Jasmine by Kent Liao And Bill

本站由提供云存储服务