使用Docker快速搭建Nginx PHP-FPM环境

背景

在上一周笔者对docker了解,仅限于这样认知:它能替代虚拟机,并且比虚拟机更省资源。

在老师和同事的感染下,感觉不学习docker好像就不能在IT圈混一样,于是,开始涌入Docker的大潮中。但万事开头难,听了同事的推荐,看了宁皓网的基础的视频之后感觉仅是对基本的命令进行了了解。但是真拿出来用还是不够用的,于是开始搜罗更

重资料,学习搭建LNMP环境。
终于功夫不负有心人,在今天终于是实验成功了,特此写下这篇笔记,为后来人提供一个示例。

实战

1、下载nginx官方镜像和php-fpm镜像

docker pull nginx
docker pull bitnami/php-fpm

未分类

笔者未进行更改docker源,依然是官方源。
当然,你可以使用中国源。

2、使用php-fpm镜像开启php-fpm应用容器

docker run -d --name myFpm -p  -v /var/www/html:/usr/share/nginx/html bitnami/php-fpm
  • -d : 该参数为后台运行之意
  • -v : 指定宿主机与容器的映射关系。/var/www/html为宿主机的项目目录(自定义的),/usr/share/nginx/html为nginx服务器项目默认的路径。

3、使用nginx镜像开启nginx应用容器

docker run -d --name myNginx -p 8080:80 -v /var/www/html:/usr/share/nginx/html nginx
  • -p : 该参数设置端口对应的关系。所有访问宿主机8080端口的URL会转发到nginx容器的80端口。

4、查看对应的IP信息

  • 首先查看是否启动成功
docker ps -a

未分类

可以看到,上述在STATUS一栏中显示UP,其含义为正在运行。

  • 查看IP信息
docker inspect myFpm | grep "IPAddress"

未分类

5、修改nginx的相关配置

在容器中是没有vim命令的,所以不能在容器中直接修改配置文件。所以我们必须通过变通的方式去解决这个问题,否则只能在每个容器中安装vim。

  • 首先登录到对应的容器中,查看配置信息路径,这在之后修改时会用到。
docker exec -it myNginx /bin/bash

未分类

  • -i : –interactive,交互模式。
  • -t : –tty,开启一个伪终端。
  • /bin/bash : 必须写,否则会报错。这是开始伪终端时,进入bash界面,也就是命令行界面。

  • 查看对应的配置文件位置

/etc/nginx/conf.d/default.conf

未分类

  • 退出命令行,不要使用exit,因为exit会让容器停止。这里使用ctrl + p + q来退出容器。

  • 使用专用的复制命令将配置文件复制到宿主机,然后在宿主机进行编辑(这就是变通的方法)

docker cp myNginx:/etc/nginx/conf.d/default.conf ./default.conf

这里用到了上一步查询到的配置文件路径信息

  • 在宿主机修改配置文件的php部分,内容如下:
location ~ .php$ {
   fastcgi_pass   172.17.0.2:9000;
   fastcgi_index  index.php;
   fastcgi_param  SCRIPT_FILENAME  /usr/share/nginx/html$fastcgi_script_name;
   fastcgi_param  SCRIPT_NAME      $fastcgi_script_name;
   include        fastcgi_params;
}
  • 再次使用复制命令将其复制到容器中,然后再次进入容器中,将nginx配置文件重新载入
docker cp ./default.conf myNginx:/etc/myNginx:/etc/nginx/conf.d/default.conf
  • 进入到nginx容器中重新载入配置文件
docker exec -it myNginx /bin/bash
service nginx reload

成功了

我看了一下,用到的模块还都有。

未分类

未分类

未分类

未分类

docker容器网络下UDP协议的一个问题

  • 最近在工作中遇到一个 docker 容器下 UDP 协议网络不通的问题,困扰了很久,也比较有意思,所以想写下来和大家分享。

  • 我们有个应用是 UDP 协议的,部署上去发现无法工作,但是换成 TCP 协议是可以的(应用同时支持 UDP、TCP 协议,切换成 TCP 模式发现一切正常)。虽然换成 TCP 能解决问题,但是我们还是想知道到底 UDP 协议在网络模式下为什么会出现这个问题,以防止后面其他 UDP 应用会有异常。

  • 这个问题抽象出来是这样的:如果有 UDP 服务运行在主机上(或者运行在网络模型为 Host 的容器里),并且监听在 0.0.0.0 地址(也就是所有的 ip 地址),从运行在 docker bridge 网络的容器运行客户端访问服务,两者通信有问题。

  • 注意以上的的限制条件,通过测试,我们发现下来几种情况都是正常的:

    • 使用 TCP 协议没有这个问题,这个已经说过了
    • 如果 UDP 服务器监听在 eth0 IP 地址上也不会出现这个问题
    • 并不是所有的应用都有这个问题,我们的 DNS(dnsmasq + kubeDNS) 也是同样的部署方式,但是功能都正常
  • 这个问题在 docker 上也有 issue 记录:https://github.com/moby/moby/issues/15127,但是目前并没有合理的解决方案。

这篇文章就分析一下出现这个问题的原因,希望给同样遇到这个问题的读者提供些帮助。

问题重现

这个问题很容易重现,我的实验是在 ubuntu16.04 下用 netcat 命令完成的,其他系统应该类似。在主机上通过 nc 监听 56789 端口,然后在容器里使用 nc 发数据。第一个报文是能发送出去的,但是以后的报文虽然在网络上能看到,但是对方无法接收。

在主机上运行 nc UDP 服务器(-u 表示 UDP 协议,-l 表示监听的端口)

$ nc -ul 56789

然后启动一个容器,运行客户端:

$ docker run -it apline sh
/ # nc -u 172.16.13.13 56789

nc 的通信是双方的,不管对方输入什么字符,回车后对方就能立即收到。但是在这个模式下,客户端第一次输入对方能够收到,后续的报文对方都收不到。

在这个实验中,容器使用的是 docker 的默认网络,容器的 ip 是 172.17.0.3,通过 veth pair(图中没有显示)连接到虚拟网桥 docker0(ip 地址为 172.17.0.1),主机本身的网络为 eth0,其 ip 地址为 172.16.13.13。

 172.17.0.3
+----------+
|   eth0   |
+----+-----+
     |
     |
     |
     |
+----+-----+          +----------+
| docker0  |          |  eth0    |
+----------+          +----------+
172.17.0.1            172.16.13.13

tcpdump 抓包

遇到这种疑难杂症,第一个想到的抓包,我们需要在 docker0 上抓包,因为这是报文必经过的地方。通过过滤容器的 ip 地址,很容器找到感兴趣的报文:

$ tcpdump -i docker0 -nn host 172.17.0.3

为了模拟多数应用一问一答的通信方式,我们一共发送三个报文,并用 tcpdump 抓取 docker0 接口上的报文:

  • 客户端先向服务器端发送 hello 字符串
  • 服务器端回复 world
  • 客户端继续发送 hi 消息

抓包的结果如下,可以发现第一个报文发送出去没有任何问题(因为 UDP 是没有 ACK 报文的,所以客户端无法知道对方有没有收到,这里说的没有问题是值没有对应的 ICMP 报文),但是第二个报文从服务端发送的报文,对方会返回一个 ICMP 告诉端口 38908 不可达;第三个报文从客户端发送的报文也是如此。以后的报文情况类似,双方再也无法进行通信了。

11:20:43.973286 IP 172.17.0.3.38908 > 172.16.13.13.56789: UDP, length 6
11:20:50.102018 IP 172.17.0.1.56789 > 172.17.0.3.38908: UDP, length 6
11:20:50.102129 IP 172.17.0.3 > 172.17.0.1: ICMP 172.17.0.3 udp port 38908 unreachable, length 42
11:20:54.503198 IP 172.17.0.3.38908 > 172.16.13.13.56789: UDP, length 3
11:20:54.503242 IP 172.16.13.13 > 172.17.0.3: ICMP 172.16.13.13 udp port 56789 unreachable, length 39

而此时主机上 UDP nc 服务器并没有退出,使用 lsof -i :56789 可能看到它仍然在监听着该端口。

问题原因

从网络报文的分析中可以看到服务端返回的报文源地址不是我们预想的 eth0 地址,而是 docker0 的地址,而客户端直接认为该报文是非法的,返回了 ICMP 的报文给对方。

那么问题的原因也可以分为两个部分:

  • 为什么应答报文源地址是错误的?
  • 既然 UDP 是无状态的,内核怎么判断源地址不正确呢?

主机多网络接口 UDP 源地址选择问题

第一个问题的关键词是:UDP 和多网络接口。因为如果主机上只有一个网络接口,发出去的报文源地址一定不会有错;而我们也测试过 TCP 协议是能够处理这个问题的。

通过搜索,发现这确实是个已知的问题。在 UNP() 这本书中,已经描述过这个问题,下面是对应的内容:

未分类

这个问题可以归结为一句话:UDP 在多网卡的情况下,可能会发生服务器端源地址不对的情况,这是内核选路的结果。 为什么 UDP 和 TCP 有不同的选路逻辑呢?因为 UDP 是无状态的协议,内核不会保存连接双方的信息,因此每次发送的报文都认为是独立的,socket 层每次发送报文默认情况不会指明要使用的源地址,只是说明对方地址。因此,内核会为要发出去的报文选择一个 ip,这通常都是报文路由要经过的设备 ip 地址。

有了这个原因,还要解释一下问题:为什么 dnsmasq 服务没有这个问题呢?因此我使用 strace 工具抓取了 dnsmasq 和出问题应用的网络 socket 系统调用,来查看它们两个到底有什么区别。

dnsmasq 在启动阶段监听了 UDP 和 TCP 的 54 端口(因为是在本地机器上测试的,为了防止和本地 DNS 监听的 DNS端口冲突,我选择了 54 而不是标准的 53 端口):

socket(PF_INET, SOCK_DGRAM, IPPROTO_IP) = 4
setsockopt(4, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(4, {sa_family=AF_INET, sin_port=htons(54), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
setsockopt(4, SOL_IP, IP_PKTINFO, [1], 4) = 0

socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 5
setsockopt(5, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(5, {sa_family=AF_INET, sin_port=htons(54), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(5, 5)                            = 0

比起 TCP,UDP 部分少了 listen,但是多个 setsockopt(4, SOL_IP, IP_PKTINFO, [1], 4) 这句。到底这两点和我们的问题是否有关,先暂时放着,继续看传输报文的部分。

dnsmasq 收包和发包的系统调用,直接使用 recvmsg 和 sendmsg 系统调用:

recvmsg(4, {msg_name(16)={sa_family=AF_INET, sin_port=htons(52072), sin_addr=inet_addr("10.111.59.4")}, msg_iov(1)=[{"315n1 11fterminal19-05u50163"..., 4096}], msg_controllen=32, {cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=, ...}, msg_flags=0}, 0) = 67

sendmsg(4, {msg_name(16)={sa_family=AF_INET, sin_port=htons(52072), sin_addr=inet_addr("10.111.59.4")}, msg_iov(1)=[{"315n201200111fterminal19-05u50163"..., 83}], msg_controllen=28, {cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=, ...}, msg_flags=0}, 0) = 83

而出问题的应用 strace 结果如下:

[pid   477] socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP) = 124
[pid   477] setsockopt(124, SOL_IPV6, IPV6_V6ONLY, [0], 4) = 0
[pid   477] setsockopt(124, SOL_IPV6, IPV6_MULTICAST_HOPS, [1], 4) = 0
[pid   477] bind(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 0

[pid   477] getsockname(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
[pid   477] getsockname(124, {sa_family=AF_INET6, sin6_port=htons(6088), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0

[pid   477] recvfrom(124, "j20124502012422413215242321n243160f0n24142222524224"..., 2048, 0, {sa_family=AF_INET6, sin6_port=htons(38790), inet_pton(AF_INET6, "::ffff:172.17.0.3", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 168

[pid   477] sendto(124, "k20222102022r2403215241321v2435333TDH2442202024032"..., 533, 0, {sa_family=AF_INET6, sin6_port=htons(38790), inet_pton(AF_INET6, "::ffff:172.17.0.3", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = 533

其对应的逻辑是这样的:使用 ipv6 绑定在 0.0.0.0 和 6088 端口,调用 getsockname 获取当前 socket 绑定的端口信息,数据传输过程使用的是 recvfrom 和 sendto。

对比下来,两者的不同有几点:

  • 后者使用的是 ipv6,而前者是 ipv4
  • 后者使用 recvfrom 和 sendto 传输数据,而前者是 sendmsg 和 recvmsg
  • 前者有调用 setsockopt 设置 IP_PKTINFO 的值,而后者没有

因为是在传输数据的时候出错的,因此第一个疑点是 sendmsg 和 sendto 的某些区别导致选择源地址有不同,通过 man sendto 可以知道 sendmsg 包含了更多的控制信息在 msghdr。一个合理的猜测是 msghdr 中包含了内核选择源地址的信息!

通过查找,发现 IP_PKTINFO 这个选项就是让内核在 socket 中保存 IP 报文的信息,当然也包括了报文的源地址和目的地址。IP_PKTINFO 和 msghdr 的关系可以在这个 stackoverflow 中找到:

https://stackoverflow.com/questions/3062205/setting-the-source-ip-for-a-udp-socket。

而 man 7 ip 文档中也说明了 IP_PKTINFO 是怎么控制源地址选择的:

IP_PKTINFO (since Linux 2.2)
              Pass  an  IP_PKTINFO  ancillary message that contains a pktinfo structure that supplies some information about the incoming packet.  This only works for datagram ori‐
              ented sockets.  The argument is a flag that tells the socket whether the IP_PKTINFO message should be passed or not.  The message itself can only be sent/retrieved as
              control message with a packet using recvmsg(2) or sendmsg(2).

                  struct in_pktinfo {
                      unsigned int   ipi_ifindex;  /* Interface index */
                      struct in_addr ipi_spec_dst; /* Local address */
                      struct in_addr ipi_addr;     /* Header Destination
                                                      address */
                  };

              ipi_ifindex  is the unique index of the interface the packet was received on.  ipi_spec_dst is the local address of the packet and ipi_addr is the destination address
              in the packet header.  If IP_PKTINFO is passed to sendmsg(2) and ipi_spec_dst is not zero, then it is used as the local source address for the  routing  table  lookup
              and  for  setting up IP source route options.  When ipi_ifindex is not zero, the primary local address of the interface specified by the index overwrites ipi_spec_dst
              for the routing table lookup.

如果 ipi_spec_dst 和 ipi_ifindex 不为空,它们都能作为源地址选择的依据,而不是让内核通过路由决定。

也就是说,通过设置 IP_PKTINFO socket 选项为 1,然后使用 recvmsg 和 sendmsg 传输数据就能保证源地址选择符合我们的期望。这也是 dnsmasq 使用的方案,而出问题的应用是因为使用了默认的 recvfrom 和 sendto。

关于 UDP 连接的疑惑

另外一个疑惑是:为什么内核会把源地址和之前不同的报文丢弃?认为它是非法的?因为我们前面已经说过,UDP 协议是无连接的,默认情况下 socket 也不会保存双方连接的信息。即使服务端发送报文的源地址有误,只要对方能正常接收并处理,也不会导致网络不通。

因为 conntrack,内核的 netfilter 模块会保存连接的状态,并作为防火墙设置的依据。它保存的 UDP 连接,只是简单记录了主机上本地 ip 和端口,和对端 ip 和端口,并不会保存更多的内容。

可以参考 intables info 网站的文章:http://www.iptables.info/en/connection-state.html#UDPCONNECTIONS。

在找到根源之前,我们曾经尝试过用 SNAT 来修改服务端应答报文的源地址,期望能够修复该问题。但是却发现这种方法行不通,为什么呢?

因为 SNAT 是在 netfilter 最后做的,在之前 netfilter 的 conntrack 因为不认识该 connection,直接丢弃了,所以即使添加了 SNAT 也是无法工作的。

那能不能把 conntrack 功能去掉呢?比如解决方案:

iptables -I OUTPUT -t raw -p udp --sport 5060 -j CT --notrack
iptables -I PREROUTING -t raw -p udp --dport 5060 -j CT --notrack

答案也是否定的,因为 NAT 需要 conntrack 来做翻译工作,如果去掉 conntrack 等于 SNAT 完全没用。

解决方案

知道了问题的原因,解决方案也就很容易找到。

使用 TCP 协议

如果服务端和客户端使用 TCP 协议进行通信,它们之间的网络是正常的。

$ nc -l 56789

监听在特定端口

使用 nc 启动一个 udp 服务器,监听在 eth0 上:

➜  ~ nc -ul 172.16.13.13 56789

nc 可以跟两个参数,分别代表 ip 和 端口,表示服务端监听在某个特定 ip 上。如果接收到的报文目的地址不是 172.16.13.13,也会被内核直接丢弃。

这种情况下,服务端和客户端也能正常通信。

改动应用程序实现

修改应用程序的逻辑,在 UDP socket 上设置 IP_PKTIFO,并通过 recvmsg 和 sendmsg 函数传输数据。

导出并导入docker镜像(适用于保存传输镜像)

假如由于网络原因,需要在一台无网络的电脑上运行镜像,那么docker是支持的。

最关键的是,学会使用docker的 save 命令。

可以参考:https://docs.docker.com/engine/reference/commandline/save/

你需要做的主要有3步骤:

  • 先从一个有网络的电脑下载docker镜像
sudo docker pull ubuntu
  • 保存镜像到本地文件
sudo docker save -o ubuntu_image.docker ubuntu
  • 把镜像拷贝到无网络的电脑,然后通过docker加载镜像即可。
sudo docker load ubuntu_image.docke

Docker镜像的存储机制

近几年 Docker 风靡技术圈,不少从业人员都或多或少使用过,也了解如何通过 Dockerfile 构建镜像,从远程镜像仓库拉取自己所需镜像,推送构建好的镜像至远程仓库,根据镜像运行容器等。这个过程十分简单,只需执行 docker build、docker pull、docker push、docker run 等操作即可。但大家是否想过镜像在本地到底是如何存储的?容器又是如何根据镜像启动的?推送镜像至远程镜像仓库时,服务器又是如何存储的呢?下面我们就来简单聊一聊。

Docker 镜像本地存储机制及容器启动原理

  • Docker 镜像不是一个单一的文件,而是有多层构成。我们可通过 docker images 获取本地的镜像列表及对应的元信息, 接着可通过docker history 查看某个镜像各层内容及对应大小,每层对应着 Dockerfile 中的一条指令。Docker 镜像默认存储在 /var/lib/docker/中,可通过 DOCKER_OPTS 或者 docker daemon 运行时指定 –graph= 或 -g 指定。

  • Docker 使用存储驱动来管理镜像每层内容及可读写的容器层,存储驱动有 devicemapper、aufs、overlay、overlay2、btrfs、zfs 等,不同的存储驱动实现方式有差异,镜像组织形式可能也稍有不同,但都采用栈式存储,并采用 Copy-on-Write(CoW) 策略。且存储驱动采用热插拔架构,可动态调整。那么,存储驱动那么多,该如何选择合适的呢?大致可从以下几方面考虑:

    • 若内核支持多种存储驱动,且没有显式配置,Docker 会根据它内部设置的优先级来选择。优先级为 aufs > btrfs/zfs > overlay2 > overlay > devicemapper。若使用 devicemapper 的话,在生产环境,一定要选择 direct-lvm, loopback-lvm 性能非常差。

    • 选择会受限于 Docker 版本、操作系统、系统版本等。例如,aufs 只能用于 Ubuntu 或 Debian 系统,btrfs 只能用于 SLES (SUSE Linux Enterprise Server, 仅 Docker EE 支持)。

    • 有些存储驱动依赖于后端的文件系统。例如,btrfs 只能运行于后端文件系统 btrfs 上。

    • 不同的存储驱动在不同的应用场景下性能不同。例如,aufs、overlay、overlay2 操作在文件级别,内存使用相对更高效,但大文件读写时,容器层会变得很大;devicemapper、btrfs、zfs 操作在块级别,适合工作在写负载高的场景;容器层数多,且写小文件频繁时,overlay 效率比 overlay2 更高;btrfs、zfs 更耗内存。

  • Docker 容器其实是在镜像的最上层加了一层读写层,通常也称为容器层。在运行中的容器里做的所有改动,如写新文件、修改已有文件、删除文件等操作其实都写到了容器层。容器层删除了,最上层的读写层跟着也删除了,改动自然也丢失了。若要持久化这些改动,须通过 docker commit [repository[:tag]] 将当前容器保存成为一个新镜像。若想将数据持久化,或是多个容器间共享数据,需将数据存储在 Docker volume 中,并将 volume 挂载到相应容器中。

存储驱动决定了镜像及容器在文件系统中的存储方式及组织形式,下面分别对常见的 aufs、overlay 作一简单介绍。

AUFS

AUFS 简介

AUFS 是 Debian (Stretch 之前的版本,Stretch默认采用 overlay2) 或 Ubuntu 系统上 Docker 的默认存储驱动,也是 Docker 所有存储驱动中最为成熟的。具有启动快,内存、存储使用高效等特点。如果使用的 Linux 内核版本为 4.0 或更高,且使用的是 Docker CE,可考虑使用overlay2 (比 AUFS 性能更佳)。

配置 AUFS 存储驱动

  • 验证内核是否支持 AUFS
$ grep aufs /proc/filesystems
nodev aufs
  • 若内核支持,可在 docker 启动时通过指定参数 –storage-driver=aufs 选择 AUFS

AUFS 存储驱动工作原理

采用 AUFS 存储驱动时,有关镜像和容器的所有层信息都存储在 /var/lib/docker/aufs/目录下,下面有三个子目录:

  • /diff:每个目录中存储着每层镜像包含的真实内容

  • /layers:存储有关镜像层组织的元信息,文件内容存储着该镜像的组建镜像列表

  • /mnt:挂载点信息存储,当创建容器后,mnt 目录下会多出容器对应的层及该容器的 init 层。目录名称与容器 Id 不一致。实际的读写层存储

在/var/lib/docker/aufs/diff,直到容器删除,此读写层才会被清除掉。

采用 AUFS 后容器如何读写文件?

读文件

容器进行读文件操作有以下三种场景:

  • 容器层不存在: 要读取的文件在容器层中不存在,存储驱动会从镜像层逐层向下找,多个镜像层中若存在同名文件,上层的有效。

  • 文件只存在容器层:读取容器层文件

  • 容器层与镜像层同时存在:读取容器层文件

修改文件或目录

容器中进行文件的修改同样存在三种场景:

  • 第一次写文件:若待修改的文件在某个镜像层中,aufs 会先执行 copy_up 操作将文件从只读的镜像层拷贝到可读写的容器层,然后进行修改。在文件非常大的情况下效率比较低下。

  • 删除文件:删除文件时,若文件在镜像层,其实是在容器层创建一个特殊的 whiteout 文件,容器层访问不到,并没有实际删掉。

  • 目录重命名:目前 AUFS 还不支持目录重命名。

OverlayFS

OverlayFS 简介

OverlayFS 是一种类似 AUFS 的现代联合文件系统,但实现更简单,性能更优。OverlayFS 严格说来是 Linux 内核的一种文件系统,对应的 Docker 存储驱动为 overlay 或者 overlay2,overlay2 需 Linux 内核 4.0 及以上,overlay 需内核 3.18 及以上。且目前仅 Docker 社区版支持。条件许可的话,尽量使用 overlay2,与 overlay 相比,它的 inode 利用率更高。

容器如何使用 overlay/overlay2 读写文件

读文件

读文件存在以下三种场景:

  • 文件不存在容器层:若容器要读的文件不在容器层,会继续从底层的镜像层找

  • 文件仅在容器层:若容器要读的文件在容器层,直接读取,不用在底层的镜像层查找

  • 文件同时在容器层和镜像层:若容器要读的文件在容器层和镜像层中都存在,则从容器层读取

修改文件或目录

写文件存在以下三种场景:

  • 首次写文件:若要写的文件位于镜像层中,则执行 copy_up 将文件从镜像层拷贝至容器层,然后进行修改,并在容器层保存一份新的。若文件较大,效率较低。OverlayFS 工作在文件级别而不是块级别,这意味着即使对文件稍作修改且文件很大,也须将整个文件拷贝至容器层进行修改。但需注意的是,copy_up 操作仅发生在首次,后续对同一文件进行修改,操作容器层文件即可

  • 删除文件或目录:容器中删除文件或目录时,其实是在容器中创建了一个 whiteout 文件,并没有真的删除文件,只是使其对用户不可见

  • 目录重命名:仅当源路径与目标路径都在容器层时,调用 rename(2) 函数才成功,否则返回 EXDEV

远程镜像仓库如何存储镜像?

不少人可能经常使用 docker,那么有没有思考过镜像推送至远程镜像仓库,是如何保存的呢?Docker 客户端是如何与远程镜像仓库交互的呢?

  • 我们平时本地安装的 docker 其实包含两部分:docker client 与 docker engine,docker client 与 docker engine 间通过 API 进行通信。Docker engine 提供的 API 大致有认证、容器、镜像、网络、卷、swarm 等,具体调用形式请参考:Docker Engine API(https://docs.docker.com/engine/api/v1.27/#)。

  • Docker engine 与 registry (即:远程镜像仓库)的通信也有一套完整的 API,大致包含 pull、push 镜像所涉及的认证、授权、镜像存储等相关流程,具体请参考:Registry API(https://github.com/docker/distribution/blob/master/docs/spec/api.md)。目前常用 registry 版本为 v2,registry v2 拥有断点续传、并发拉取镜像多层等特点。能并发拉取多层是因为镜像的元信息与镜像层数据分开存储,当 pull 一个镜像时,先进行认证获取到 token 并授权通过,然后获取镜像的 manifest 文件,进行 signature 校验。校验完成后,依据 manifest 里的层信息并发拉取各层。其中 manifest 包含的信息有:仓库名称、tag、镜像层 digest 等, 更多,请参考:manifest 格式文档(https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md)。

  • 各层拉下来后,也会先在本地进行校验,校验算法采用 sha256。Push 过程则先将镜像各层并发推至 registry,推送完成后,再将镜像的 manifest 推至 registry。Registry 其实并不负责具体的存储工作,具体存储介质根据使用方来定,registry 只是提供一套标准的存储驱动接口,具体存储驱动实现由使用方实现。

  • 目前官方 registry 默认提供的存储驱动包括:微软 azure、Google gcs、Amazon s3、Openstack swift、阿里云 oss、本地存储等。若需要使用自己的对象存储服务,则需要自行实现 registry 存储驱动。网易云目前将镜像存储在自己的对象存储服务 nos 上,故专门针对 nos 实现了一套存储驱动,另外认证服务也对接了网易云认证服务,并结合自身业务实现了一套认证、授权逻辑,并有效地限制了仓库配额。

  • Registry 干的事情其实很简单,大致可分为:① 读配置 ;② 注册 handler ;③ 监听。本质上 registry 是个 HTTP 服务,启动后,监听在配置文件设定的某端口上。当 http 请求过来后,便会触发之前注册过的 handler。Handler 包含 manifest、tag、blob、blob-upload、blob-upload-chunk、catalog 等六类,具体请可参考 registry 源码:/registry/handlers/app.go:92。配置文件包含监听端口、auth 地址、存储驱动信息、回调通知等。

删除Docker镜像中为none的镜像

Dockerfile 代码更新频繁,自然docker build构建同名镜像也频繁的很,产生了众多名为none的无用镜像。

分别执行以下三行可清除:

docker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker stop
docker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker rm
docker images|grep none|awk '{print $3 }'|xargs docker rmi

之前有写过一些 Docker 技巧和命令(在文章的尾部),这里再重复2个。

停止所有容器,删除所有容器:

docker kill $(docker ps -q) ; docker rm $(docker ps -a -q)

停止所有容器,删除所有容器,删除所有镜像:

docker kill $(docker ps -q) ; docker rm $(docker ps -a -q) ; docker rmi $(docker images -q -a)

利用docker部署ceph集群

背景

Ceph官方现在提供两类镜像来创建集群,一种是常规的,每一种Ceph组件是单独的一个镜像,如ceph/daemon、ceph/radosgw、ceph/mon、ceph/osd等;另外一种是最新的方式,所有的Ceph组件都集成在一个镜像ceph/daemon中,如果要创建对应的Ceph组件容器,只需要指定类型即可。这里,我们使用第一种方式来创建Ceph集群。
  
另外,这里所有的容器,都是在同一个Docker host上创建的。

重新开始

在搭建过程中,如果遇到了任何问题,想删除之前的环境,并重新开始搭建时,可以运行如下命令:

#  rm -rf /etc/ceph /var/lib/ceph/  /opt/osd/; mkdir -p /etc/ceph /var/lib/ceph/osd /opt/osd/;chown -R 64045:64045 /var/lib/ceph/osd/;chown -R 64045:64045 /opt/osd/; docker rm -f $(docker ps -a | grep "ceph" | awk '{print $1}');

  
这里,分别删除了ceph配置、osd目录。并重新创建目录并更改所属用户(这一步非常重要),再删除所有有ceph关键字的容器。

搭建集群

创建monitor节点

这里,假设monitor节点的ip为192.168.1.111。

# docker run -itd --name mymon --network my-bridge --ip 192.168.1.111 -e MON_NAME=mymon -e MON_IP=192.168.1.111 -v /etc/ceph:/etc/ceph ceph/mon

在monitor节点上标识osd

# docker exec mymon ceph osd create
0
# docker exec mymon ceph osd create
1
# docker exec mymon ceph osd create
2

创建osd节点

# docker run -itd --name osd0 --network my-bridge -e CLUSTER=ceph -e WEIGHT=1.0 -e MON_NAME=mymon -e MON_IP=192.168.1.111 -v /etc/ceph:/etc/ceph -v /opt/osd/0:/var/lib/ceph/osd/ceph-0 ceph/osd 
# docker run -itd --name osd1 --network my-bridge -e CLUSTER=ceph -e WEIGHT=1.0 -e MON_NAME=mymon -e MON_IP=192.168.1.111 -v /etc/ceph:/etc/ceph -v /opt/osd/1:/var/lib/ceph/osd/ceph-1 ceph/osd 
# docker run -itd --name osd2 --network my-bridge -e CLUSTER=ceph -e WEIGHT=1.0 -e MON_NAME=mymon -e MON_IP=192.168.1.111 -v /etc/ceph:/etc/ceph -v /opt/osd/2:/var/lib/ceph/osd/ceph-2 ceph/osd 

再创建两个monitor节点组成集群

# docker run -itd --name mymon_1 --network my-bridge --ip 192.168.1.112 -e MON_NAME=mymon_1 -e MON_IP=192.168.1.112 -v /etc/ceph:/etc/ceph ceph/mon
# docker run -itd --name mymon_2 --network my-bridge --ip 192.168.1.113 -e MON_NAME=mymon_2 -e MON_IP=192.168.1.113 -v /etc/ceph:/etc/ceph ceph/mon

创建Ceph网关节点

#  docker run -itd --name myrgw --network my-bridge --ip 192.168.1.100 -p 9080:80 -e RGW_NAME=myrgw -v /etc/ceph:/etc/ceph ceph/radosgw

验证

执行ceph命令来查看ceph集群状态。

# docker exec mymon ceph -s

缺点

这种搭建方法,所有容器共享了/etc/ceph中的配置,因此,仅限于在一个docker host中搭建集群。在最新的Ceph镜像ceph/daemon中,提供了一种共享kv存储etcd的实现方式,可以实现多host情况下的Ceph集群搭建,适用于swarm、k8s或者Marathon的环境下搭建Ceph,有兴趣的朋友可以试一试。

5种Docker日志最佳实践

译者注:微服务和容器很好地结合了,但是它们的结合让日志记录也变成了一个难题。作者在本文描述了一些因素,在设置监控的时候是需要考虑的。以下为译文

在过去的几年中,容器已经成为IT领域的一个重要话题,尤其是在DevOps领域。简单地说,当从一个环境迁移到另一个环境时,容器提供了一种简单且可扩展的方法可以运行软件。

容器是通过在一个包中提供完整的运行环境实现的,其中就包括了应用程序,所有的依赖项,库,其它二进制文件以及运行时所需的配置文件。

与容器紧密结合的是微服务,它代表了开发应用程序的一种更灵活的方式。微服务体系结构将应用程序构建为一组松耦合的服务,这些服务通过处理离散业务功能的API连接起来。微服务主要为应用程序开发提供了一种“分而治之”的方法,而不是一个大型的单一代码库。

Docker在容器的基础架构领域是处于世界领先地位的,它是一个部署容器级软件应用的平台。容器的真正价值在于它们允许团队动态地启动一个完整的运行环境。Docker可以说是让企业采用微服务的最具影响力的平台。

类似于虚拟机通过向来自一个服务器的终端用户提供一个操作系统的多个实例来简化软件开发和测试,容器在应用程序和主机操作系统之间添加了一个额外的抽象层。最大的不同是,容器不需要管理程序,只运行操作系统的一个实例;总的来说,这等同于内存更少,运行时间更快。

与开发任何应用程序一样,日志记录是过程的中心部分,在出现问题时尤其有用。但是,在集装箱化应用程序的世界里,与传统应用程序相比,它是不同的。日志Docker实际上意味着不仅记录应用程序和应用程序
主机操作系统,以及Docker服务。在处理多码应用程序时,有许多日志记录技术和方法可以记住。我们将在下面详细介绍前五种最佳实践。

基于应用程序的日志记录

在基于应用程序的方法中,容器内的应用程序使用日志框架来处理日志记录过程。例如,某个Java应用程序可能会使用Log4j 2来对日志文件格式化,然后发送到远程服务器,并完全绕过Docker环境和操作系统。

虽然基于应用程序的日志记录使开发人员对日志事件有了最大的控制权,但是这种方法也会在应用程序过程中产生大量的消耗。这种方法对于那些工作在传统应用程序环境中的人来说可能是有用的,因为它允许开发人员继续使用应用程序的日志框架(例如Log4j 2)而不需要向主机添加日志功能。

Logging Docker实际上意味着不仅需要记录应用程序和主机操作系统,还包括了Docker服务。

使用数据卷

容器本质上是临时的,这意味着如果容器关闭了,那么容器内的任何文件最终都会丢失。相反,容器必须将日志事件转发到集中式日志记录服务(比如Loggly),或者将日志事件存储在数据卷中。数据卷的定义为“容器内的一个标记目录,该目录用来保存持久或共享的数据”。

使用数据卷来记录事件的好处是,由于它们链接到主机上的一个目录,所以日志数据仍然存在,并且可以与其它容器共享。这种方法的优点是它减少了在容器失败或关闭时丢失数据的可能性。在这里可以找到关于在Ubuntu中设置Docker数据卷的说明。

Docker日志驱动

在Docker中进行日志记录的第三种方法是使用平台的日志驱动程序将日志事件转发给在主机上运行的syslog实例。Docker日志驱动程序直接从容器的stdout和stderr输出里面读取日志事件;这就消除了从日志文件中读取和写入的需要,最终也会稍微改善性能。

然而,使用Docker日志驱动程序也有一些缺点:

  • 它不允许进行日志解析,只允许进行日志转发。

  • Docker日志命令只与日志驱动程序JSON文件一起工作。

  • 当TCP服务器不可访问时,容器就会终止。

这里可以找到为Docker配置默认日志驱动程序的说明。

容器专用日志

这种方法的主要优点是允许在Docker环境中完全地管理日志事件。由于专用的日志容器可以从其他容器收集日志事件,聚合它们,然后将事件存储或转发到第三方服务,这种方法消除了对主机的依赖。

专用日志容器的其它优点是:

  • 自动收集、监视和分析日志事件。

  • 在没有配置的情况下自动缩放日志事件。

  • 通过多个日志事件、stats和Docker API数据流来检索日志。

Sidecar方法

Sidecars已经成为管理微服务架构的流行方法。Sidecar的想法来自于类似摩托车的sidecar是如何附着在摩托车上的。引用一个消息源,“Sidecar作为第二个过程在你的服务旁边运行,并通过类似于HTTP上的REST-like API 这样一个同类接口提供了’平台基础设施’的特性。”

从日志记录的角度来看,Sidecar方法的优点是每个容器都与它自己的日志容器有关(应用程序容器保存日志事件和日志容器标记,然后像Loggly那样将它们转发到日志管理系统)。

未分类

Sidecar方法对于大型部署来说尤其有用,因为这些部署需要有更专门的日志信息和自定义标记。不过,建立Sidecar非常复杂,而且难度也很大。

Docker安装配置tomcat jdk

第一步:将tomcat及JDK 复制到指定目录 /docker/

未分类

第二步:在该目录下编写Dockerfile文件,进行添加进行文件内容

未分类

第三步:编译Dockerfile文件

docker build -t tomcat .

build :表示编译Dockerfile文件

-t tomcat :表示 镜像文件名称为tomcat,这里可以自己定义名称

. : 一定要记住点 . 表示当前目录

未分类

第四步:查看是否生成镜像文件

docker images

未分类

第五步:创建启动容器

docker create –name item_tomcat -p 8080:8080 tomcat

–name item_tomcat : 表示给容器创建一个名称,可以自定义

-p 8080:8080 :表示指定容器端口与本机端口对应,访问的时候会自动调整到指定容器端口8080

tomcat:表示使用哪个镜像文件进行创建容器

未分类

第六步:启动容器

docker start item_tomcat

未分类

第七步:查看启动容器日志

docker logs item_tomcat

未分类

第八步:访问tomcat启动是否成功

浏览器https://localhost:8080

未分类

或者使用命令直接访问 curl https://localhost:8080

未分类

Docker容器部署启动已经完成。

使用Docker配置Nginx环境部署Nextcloud

相关介绍

  • NGINX
    Nginx是一款功能强大的反向代理服务器,支持HTTP、HTTPS、SMTP、IMAP、POP3等协议,它也可以作为负载均衡器、HTTP缓存或是Web服务器。

  • Docker
    Docker是一款轻量级虚拟机软件,他充分利用操作系统本身已有的机制和特性,实现远超传统虚拟机额度轻量级虚拟化。

  • Nextcloud
    Nextcloud是一款功能强大的PHP网盘程序,衍生自著名开源项目ownCloud,拥有美观的Web界面和强大的扩展能力,以及优秀的安全性能。可满足复杂条件下对私有云的需求。

前言

Nextcloud复杂的功能和高安全似乎决定了它的臃肿——但我们不能像对于WordPress一样责怪开发人员。但Nextcloud无后端设计使它在性能落后于Seafile这样的程序,我一直坚持Apache+PHP是Nextcloud最好的搭档,现在我依旧坚持这一点,看看那冗长的Nginx配置文件,而Apache几乎什么都不用做。尽管如此,我还是不得不嫉妒Nginx处理静态文件的优秀性能。

或许以上就是LNMPA诞生的原因吧。但我今天并不准备演示一遍如何搭建LNMPA环境,而是寻找另一种更简易的方法:Nginx+Docker。

其实很简单,不过是将Apache装入容器中而已:

未分类

由此图可以看出,这并没有什么复杂的地方,根本谈不上高深。

那么,将ApacheMySQL等服务放进容器有什么好处呢?

  • 易部署:所使用的都是现成的景象,随时启用,随时删除。
  • 高容错:操作出现任何问题也不会对宿主机有什么影响。
  • 模块化:附加的服务(ONLYOFFICE、Collabora Online、XMPP等)均运行于独立的容器中,互不干扰,增删方便。
  • 免于处理各种复杂的兼容问题。

但我今天就是要简单的问题复杂化,把每一部分都分析透彻。

Docker

Nextcloud在Docker Hub上有已经配置完成的镜像,使用Apache+PHP或是Nginx+FPM,但是不包含MySQL或MariaDB这样的数据库应用,也不直接支持HTTPS访问。

对于缺少的数据库应用,当然可以使用SQLite来应付这个问题,但是,显然不是最佳的解决方案。

最佳的解决方案也不是使用宿主机的数据库服务,而是使用Docker的一个关键功能——容器互联。

容器互联(linking)是一种让多个容器中的应用进行快速交互的方式。它会在源和接受容器中间创建连接关系,接受容器可以通过容器名快速访问到源容器而不用指出具体的IP地址。

举个例子,我们运行一个容器的命令一般是这样的:

dock run -d <container>

可以加上 –name 来为这个容器指定一个名字吗,比如“c1”

docker run -d --name c1 <container>

概念:Docker网桥(Net Bridge)

Docker在创建容器时会默认将容器连接于一个虚拟网桥(docker0)上,这实际上是一个Linux网桥,可以理解为是一个软件交换机(和家里的路由器有几分相像)。它会在挂载其上的接口进行转发,如图:

未分类

docker0可以理解为一个局域网,就像你家的网络与电信服务商之间隔了一个路由器,两个网络之间无法直接访问,除非映射端口。

未分类

(映射端口的操作使用 -p 宿主机端口:容器端口来完成)

如果你操作过路由器上的端口映射功能,这部分会很好理解。

对于docker0内部,每个容器都会分配到一个IP地址,同时,在每个容器内的hosts文件中会记下IP地址与容器的对应关系,这样,如果一个容器想要访问另一个容器,只需要知道容器的ID或者容器名,就像域名一样,而不必获知它的IP地址。

有了网桥,我们就可以将Apache和MySQL分别部署到两个容器中,通过容器名来访问。

数据的操作和持久化

无论是使用Docker,还是Virtualbox亦或是VMware这样的虚拟机软件,实现宿主机和虚拟机之间的文件互访一直是很重要的一件事。这儿我们要用到Docker的数据管理方式之一——数据卷。

  • 数据卷(Data Volumes):容器内数据直接映射到本地主机环境。

数据卷是一个可供容器使用的特殊目录,它会将主机操作系统目录直接映射至容器。

一个典型的例子:我创建了一个带有HTTP服务器的容器,在宿主机上使用Nginx反向代理指向它的请求,为了提高性能,需要分离客户端的动态请求和静态请求。此时,我就可以将容器内的文件映射出来,对于动态请求,Nginx会与容器进行通信,而对于静态请求,Nginx可以直接从本地获得静态文件,提高速度。

提前说一下,上面的例子并不适用于Nextcloud,或者说,我还没找到正确的途径。

1. 在容器内创建一个数据卷

在用Docker run命令的时候,使用 -v 标记可以在容器内创建一个数据卷。标记可重复使用。示例:

docker run -d -P --name web -v /webapp training/webapp python app.py

2. 挂载一个主机目录作为数据卷

格式和映射端口相同, -v 本地目录:容器内目录 (本地目录必须为绝对路径)。

如果想更集中地去管理容器的数据的话,可以使用数据卷容器,不再赘述。

Nginx

Nginx在这里的身份是反向代理服务器,之前我一直将Nginx用作HTTP服务器,现在才正式接触Nginx一直所标榜的功能。

反向代理的配置可以十分简单,直接在server{}中加入:

location / 
{
proxy_pass http://代理地址:端口;
}

以上就是一个反向代理配置,但是这还不够,并且在接下来的时间里就会发现这远远不够。

所谓反向代理,对真实服务器来说无感知,也就是说它并不知道有Nginx这一个的存在。因为对服务端来说,它一直只被一个永户访问,就是反向代理服务器Nginx,而且一直是使用一个URL访问(http://代理地址:端口)。所幸的是,这些状况都是由上方的那简单的配置而导致的,通过添加一些配置信息,就可以解决这个问题:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         }

看看,都添加了哪些东西。

  • proxy_set_header Host $http_host; 传递了客户端(相对于Nginx)的URL地址,使得服务端得知访问它所用的URL是外部客户端所访问的真实URL。
  • proxy_set_header X-Real-IP $remote_addr; 获得客户端的真实IP,而不是Nginx的127.0.0.1
  • proxy_set_header X-Forwarded-Proto $scheme; 使服务端能正确识别客户端所用的是HTTP协议还是HTTPS协议

WebSocket代理

现在还有一个棘手的问题,很多Web应用都使用了WebSocket技术,以实现ajax难以实现的一些功能。但在前文中的配置下,Nginx并不会去代理WebSocket请求,Websocket协议是这样的: ws://服务器地址/ 或 wss://服务器地址/。

既然我这儿都把问题说出来了,那肯定就有解决方法咯。

在前文的配置中再加入以下内容:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

这样,我们就得到了一个靠谱的反向代理配置:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header Upgrade $http_upgrade; 
         proxy_set_header Connection $connection_upgrade;
         }

它应该能应付大多数应用了。

等下……

欸?为什么反向代理就高效了?

明明没有啊?

的确没有。

在目前的配置下,我们只是在客户端和服务端之间安排了一个中间人,而且既符合常理也符合实际地说,它变慢了(虽然感觉上微乎其微)。就好比给网站做CDN,做了的全站CDN却没有缓存,所有请求由CDN服务器转发给源服务器,再由源服务器将所有回应全部转发给CDN服务器,实际上增加了中间过程,对响应速度没有丝毫改善。而如果做了CDN缓存,CDN服务器会承担相当一部分请求,回源的请求会大幅减少甚至为0。

同样的,Nginx也是如此,我们得想办法让Nginx自己去根据静态请求返回静态文件,让服务端少为它不擅长的静态文件传输浪费功夫,全心全意地去处理静态请求(Tomcat是个典型的例子,所以我们经常看到把Nginx和Tomcat结合起来用)。

这个解决办法就叫动静分离。

只需要对客户端发来的请求过滤一下,分出其中哪些是动态请求,哪些是静态请求。至于分离方法,可以使用强大正则表达式来匹配URL:

.*.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*)

应用到Nginx中:

location / {
         proxy_pass http://代理地址:端口;
         proxy_set_header Host $http_host;
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header Upgrade $http_upgrade; 
         proxy_set_header Connection $connection_upgrade;
         }
location ~ .*.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*)
         {
         root /var/www/html/static; #静态文件存放位置
         expires 30d; #缓存30天
         }

这就将动静态请求分离了出来,凡是带有gif、jpg、png……的请求统统从本地 /var/www/html/static目录中获取。而动态请求则发送至服务端。

为什么之前说不适用于Nextcloud呢?

因为Nextcloud的一些静态文件实则是由动态语言实时生成的,比如这个:

https://cloud.orgleaf.com/index.php/css/files/92221bb11c10969dc0aad6345517ad93-merged.css

对于这种请求,Nginx仍然会傻乎乎地到/var/www/html/static里去找,当然找不到。

这种问题也许能用更复制的正则表达式来解决,但有没有更简单的办法呢?

直接设置缓存所有静态请求,这样第二次访问的时候就自动从自身的缓存中获取,缓存中没有的内容就找服务端获取,然后再缓存下来:

location ~ .*.(gif|jpg|png|htm|html|css|js|flv|ico|swf)(.*) {
         proxy_pass http://代理地址:端口;
         proxy_redirect off;         
         proxy_set_header Host $host;
         proxy_cache cache_one;
         proxy_cache_valid 200 302 1h;      
         proxy_cache_valid 301 1d;
         proxy_cache_valid any 1m;
         expires 30d;
        }

另外还要在http{}中加入以下内容:

proxy_temp_path /app/nextcloud/temp_dir;  #临时文件夹
proxy_cache_path /app/nextcloud/cache levels=1:2 keys_zone=cache_one:200m inactive=1d max_size=30g;
                 #     ⇑缓存位置          目录深度⇑                                         ⇑最大体积

好了,现在Nginx的反向代理真的有了它的积极作用。

还有一点——post最大体积

使用Docker和Nginx搭建Nextcloud完成后,我在上传一个约70MB的文件时出现错误,Nextcloud本身没什么动静。看了下console,发现服务器返回错误码413:请求实体太大。

POST的最大体积需要在http{}中的 client_max_body_size 设置,例如:

client_max_body_size 100m;

完成后就可以上传小于100MB的文件了。

Nextcloud

或许我应该把这部分放到Docker里说。

从hub.docker.com获得的官方Nextcloud镜像是不包含数据库服务的,而镜像也阉割了很多常用命令。这样我们就不得不用容器互联来用上数据库了。

如前所述,如果我添加了两个容器,一个运行Nextcloud,另一个运行MySQL,这两个容器默认是在同一网桥上,而我就可以把它们连接起来。

首先运行MySQL容器,我给它起名为db1,然后在MYSQL_ROOT_PASSWORD后面指定root密码为my-secret-pw

docker run --name db1 -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

然后运行Nextcloud容器,用 –link my-mysql:mysql 把它和MySQL容器连接起来

docker run -d -p 8080:80  --link db1:mysql nextcloud 

官方提供了一个docker-compose文件,可以看出他这儿使用MariaDB作为数据库,命名为db,并在nextcloud服务中连接

version: '2'

volumes:
  nextcloud:
  db:

services:
  db:
    image: mariadb
    restart: always
    volumes:
      - db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=
      - MYSQL_PASSWORD=
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud

  app:  
    image: nextcloud
    ports:
      - 8080:80
    links:
      - db
    volumes:
      - nextcloud:/var/www/html
    restart: always

启动容器后访问Nextcloud容器,填入MySQL/MariaDB的服务器地址时直接填“db”就可以了,因为“db”就是MySQL容器的主机名,hosts文件中已经指明了“db”对应的容器。

数据卷

把数据卷映射至本地,这样我们如果要更改Nextcloud的文件,直接在宿主机上就可以操作了,即前文中所说的方法。

我们一共要映射这三个目录:

nextcloud:/var/www/html          #Nextcloud的程序
data:/var/www/html/data          #Nextcloud的数据目录
config:/var/www/html/config      #Nextcloud的配置文件所在目录
apps:/var/www/html/apps          #Nextcloud应用的目录

具体命令就是这样:

docker run -d nextcloud 
-v nextcloud:/var/www/html 
-v apps:/var/www/html/custom_apps 
-v config:/var/www/html/config 
-v data:/var/www/html/data

注意:冒号前面的nextcloud、apps、config、data要全部替换为本地的绝对路径

OCC命令

使用docker exec命令以在宿主机上为容器执行OCC命令:

docker exec --user www-data CONTAINER_ID php occ

CONTAINER_ID就是运行的Nextcloud容器的ID,如果你给容器命了名,那么也可以是这个容器的名字。

本文到此结束,我也要暂时的疏远Nextcloud/ownCloud了,更广阔的世界还等着我去探索。

Docker安装部署MongoDB的两种方法

方法一、通过 Dockerfile 构建

创建Dockerfile

首先,创建目录mongo,用于存放后面的相关东西。

runoob@runoob:~$ mkdir -p ~/mongo  ~/mongo/db

db目录将映射为mongo容器配置的/data/db目录,作为mongo数据的存储目录

进入创建的mongo目录,创建Dockerfile

FROM debian:wheezy

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd -r mongodb && useradd -r -g mongodb mongodb

RUN apt-get update 
    && apt-get install -y --no-install-recommends 
        numactl 
    && rm -rf /var/lib/apt/lists/*

# grab gosu for easy step-down from root
ENV GOSU_VERSION 1.7
RUN set -x 
    && apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/* 
    && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" 
    && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" 
    && export GNUPGHOME="$(mktemp -d)" 
    && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 
    && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu 
    && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc 
    && chmod +x /usr/local/bin/gosu 
    && gosu nobody true 
    && apt-get purge -y --auto-remove ca-certificates wget

# gpg: key 7F0CEB10: public key "Richard Kreuter <[email protected]>" imported
RUN apt-key adv --keyserver ha.pool.sks-keyservers.net --recv-keys 492EAFE8CD016A07919F1D2B9ECBEC467F0CEB10

ENV MONGO_MAJOR 3.0
ENV MONGO_VERSION 3.0.12

RUN echo "deb http://repo.mongodb.org/apt/debian wheezy/mongodb-org/$MONGO_MAJOR main" > /etc/apt/sources.list.d/mongodb-org.list

RUN set -x 
    && apt-get update 
    && apt-get install -y 
        mongodb-org=$MONGO_VERSION 
        mongodb-org-server=$MONGO_VERSION 
        mongodb-org-shell=$MONGO_VERSION 
        mongodb-org-mongos=$MONGO_VERSION 
        mongodb-org-tools=$MONGO_VERSION 
    && rm -rf /var/lib/apt/lists/* 
    && rm -rf /var/lib/mongodb 
    && mv /etc/mongod.conf /etc/mongod.conf.orig

RUN mkdir -p /data/db /data/configdb 
    && chown -R mongodb:mongodb /data/db /data/configdb
VOLUME /data/db /data/configdb

COPY docker-entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

EXPOSE 27017
CMD ["mongod"]

通过Dockerfile创建一个镜像,替换成你自己的名字

runoob@runoob:~/mongo$ docker build -t mongo:3.2 .

创建完成后,我们可以在本地的镜像列表里查找到刚刚创建的镜像

runoob@runoob:~/mongo$ docker images  mongo:3.2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
mongo               3.2                 282fd552add6        9 days ago          336.1 MB

方法二、docker pull mongo:3.2

查找Docker Hub上的mongo镜像

runoob@runoob:~/mongo$ docker search mongo
NAME                              DESCRIPTION                      STARS     OFFICIAL   AUTOMATED
mongo                             MongoDB document databases ...   1989      [OK]       
mongo-express                     Web-based MongoDB admin int...   22        [OK]       
mvertes/alpine-mongo              light MongoDB container          19                   [OK]
mongooseim/mongooseim-docker      MongooseIM server the lates...   9                    [OK]
torusware/speedus-mongo           Always updated official Mon...   9                    [OK]
jacksoncage/mongo                 Instant MongoDB sharded cluster  6                    [OK]
mongoclient/mongoclient           Official docker image for M...   4                    [OK]
jadsonlourenco/mongo-rocks        Percona Mongodb with Rocksd...   4                    [OK]
asteris/apache-php-mongo          Apache2.4 + PHP + Mongo + m...   2                    [OK]
19hz/mongo-container              Mongodb replicaset for coreos    1                    [OK]
nitra/mongo                       Mongo3 centos7                   1                    [OK]
ackee/mongo                       MongoDB with fixed Bluemix p...  1                    [OK]
kobotoolbox/mongo                 https://github.com/kobotoolb...  1                    [OK]
valtlfelipe/mongo                 Docker Image based on the la...  1                    [OK]

这里我们拉取官方的镜像,标签为3.2

runoob@runoob:~/mongo$ docker pull mongo:3.2

等待下载完成后,我们就可以在本地镜像列表里查到REPOSITORY为mongo,标签为3.2的镜像。

使用mongo镜像

运行容器

runoob@runoob:~/mongo$ docker run -p 27017:27017 -v $PWD/db:/data/db -d mongo:3.2
cda8830cad5fe35e9c4aed037bbd5434b69b19bf2075c8626911e6ebb08cad51
runoob@runoob:~/mongo$

命令说明:

-p 27017:27017 :将容器的27017 端口映射到主机的27017 端口

-v $PWD/db:/data/db :将主机中当前目录下的db挂载到容器的/data/db,作为mongo数据存储目录

查看容器启动情况

runoob@runoob:~/mongo$ docker ps 
CONTAINER ID   IMAGE        COMMAND                   ...    PORTS                      NAMES
cda8830cad5f   mongo:3.2    "/entrypoint.sh mongo"    ...    0.0.0.0:27017->27017/tcp   suspicious_goodall

使用mongo镜像执行mongo 命令连接到刚启动的容器,主机IP为172.17.0.1

runoob@runoob:~/mongo$ docker run -it mongo:3.2 mongo --host 172.17.0.1
MongoDB shell version: 3.2.7
connecting to: 172.17.0.1:27017/test
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
  http://docs.mongodb.org/
Questions? Try the support group
  http://groups.google.com/group/mongodb-user
>