linux笔记 — docker 集合

  • docker的使用给我带来了极大的便利,很久就想记录一下docker基础的内容,苦于健忘,今天才补上../(ㄒoㄒ)/~~..

  • 资料来源:

    https://yeasy.gitbooks.io/docker_practice/introduction/why.html
    https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html

  • 更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    19.01.01 初始汇总
    19.01.10 补充 docker-compose
    19.01.25 补充proxy设置
    19.07.22 补充 vscode-docker
    19.09.23 补充ipv6-nat,更新内容
    19.09.25 重新整理内容
    19.10.10 补充空间清理,重新整理内容
    19.10.29 补充文件权限相关问题
    20.02.05 补充 iptables
    20.10.12 补充清理的一点问题
    21.01.07 更新 ipv6-nat 部分
    21.03.14 更新 ipv6-nat 部分
    22.03.10 更新一部分网络内容
    22.12.11 容器内抓包

导语

  • 这是一篇大杂烩,仅作为自己的遇到docker问题的记录.

Docker

  • docker起源与对linux容器的封装,提供了一个软件虚拟化环境,实质上是linux系统底层的一个进程,所需要资源比虚拟机低了一个数量级.
  • docker在各类支持的宿主机内随便迁移,保证一致的运行环境。
  • 基于层次的镜像构建,官方维护了一批高质量的基础镜像,定制非常简单。
  • 真正的一次配置,到处运行.

基础

  • 镜像Image
    • 镜像本质上就是容器运行的最小文件系统,包含必要的配置、程序、资源等等。镜像中不包含任何动态数据,镜像构建后其内容不会有任何变化.(可以说是面向对象中的类)
    • 官方镜像 ubuntu:18.04 是完整的一套 Ubuntu 18.04 最小系统的 root 文件系统.(所有类中,由系统提供的基类)
    • 镜像本身为分层构建,透过基础镜像,个性化定制边的非常容易.
  • 容器Container
    • 容器就行类的运行实例,可以创建、启动、停止、删除等.
    • 容器停止时,其对应文件系统实体,并不会删除.但docker的最佳实践要求,不要使用容器本身作为持久化数据的载体,容器本身不应该写入任何持久化数据.
    • 使用数据卷、绑定宿主机目录来保存持久化数据.
  • 仓库Repository
    • 分发镜像的地方,类比GitHub.
    • 安装docker后默认的是官方镜像仓库Docker Hub,但是速度可想而知.
    • 国内有各类的加速镜像,可替换官方镜像.
    • 用户还可以自建各类私有的镜像仓库.

安装

  • docker有两个版本.CEEE,前者是社区免费版,后者是企业版,一般的个人使用的以CE为主.
  • 下面以 ubunt16.04win10Raspberry Pi 3B为例.以官方源安装docker.

ubuntu16.04

  • 一键脚本,docker官方提供了一键安装脚本,适配14,16,18

    1
    2
    curl -fsSL https://get.docker.com -o get-docker.sh
    sudo sh get-docker.sh
  • Apt安装

    • 添加CA证书

      1
      2
      3
      4
      5
      6
      sudo apt-get update
      sudo apt-get install \
      apt-transport-https \
      ca-certificates \
      curl \
      software-properties-common
    • 添加 GPG 密钥

      1
      curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
    • 添加官方源(这里是stable,需要测试/每日构建版本,改为test/nightly)

      1
      2
      3
      4
      sudo add-apt-repository \
      "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) \
      stable"
    • 安装 Docker CE

      1
      2
      sudo apt-get update
      sudo apt-get install docker-ce
  • 其他

    • 启动docker, 执行完成,docker就加入了开机启动.

      1
      2
      sudo systemctl enable docker
      sudo systemctl start docker
    • 验证docker

      1
      docker run hello-world
    • 有输出,没有提示错误即docker正常安装.

      1
      2
      3
      4
      Unable to find image 'hello-world:latest' locally
      latest: Pulling from library/hello-world
      d1725b59e92d: Pull complete
      ...
    • 建立dokcer用户组.
      docker命令会使用socket与docker引擎通信,但这个操作通常只有root/docker才会使用,为确保安全,需要使用 docker 的用户加入 docker 用户组.
      问题

  • docker for windows 要求64位系统且必须开启Hyper-V.(前期版本是直接使用了VirtualBox)

  • stable版本

  • 之后与正常windows程序安装一致.

  • 验证方式一致.

Raspberry Pi 3B

  • 与win或者ubuntu不同,Raspberry Pi是arm架构,docker同样支持,但两者镜像不能通用.
  • 整体过程与ubuntu相同.
  • 树莓派支持镜像需要访问arm32v7

仓库

  • git一样,你可以在仓库中搜索、发布和拉取镜像.

  • 大体流程与git相似,下面只是罗列几个常见命令.

    1
    2
    3
    4
    5
    docker login #登陆默认仓库
    docker logout #登出
    docker search #搜索镜像,类似 ubuntu 不带前缀的时docker官方维护,username/imagename 为用户镜像.
    docker pull #拉取镜像
    docker push #推送镜像

镜像

  • 拉取镜像

    1
    docker pull ubuntu:18.04
  • 运行镜像

    1
    2
    3
    4
    docker run -it \ # -it 启动交护操作 启动终端
    --rm \ # --rm 容器停止后,自动删除.
    ubuntu:18.04 \ # 启动ubuntu:18.04
    bash # 容器启动后执行 启动bash命令

    exit 退出容器.

  • 列出镜像

    1
    docker image ls

    结果类似

    1
    2
    3
    REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
    centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
    redis alpine 501ad78535f0 3 weeks ago 21.03 MB
  • 删除镜像

    1
    docker image rm [选项] <镜像1> [<镜像2> ...]

    可以是 ID镜像名或者摘要.

docker pull proxy

  • docker pull 操作在全局设置 http_proxy/https_proxy 情况下依旧不能下载.

  • 参考

    https://docs.docker.com/config/daemon/systemd/

  • docker.service(官方推荐),在我的测试中最终生效的.

    • 创建目录

      1
      sudo mkdir -p /etc/systemd/system/docker.service.d
    • 创建并写入http_proxy.

      1
      2
      3
      4
      5
      vim /etc/systemd/system/docker.service.d/http-proxy.conf

      # 写入
      [Service]
      Environment="HTTP_PROXY=http://proxy.example.com:80/"
    • 创建并写入https_proxy.

      1
      2
      3
      4
      5
      vim /etc/systemd/system/docker.service.d/https-proxy.conf

      #写入
      [Service]
      Environment="HTTPS_PROXY=https://proxy.example.com:443/"
    • 如果存在不需要通过proxy访问的,可以在对应的文件,增加 NO_PROXY 字段,以 http_proxy.conf 为例.

      1
      2
      [Service]
      Environment="HTTP_PROXY=http://proxy.example.com:80/" "NO_PROXY=localhost,127.0.0.1,docker-registry.somecorporation.com"
    • 应用更改并重启

      1
      2
      sudo systemctl daemon-reload #应用更改
      sudo systemctl restart docker #重启docker
    • 查看结果

      1
      systemctl show --property=Environment docker
  • 也有直接写入 /lib/systemd/system/docker.service 的,但是不推荐.且以第一种方式会覆盖 docker.service 的对应字段.

定制镜像(Dockerfile 指令)

  • 示例

    1
    2
    3
    4
    5
    FROM node:8.4
    COPY . /app
    WORKDIR /app
    RUN npm install --registry=https://registry.npm.taobao.org
    EXPOSE 3000
  • 说明

    • FROM: docker镜像是分层构建,FROM就是构建镜像的上一层.
    • COPY: 拷贝当前目录下所有文件到 镜像的/app文件夹
    • WORKDIR: 将接下来的路径设为/app文件夹.
    • RUN: 执行 以下命令,注意 ,一个RUN命令就是一个镜像层,一般使用 \ 将命令链接起来,避免过多层级.
    • EXPOSE: 暴露容器3000端口.
  • 其他详细指令参考Dockerfile 指令详解

容器

  • 运行

    1
    docker run -t -i ubuntu:18.04 /bin/bash

    -i -t 启动交互式终端,容器启动后执行 bash.
    当image不存在时,docker会搜索默认的docker仓库.

    1
    docker run -d ubuntu:18.04

    -d参数,容器在后台运行,所有容器的输出需要log命令获取

    1
    docker container logs [id / name]
  • 进入
    进入正在运行的容器有attachexec两个命令,前者会导致容器终止,推荐使用exec

    1
    docker exec -i -t [id/name] bash #示例
  • 停止
    docker container stop [id/name],终止后可通过docker container start重新启动.

  • 删除
    docker container rm [id/name]来删除已停止运行容器.
    对于正在运行容器需要添加 -f 参数.
    如已停止容器较多, docker container prune可一次性删除所有处于终止状态的容器.

日常使用

Docker Compose

  • Docker Compose 是 Docker 三剑客之一,负责快速的部署分布式应用.本质上是通过python调用docker的API,实现快速的部署和管理.

  • Docker Compose 有两个重要概念

    • service: 默认是一个应用的容器.实际上也可以是若干组相同的镜像.
    • project: 由一组关联的服务(容器)组成的完整业务单元.在docker-compose.yml文件中定义.
  • 通过编写 docker-compose.yml 可一次性部署同一业务下所属的N个容器,进一步,通过python脚本,实现全自动的部署和管理.

安装Docker Compose

  • docker for windows 和 docker for mac ,已经自带了 Docker Compose.

  • ubuntu 建议直接由官方二进制文件安装.

    1
    2
    sudo curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
    sudo chmod +x /usr/local/bin/docker-compose
  • Raspberry Pi等 arm架构的计算机,建议使用 pip方式安装.

    1
    sudo pip install -U docker-compose
  • 当然Docker Compose 是一个python应用,也可以直接跑在容器内.

    1
    2
    curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
    chmod +x /usr/local/bin/docker-compose

使用(以caddy镜像使用为例)

  • caddy是一个go语言编写的web服务端.使用户可以非常方便的搭建一个网站,

    • HTTP/2 全自动支持HTTP/2协议.
    • HTTPS Caddy 使用 Let’s Encrypt 全自动申请+续签.
    • IPv6 完整支持
    • WebSockets 对WebSockets有很好的支持.
    • go语言编写 没有依赖,非常方便部署。
  • 这里我们使用 dockerhub 上最流行的 caddy镜像 abiosoft/caddy.

  • 使用caddy 需要提供 Caddyfile 的配置文件, abiosoft/caddy 的默认位置在 /etc/Caddyfile.我们需要将宿主机的Caddyfile 挂载到镜像的/etc/Caddyfile .
    这里我提供的 Caddyfile 文件示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    http://www.xxxx.com:80 {
    timeouts none
    redir https://www.xxxx.com
    }
    https://www.xxxx.com {
    root /www
    timeouts none
    gzip
    tls [email protected]
    }
    http://xxxx.com:80 {
    timeouts none
    redir https://xxxx.com
    }
    https://xxxx.com {
    root /www
    timeouts none
    gzip
    tls [email protected]
    }
  • 同时我们需要将镜像 /root/.caddy(证书存放地址) ,挂载到宿主机目录.

    链接镜像80 443端口到宿主机 80 443端口.

  • 使用docker run 命令

    1
    2
    3
    4
    5
    6
    docker run -i -t  \
    -v /root/Caddyfile:/etc/Caddyfile \
    -v /root/www:/www \
    -v /root/.caddy:/root/.caddy \
    -p 80:80 -p 443:443 \
    abiosoft/caddy

    在交护终端选择 y .同意 Let’s Encrypt 的协议.
    访问 https://www.xxxx.com 即可.

  • Docker Compose

    • 编写docker-compose.yml

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      version: '2'
      services:
      caddy:
      container_name: caddy
      image: abiosoft/caddy
      volumes:
      - "/root/Caddyfile:/etc/Caddyfile"
      - "/root/www:/www"
      - "/root/.caddy:/root/.caddy"
      environment:
      ACME_AGREE: "true"
      ports:
      - 80:80
      - 443:443
      network_mode: "host"
      restart: always`
    • 这里 特别注意 环境变量要附加 ACME_AGREE: "true",等同于 同意Let’s Encrypt 的协议.

    • docker-compose up 启动项目.
      访问 https://www.xxxx.com 即可

    • 其他命令

      • docker-compose ps 列出运行的服务
      • docker-compose stop 停止某个服务
      • docker-compose start 启动某个暂停的服务
    • ps: restart: always 对应的service即开机自启

  • Compose 命令说明

  • Compose 模板文件

管理插件

  • 还是推荐 vscode-docker.

  • 微软官方出品,值得信赖.日常的镜像/容器/数据卷/网络栈管理,支持接入dockerhub,非常方便.

  • 话说现在 vscode 已经支持直接ssh登录镜像进行开发了,一种语言一个docker,顶多挂载几个文件夹.(待补充)

  • 直接上图了

    • 1
    • 2

网络

  • 提及 docker 的网络又是一个深坑.至今还在里面.这里暂时只提及ipv6.

ipv6

  • 参考 Enabling IPv6 Functionality for Docker & Docker Compose , IPv6 with Docker

  • docker 对ipv6 的支持是在2017年以后.

  • 终究还是不习惯 ipv6 的方式.

启用ipv6
  • 首先你的宿主机要有至少相对固定的 ipv6 地址.每次ipv6 地址变化改配置文件很麻烦..

  • 编辑 /etc/docker/daemon.json添加

    1
    2
    "ipv6": true,
    "fixed-cidr-v6" : "2001:db8:1::/64"
    • 2001:db8:1::/64要分配给docker的子网ip段.
  • 重启docker .

    1
    systemctl restart docker
  • 同时也需要添加路由表和转发.

    1
    2
    3
    ip -6 route add 2001:db8:1::/64 dev docker0
    sysctl net.ipv6.conf.default.forwarding=1
    sysctl net.ipv6.conf.all.forwarding=1
  • ifconfig 查看 docker0 网卡的ipv6 地址.

ipv6-nat
  • ipv6 问题

    • 国内的 ipv6 地址经常变化,该配置文件太麻烦了.
    • 容器独立的ipv6地址意味着完全没有nat,主机的v6防火墙形同虚设.
    • 虽然ipv6下nat没有什么意义了,但就安全而言,容器暂时没有较好的防火墙设置.v6端口全开终究不是什么好事.
  • docker 官方暂时不会支持ipv6-nat了,这里来自 robbertkl/docker-ipv6nat

  • 启用一个-network = host 的本地容器处理ipv6 的nat.但是只支持 bridge 类型的网桥.包括 docker 0(默认bridge类型) 和用户自定义的 bridge 网桥.

  • 最方便的是在 docker-compose 中随文件自定义网桥.

配置nat
  • 前置条件

    • 能够转发的ip地址范围: fc00::/7,用户自定义网桥需要注意.

    • 宿主机需要加载 ip6_tables 模块.如果 lsmod 没有发现,echo "ip6_tables" >> /etc/modules 加载并重启一下.

    • 启用转发等

      1
      2
      sysctl net.ipv6.conf.default.forwarding=1
      sysctl net.ipv6.conf.all.forwarding=1
  • 启动 docker-ipv6nat

    1
    docker run -d --restart=always -v /var/run/docker.sock:/var/run/docker.sock:ro --privileged --net=host robbertkl/ipv6nat
  • 容器使用

    • docker 0 网桥: 将上文中 fixed-cidr-v6 的ip地址范围修改为 fc00::/7 内,例如 fd00:dead:beef::/48.这样 docker 0 网桥可以正常访问ipv6了.

    • 用户自行创建一个网桥,也非常简单.

      1
      docker network create --ipv6 --subnet=fd00:dead:beef::/48 mynetwork

      容器启动时使用 --net=mynetwork 链接到 mynetwork.

托管到 docker-compose

前置条件相同

在 docker-compose 中直接新建一个受支持的网桥,容器分配到这个网桥下即可.

因为需要定义 enable_ipv6 所以创建网桥的 docker-compose 的版本只能限制为 2.1.

docker-compose version 3.x 只能曲线救国,即由 2.x 创建网桥,其他 3.x 通过 external 引用到此网桥下.

为了方便管理,这里将创建网桥和启动 robbertkl/ipv6nat 放在一起了.

  • 为了使网桥正常创建,还需要将网桥分配给那个凑数容器..
  • 与直接命令参数相同,需要 privileged: true 提权.
  • 系统创建的网桥名称是 文件夹名_nat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
version: '2.1'

networks:
nat:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: fd00:dead:beef::/48

services:
driver:
image: robbertkl/ipv6nat
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /lib/modules:/lib/modules:ro

privileged: true
network_mode: "host"

alpine:
container_name: alpine
image: alpine:latest
networks:
- nat

容器内引用网桥(假设是 ipv6_nat)

1
2
3
4
5
6
7
8
9
10
11
version: '3.8'
services:
debian:
image: debian
networks:
- ipv6-nat

networks:
ipv6-nat:
external:
name: ipv6_nat

这样启动容器后,ifconfig 就可以看到分配的 v6 地址.ping -6 也能正常ping通.

其他

有一个奇葩需求: 多个容器共享同一个网络栈.

  • 这个需求来源大概是一些软件只能监听 127.0.0.1,需要其映射到局域网

docker-compose:

  • network_mode: service:[service name]

空间清理

  • 参考如何清理Docker占用的磁盘空间?

  • docker system df 类似linux的df命令,查看docker的磁盘空间占用.

    1
    2
    3
    4
    5
    6
    # Docker 镜像占用了7.2GB磁盘 Docker 容器占用了104.8MB磁盘 Docker 数据卷占用了1.4GB磁盘
    TYPE TOTAL ACTIVE SIZE RECLAIMABLE
    Images 147 36 7.204GB 3.887GB (53%)
    Containers 37 10 104.8MB 102.6MB (97%)
    Local Volumes 3 3 1.421GB 0B (0%)
    Build Cache 0B 0B
  • docker system prune

    • 删除关闭的容器
    • 没有使用的数据卷
    • 没有使用网络,
    • 无 tag 的镜像
  • docker system prune -a (慎用)

    • 同上
    • 没有使用的docker镜像
  • 有时docker的日志会浪费大量空间. Debian系上 Docker 的所有相关文件(镜像 容器等)都保存在/var/lib/docker/目录下.sudo du -hs /var/lib/docker/ 排查过大的文件夹.

  • 以上治标不治本.需要限制容器日志的文件大小.幸好在docker-compose中非常简单.在需要限制的 services 下添加即可.(需要指明 2.1 版本)

    1
    2
    3
    4
    logging:
    driver: "json-file"
    options:
    max-size: "50m"
  • 类似命令

    1
    2
    3
    4
    docker container prune # 清理所有 stop 容器
    docker image prune # 清理无用中间镜像
    docker volume prune # 清理未使用 volume
    docker network prune # 清理未使用网络

清理问题

有时会有无法清理的容器.

直接到 /var/lib/docker/container rm -rf..

文件/文件夹权限问题

  • 宿主机挂载文件夹到容器,非常方便容器的数据管理.

  • 不过容器创建的文件都是root权限,一直以来都未太在意,直到折腾 aria2 docker.

  • 下载的文件除了读取连删除都没法搞.

  • 容器一般默认运行在 root 用户下,创建文件自然是 root 权限.

  • 为了解决文件权限文件,我们需要指定容器运行在宿主机系统的普通用户下.

  • 背景

    • linux 目前通过 Linux user namespace 对进程进行安全隔离,一个普通用户在系统层面是通过 用户和用户组 来管理权限.而在内核,内核是通过 uid 和 gid 来管理权限,而不是用户名/用户组.
    • 容器与宿主机共用内核,我们无法直接指定容器运行用户.但是可以指定 uid 和 gid.这样解决方案就来了.
  • 解决

    • 宿主机直接使用root用户,一切权限都是浮云,但是很不安全.
    • 或者 通过获取宿主机普通用户的 uid 和 gid ,指定容器以相同的 uid 和 gid 运行,虽然在容器内用 id 命令可能无法提示正确的用户名,但是实际在宿主机的文件已经可以正常读取了.
  • 方法

    • 使用 id 获取宿主机普通用户的 uid 和 gid ,一般默认的普通用户是 1000:1000.
    • 在docker run 时 -u 指定运行时的 uid 和 gid . 例: docker run -u=1000:1000
    • docker-compose 文件中也非常简单,在 services 下增加 user: 1000:1000
  • 如此 aria2 下载的文件正常了,web UI 还在折腾中.

docker 内使用 iptables

  • docker 内使用 iptables 有两个问题

    • docker 基础镜像一般都是最小镜像,不带 iptables.
    • 权限不足.
  • 第一个问题自编译镜像时加上 iptables 即可,为了保险把 ip6tables 也一并安装.以 alpine 为例: apk add --no-cache iptables ip6tables

  • 权限问题解释有点麻烦, docker 内默认运行的 root 用户,但这个 root 用户只相当与宿主机的一个普通用户.但是docker 可以指定 privileged 参数,赋予容器真正的 root 权限.

  • docker 启动时 docker run --privileged xxx

  • docker-compose 在 server 中指定 privileged: true

  • 严重警告: 赋予容器 root 权限后,必须非常小心.

容器内抓包

参考

  • https://nutao.gitee.io/2020/04/06/Docker-%E5%AE%B9%E5%99%A8%E6%8A%93%E5%8C%85/
  • https://mozillazg.com/2020/04/use-tcpdump-for-a-container-but-outside-container.html

tcpdump 在 host 调试时候相当好用,但对容器无能为力. -> nsenter

  • 详细原理就不细究了,粗略来说 容器在与 host 不同的网络命名空间,nsenter 能临时将 shell 的网络命名空间切换到容器所在的地方.

获取容器 id ,切换 ,tcpdump 搞定

1
2
3
4
5
docker inspect --format "{{.State.Pid}}" <container id/name>
# -n 表示切换网络命名空间,-t 指定的 pid 为步骤 1 获取的容器的 root pid:
nsenter -n -t <container root id>
# ifconfig 看看网卡名
tcpdump -i eth0 tcp and port 80 -vvv

其他

docker compose 突然提示: network.external.name is deprecated in favor of network.name

1
2
3
default:
name: ddev_default
external: true

要换成下面的形式

1
2
3
default:
name: ddev_default
external: true

结束