如何 在Docker窗口中部署PHP开发环境

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何 在Docker窗口中部署PHP开发环境相关的知识,希望对你有一定的参考价值。

参考技术A 环境部署一直是一个很大的问题,无论是开发环境还是生产环境,但是 Docker
将开发环境和生产环境以轻量级方式打包,提供了一致的环境。极大的提升了开发部署一致性。当然,实际情况并没有这么简单,因为生产环境和开发环境的配置是完全不同的,比如日志等的问题都需要单独配置,但是至少比以前更加简单方便了,这里以
php 开发作为例子讲解 Docker 如何布置开发环境。

一般来说,一个 PHP 项目会需要以下工具:

Web 服务器: nginx/Tengine

Web 程序: PHP-FPM

数据库: mysql/PostgreSQL

缓存服务: Redis/Memcache

这是最简单的架构方式,在 Docker 发展早期,Docker 被大量的滥用,比如,一个镜像内启动多服务,日志收集依旧是按照 Syslog
或者别的老方式,镜像容量非常庞大,基础镜像就能达到 80M,这和 Docker 当初提出的思想完全南辕北辙了,而 Alpine Linux
发行版作为一个轻量级 Linux 环境,就非常适合作为 Docker 基础镜像,Docker 官方也推荐使用 Alpine 而不是 Debian
作为基础镜像,未来大量的现有官方镜像也将会迁移到 Alpine 上。本文所有镜像都将以 Alpine 作为基础镜像。

Nginx/Tengine

这部分笔者已经在另一篇文章 Docker 容器的 Nginx 实践中讲解了 Tengine 的 Docker 实践,并且给出了
Dockerfile,由于比较偏好 Tengine,而且官方已经给出了 Nginx 的 alpine 镜像,所以这里就用
Tengine。笔者已经将镜像上传到官方 DockerHub,可以通过

<code>docker pull chasontang/tengine:2.1.2_f</code>

获取镜像,具体请看 Dockerfile。

PHP-FPM

Docker 官方已经提供了 PHP 的 7.0.7-fpm-alpine 镜像,Dockerfile 如下:

FROM alpine:3.4

# persistent / runtime deps
ENV PHPIZE_DEPS \
autoconf \
file \
g++ \
gcc \
libc-dev \
make \
pkgconf \
re2c
RUN apk add --no-cache --virtual .persistent-deps \
ca-certificates \
curl

# ensure www-data user exists
RUN set -x \
&& addgroup -g 82 -S www-data \
&& adduser -u 82 -D -S -G www-data www-data
# 82 is the standard uid/gid for "www-data" in Alpine
# http://git.alpinelinux.org/cgit/aports/tree/main/apache2/apache2.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/lighttpd/lighttpd.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/nginx-initscripts/nginx-initscripts.pre-install?h=v3.3.2

ENV PHP_INI_DIR /usr/local/etc/php
RUN mkdir -p $PHP_INI_DIR/conf.d

##<autogenerated>##
ENV PHP_EXTRA_CONFIGURE_ARGS --enable-fpm --with-fpm-user=www-data --with-fpm-group=www-data
##</autogenerated>##

ENV GPG_KEYS 1A4E8B7277C42E53DBA9C7B9BCAA30EA9C0D5763

ENV PHP_VERSION 7.0.7
ENV PHP_FILENAME php-7.0.7.tar.xz
ENV PHP_SHA256 9cc64a7459242c79c10e79d74feaf5bae3541f604966ceb600c3d2e8f5fe4794

RUN set -xe \
&& apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
curl-dev \
gnupg \
libedit-dev \
libxml2-dev \
openssl-dev \
sqlite-dev \
&& curl -fSL "http://php.net/get/$PHP_FILENAME/from/this/mirror" -o "$PHP_FILENAME" \
&& echo "$PHP_SHA256 *$PHP_FILENAME" | sha256sum -c - \
&& curl -fSL "http://php.net/get/$PHP_FILENAME.asc/from/this/mirror" -o "$PHP_FILENAME.asc" \
&& export GNUPGHOME="$(mktemp -d)" \
&& for key in $GPG_KEYS; do \
gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \
done \
&& gpg --batch --verify "$PHP_FILENAME.asc" "$PHP_FILENAME" \
&& rm -r "$GNUPGHOME" "$PHP_FILENAME.asc" \
&& mkdir -p /usr/src \
&& tar -Jxf "$PHP_FILENAME" -C /usr/src \
&& mv "/usr/src/php-$PHP_VERSION" /usr/src/php \
&& rm "$PHP_FILENAME" \
&& cd /usr/src/php \
&& ./configure \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \
$PHP_EXTRA_CONFIGURE_ARGS \
--disable-cgi \
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
--enable-mysqlnd \
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
--enable-mbstring \
--with-curl \
--with-libedit \
--with-openssl \
--with-zlib \
&& make -j"$(getconf _NPROCESSORS_ONLN)" \
&& make install \
&& find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '' + || true; \
&& make clean \
&& runDeps="$( \
scanelf --needed --nobanner --recursive /usr/local \
| awk ' gsub(/,/, "\nso:", $2); print "so:" $2 ' \
| sort -u \
| xargs -r apk info --installed \
| sort -u \
)" \
&& apk add --no-cache --virtual .php-rundeps $runDeps \
&& apk del .build-deps

COPY docker-php-ext-* /usr/local/bin/

##<autogenerated>##
WORKDIR /var/www/html

RUN set -ex \
&& cd /usr/local/etc \
&& if [ -d php-fpm.d ]; then \
# for some reason, upstream's php-fpm.conf.default has "include=NONE/etc/php-fpm.d/*.conf"
sed 's!=NONE/!=!g' php-fpm.conf.default | tee php-fpm.conf > /dev/null; \
cp php-fpm.d/www.conf.default php-fpm.d/www.conf; \
else \
# PHP 5.x don't use "include=" by default, so we'll create our own simple config that mimics PHP 7+ for consistency
mkdir php-fpm.d; \
cp php-fpm.conf.default php-fpm.d/www.conf; \
\
echo '[global]'; \
echo 'include=etc/php-fpm.d/*.conf'; \
| tee php-fpm.conf; \
fi \
&& \
echo '[global]'; \
echo 'error_log = /proc/self/fd/2'; \
echo; \
echo '[www]'; \
echo '; if we send this to /proc/self/fd/1, it never appears'; \
echo 'access.log = /proc/self/fd/2'; \
echo; \
echo 'clear_env = no'; \
echo; \
echo '; Ensure worker stdout and stderr are sent to the main error log.'; \
echo 'catch_workers_output = yes'; \
| tee php-fpm.d/docker.conf \
&& \
echo '[global]'; \
echo 'daemonize = no'; \
echo; \
echo '[www]'; \
echo 'listen = [::]:9000'; \
| tee php-fpm.d/zz-docker.conf

EXPOSE 9000
CMD ["php-fpm"]
##</autogenerated>##

docker实战——在测试中使用Docker

在之前几章中介绍的都是Docker的基础知识,了解什么是镜像,docker基本的启动流程,以及如何去运作一个容器等等。

接下来的几个章节将介绍如何在实际开发和测试过程中使用docker。

将Docker作为本地Web开发环境是使用Docker的一个最简单的场景。这个环境可以完全重现生产环境,保证开发环境和部署环境一致。下面从将Nginx安装到容器来架构一个简单的网站开始。

使用Docker测试静态网站

## 创建一个sample的镜像目录并创建一个Dockerfile
# mkdir sample
# cd sample/
# touch Dockerfile

## 在sample 目录中创建一个叫nginx的目录,并用来存放nginx的配置文件
# mkdir nginx && cd nginx
# vim global.conf
server {
        listen          0.0.0.0:80;
        server_name     _;

        root            /var/www/html/website;
        index           index.html index.htm;

        access_log      /var/log/nginx/default_access.log;
        error_log       /var/log/nginx/default_error.log;
}

# vim nginx.conf
user www-data;
worker_processes 4;
pid /run/nginx.pid;
daemon off;

events {  }

http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
}

接下来我们编辑Dockerfile

# cd ..
# cat Dockerfile 
FROM ubuntu:14.04
MAINTAINER BOurbon Tian "bourbon@1mcloud.com"
ENV REFRESHED_AT 2017-05-25
RUN apt-get update
RUN apt-get -y -q install nginx
RUN mkdir -p /var/www/html
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

这个简单的Dockerfile内容包括以下几项:

  • 选择基础镜像;
  • 安装nginx;
  • 在容器中创建一个/var/www/html的目录;
  • 将我们本地创建的nginx配置文件添加到镜像中;
  • 公开镜像的80端口。

接下来通过docker build命令构建新的镜像:

# docker build -t="test/nginx" .
# docker images
REPOSITORY                                TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
test/nginx                                latest              f495ccf93291        43 minutes ago      231.3 MB

创建一个静态网站

在sample目录中创建一个名为website的目录,并创建一个静态的测试页面放到website目录中。

# cd sample
# mkdir website && cd website
# vim index.html
<head>
<title>Test website</title>
</head>
<body>
<h1>This is a test website</h1>
</body>

通过docker run创建一个新的容器:

# docker run -d -p 80 --name website -v /opt/sample/website:/var/www/html/website test/nginx nginx
  •  -v选项,将宿主机的目录作为卷,挂载到容器里。

卷在Docker里非常重要,也很有用。卷是在一个或者多个容器内被选定的目录,可以绕过分层的联合文件系统(Union File System),为Docker提供持久数据或者共享数据。这意味着对卷的修改会直接生效,并绕过镜像。当提交或者创建镜像时,卷不被包含在镜像里。卷可以在容器间共享。即便容器停止,卷里的内容依旧存在。在后面的章节会看到如何使用卷来管理数据。

当我们因为某些原因不想把应用或者代码构建到镜像中时,就体现出了卷的价值。例如:

  • 希望同时对代码做开发和测试;
  • 代码改动很频繁,不想再开发过程中重构镜像;
  • 希望在多个容器间共享代码。

参数-v指定了卷的目录(本地宿主机的目录)和容器里的目录,这两个目录通过:来分隔。如果目的目录不存在,Docker会自动创建一个。也可以通过在目的目录的后面加上rw或者ro来指定目的目录的读写状态如:

# docker run -d -p 80 --name website -v /opt/sample/website:/var/www/html/website:ro test/nginx nginx

这将使目的目录/var/www/html/website变成只读状态。

# docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                   NAMES
9e13aebecae3        test/nginx          "nginx"             2 hours ago         Up 2 hours          0.0.0.0:32775->80/tcp   website 

通过docker ps命令查看正在运行的容器,可以看到名为website容器正处于活跃状态,其80端口被映射到本地的32775端口。

如果在docker宿主机上浏览32775端口,就会到这个测试静态页:

接下来,修改宿主机上的index.html文件再次查看网站

# vi index.html
<head>
<title>Test website</title>
</head>
<body>
<h1>This is a test website for Docker</h1>
</body>

 刷新浏览器,可以看到,Sample网站已经更新了。

显然这个修改太简单了,不过可以看出,更复杂的修改也并不困难。更重要的是,你正在测试网站的运行环境,完全是生产环境里的真实状态。现在可以给每个用于生产的网站服务环境(如Apache、Nginx)配置一个容器,给不同开发框架的运行环境(如PHP或者Ruby on Rails)配置一个容器,或者给后端数据库配置一个容器,等等。

使用Docker构建并测试Web应用程序

看一个更复杂的例子,接下来我们将要测试一个基于Sinatra的Web应用程序,而不是静态网站。下面的例子会演示如何在Docker里开发并测试应用程序。这个应用程序会接收输入参数,并使用JSON散列输出这些参数。

# mkdir -p /opt/sinatra
# cd /opt/sinatra

# vim Dockerfile
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
ENV REFRESHED_AT 2017-05-25
RUN apt-get update
RUN apt-get -y install ruby ruby-dev build-essential redis-tools
RUN gem install --no-rdoc --no-ri sinatra json redis
RUN mkdir -p /opt/webapp
EXPOSE 4567
CMD ["/opt/webapp/bin/webapp"]

这里基于ubuntu创建了一个新的镜像,安装了Ruby和RubyGem,并且使用gem命令安装了sinatra、json和redis包。还创建了一个目录来存放新的Web应用程序,并公开了WEBrick的默认端口4567。最后,使用CMD指定/opt/webapp/bin/webapp作为Web应用程序的启动文件。

## 通过docker build构建新的镜像
# docker build -t="test/sinatra" .

## 构建应用程序代码
# mkdir -p webapp/bin
# mkdir -p webapp/lib
# vim webapp/bin/webapp
#!/usr/bin/ruby
$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))
require \'app\'
App.run!

# chmod +x webapp/bin/webapp


# vim webapp/lib/app.rb
require "rubygems"
require "sinatra"
require "json"
class App < Sinatra::Application
  set :bind, \'0.0.0.0\'
  get \'/\' do
    "<h1>DockerBook Test Sinatra app</h1>"
  end
  post \'/json/?\' do
    params.to_json
  end
end

通过docker run命令从镜像创建一个新的容器:

# docker run -d -p 4567 --name webapp -v /opt/sinatra/webapp:/opt/webapp test/sinatra
5f1b5d2069eb443427c8f13318fb115ec6fb4a23f71877ada982ba1c2bbfee61

这里通过之前构建的test/sinatra镜像,创建了一个名为webapp的容器。指定了一个新卷/opt/sinatra/webapp来存放新的Sinatra Web应用程序,并将到这个卷挂载到webapp容器的/opt/webapp目录。

我们可以通过以下方式查看容器的状态及一些基本信息:

# docker logs -f webapp
[2017-06-08 05:28:13] INFO  WEBrick 1.3.1
[2017-06-08 05:28:13] INFO  ruby 2.3.1 (2016-04-26) [x86_64-linux-gnu]
== Sinatra (v2.0.0) has taken the stage on 4567 for development with backup from WEBrick
[2017-06-08 05:28:13] INFO  WEBrick::HTTPServer#start: pid=1 port=4567

# docker top webapp
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                76521               1360                0                   13:28               ?                   00:00:00            /usr/bin/ruby /opt/webapp/bin/webapp

# docker port webapp 4567
0.0.0.0:32769

现在可以使用curl命令来测试这个应用程序:

# curl -i -H \'Accept: application/json\' -d \'name=Foo&status=Bar\' http://localhost:32769/json
HTTP/1.1 200 OK 
Content-Type: text/html;charset=utf-8
Content-Length: 29
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Thu, 08 Jun 2017 05:39:18 GMT
Connection: Keep-Alive

{"name":"Foo","status":"Bar"}

可以看到,我们个Sinatra应用程序传入一些参数,并看到这些参数转化成JSON散列后的输出:{"name":"Foo","status":"Bar"}

然后试试看,添加一个服务(这个服务运行在另一个容器里),能不能把当前的示例应用程序容器扩展为真正的应用程序栈。

构建Redis镜像和容器

现在我们将要扩展Sinatra应用程序,加入Redis后端数据库,并在Redis数据库中存储输入的参数。为了达到这个目的,要构建全新的镜像和容器来运行Redis数据库。之后,要利用Docker的特性来关联两个容器。

为了构建Redis数据库,要创建一个新的镜像。从一个新的Dockerfile开始,逐步让Redis运行在Docker里:

## 创建Dockerfile文件
# cat Dockerfile 
FROM ubuntu:latest
MAINTAINER Bourbon Tian "bourbon@1mcloud.com"
ENV REFRESHED_AT 2017-05-26
RUN apt-get update
RUN apt-get -y install redis-server redis-tools
EXPOSE 6379
ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

## 构建镜像并创建容器
# docker build -t="test/redis" .
# docker run -d -p 6379 --name redis test/redis

## 测试redis容器是否正常运行
# docker port redis 6379
0.0.0.0:32770
# redis-cli -h 127.0.0.1 -p 32770
redis 127.0.0.1:32770> 

这里使用redis客户端连接到127.0.0.1的32770端口,验证Redis服务器正在正常工作。

连接到Redis容器

现在来更新Sinatra应用程序,让其连接到Redis并存储传入的参数。为此,需要能够与Redis服务器对话。要做到这一点,可以有几种方法。来看看每种方法的优劣。

第一种方法涉及Docker自己的网络栈。到目前为止,我们看到的Docker容器都是公开端口并绑定到本地网络接口的,这样可以把容器里的服务在本地Docker宿主机所在的外部网络上(比如,把容器的80端口绑到本地宿主机的更高端口上)公开。除了这种用法,Docker这个特性还有种用法我们没见过,那就是内部网络。

在安装Docker时,会创建一个新的网络接口,名字是docker0。每个Docker容器都会在这个接口上分配一个IP地址。

# ifconfig docker0
docker0   Link encap:Ethernet  HWaddr 56:84:7A:FE:97:99  
          inet addr:172.17.42.1  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:6479 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7662 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:361421 (352.9 KiB)  TX bytes:26025103 (24.8 MiB)

docker0接口有符合RFC1918的私有IP地址,范围是172.16~172.30(如果子网被占用,Docker会在172.16~172.30这个范围内尝试创建子网。)。接口本身的地址是172.17.42.1是这个Docker网络的网关地址,也是所有Docker容器的网关地址。接口docker0是一个虚拟的以太网桥,用于连接容器和本地宿主网络。如果进一步的查看Docker宿主机的其他网络接口会发现一些列名字以veth开头的接口,

vethfcf286d Link encap:Ethernet  HWaddr C2:E7:4B:50:5D:99  
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
...

vethff609e8 Link encap:Ethernet  HWaddr F2:D0:D2:D1:3A:8D  
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
...

Docker每创建一个容器就会创建一组互联的网络接口。这组接口就像管道的两端。这组接口其中一端作为容器的eth0接口,而另一端统一命名为类似vethfcf286d这种名字,作为宿主机的一个端口。这里可以吧veth接口认为是虚拟网线的一端。这个虚拟网线一端插在名为docker0的网桥上,另一端插在容器里。通过把每个veth*接口绑定到docker0网桥,Docker创建了一个虚拟子网,这个子网由宿主机和所有的Docker容器共享。

root@afda9da4b5dd:/# ifconfig 
eth0      Link encap:Ethernet  HWaddr 02:42:ac:11:00:03  
          inet addr:172.17.0.3  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:8062 errors:0 dropped:0 overruns:0 frame:0
          TX packets:6398 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:24717646 (24.7 MB)  TX bytes:438531 (438.5 KB)

可以看到,Docker给容器分配了IP地址172.17.0.3作为宿主虚拟接口的另一端。这样就能够让宿主网络和容器互相通信了。让我们从容器内跟踪对外通信的路由,看看是如何建立连接的:

root@afda9da4b5dd:/# apt-get install -yqq traceroute
...

root@afda9da4b5dd:/# traceroute www.baidu.com
traceroute to www.baidu.com (180.97.33.107), 30 hops max, 60 byte packets
 1  172.17.42.1 (172.17.42.1)  0.026 ms  0.006 ms  0.005 ms
 2  172.30.10.254 (172.30.10.254)  3.016 ms  3.120 ms  3.313 ms
...

可以看到,容器地址后的下一跳是宿主网络上docker0接口的网关IP172.17.42.1。

不过Docker网络还有另一个部分配置才能允许建立连接:防火墙规则和NAT配置。这些配置允许Docker在宿主网络和容器间路由。现在来查看一下宿主机上的IPTables NAT配置:

# iptables -t nat -L -n
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination         
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           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  --  0.0.0.0/0           !127.0.0.0/8         ADDRTYPE match dst-type LOCAL 

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination         
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0           
MASQUERADE  tcp  --  172.17.0.1           172.17.0.1          tcp dpt:5000 

Chain DOCKER (2 references)
target     prot opt source               destination         
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0           tcp dpt:5000 to:172.17.0.1:5000

这里有几个值得注意的IPTables规则。首先,我们注意到,容器默认是无法访问的。从宿主网络与容器通信时,必须明确指定打开的端口。下面我们以DNAT(即目标NAT)这个规则为例,这个规则把容器里的访问路由到Docker宿主机的5000端口。

连接Redis

# docker inspect redis
...
 "NetworkSettings": {
        "Bridge": "",
        "EndpointID": "d194571e408dfe4e94f3e4e2fa4ec048b03aeef1eb57d82b313d979d0d5f9f74",
        "Gateway": "172.17.42.1",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "HairpinMode": false,
        "IPAddress": "172.17.0.6",
        "IPPrefixLen": 16,
        "IPv6Gateway": "",
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "MacAddress": "02:42:ac:11:00:06",
        "NetworkID": "612b14a43b7dee666f72c8ff28c7ec0d1f8cb03c00ed6a67d8680bf18ac67844",
        "PortMapping": null,
        "Ports": {
            "6379/tcp": [
                {
                    "HostIp": "0.0.0.0",
                    "HostPort": "32770"
                }
            ]
        },
...

docker inspect 命令展示了Docker容器的细节,这些细节包括配置信息和网络状况。为了清晰,这个例子去掉了大部分信息,只展示了网络配置。也可以在命令里使用-f标志,只获取IP地址:

# docker inspect -f \'{{ .NetworkSettings.IPAddress}}\' redis
172.17.0.6

可以看到,容器的IP地址为172.17.0.16,并使用了docker0接口作为网关地址。还可以看到6379端口被映射到本地宿主机的32770端口。只是,因为运行在本地的Docker宿主机上,所以不是一定要用映射后的端口,也可以直接使用172.17.0.6地址与Redis服务器的6379端口通信:

# redis-cli -h 172.17.0.6
redis 172.17.0.6:6379> 

Docker默认会把公开的端口绑定到所有的网络接口上。因此,也可以通过localhost或者127.0.0.1来访问Redis服务器。

虽然第一眼看上去这是让容器互联的一个好方案,但可惜的是,这种方法有两个大问题:

  • 要在应用程序里对Redis容器的IP地址做硬编码
  • 如果重启容器,Docker会改变容器的IP地址
# docker restart redis
redis
# docker inspect -f \'{{ .NetworkSettings.IPAddress}}\' redis
172.17.0.7

让Docker容器互连

Docker有个叫做连接(link)的功能非常有用,这个功能可以把一个或多个容器连接起来,让其互相通信。

让一个容器和另一个连接起来只需要一个简单的流程。

# docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                     NAMES
4359ee0cc5fd        test/redis          "/usr/bin/redis-serv   3 days ago          Up 3 days           0.0.0.0:32771->6379/tcp   redis               
5f1b5d2069eb        test/sinatra        "/opt/webapp/bin/web   3 days ago          Up 3 days           0.0.0.0:32769->4567/tcp   webapp

## 重新建一个名叫redis的容器,因为名字必须唯一,所以在这里删除原来的redis容器
# docker stop 4359ee0cc5fd
4359ee0cc5fd
# docker rm 4359ee0cc5fd
4359ee0cc5fd

## 创建一个名为redis的容器
# docker run -d --name redis test/redis
6dc87a9b56e0959f4426eedfe0f4a03ab56b2a40c1f2a89eedb9527c95517048

## 同样重新创建一个webapp容器,删除原有的容器
# docker stop 5f1b5d2069eb 
5f1b5d2069eb
# docker rm 5f1b5d2069eb 
5f1b5d2069eb
    
## 创建一个名为webapp的容器,并将它连接到redis容器上
# docker run -p 4567 --name webapp -d --link redis:db -v /opt/sinatra/webapp:/opt/webapp test/sinatra

这里,使用了一个新的标志--link。 --link标志创建了两个容器间的父子连接。这个标志需要两个参数:一个是要连接的容器名字,另一个是连接后容器的别名。这个例子中,我们把新容器连接到redis容器,并使用db作为别名。别名让我们可以访问公开的信息,而无需关注底层容器的名字。连接让父容器有能力访问子容器,并且把子容器的一些连接细节分享给父容器,这些细节有助于配置应用程序并使用这个连接。

连接也能得到一些安全上的好处。注意到启动Redis容器时,并没有使用-p标志公开Redis的端口。因为不需要这么做。通过吧容器连接在一起,可以让父容器直接访问任意子容器的公开端口(比如,父容器webapp可以连接到子容器redis的6379端口)。更妙的是,只有使用--link标志连接到这个容器的容器才能连接到这个端口。容器的端口不需要对本地宿主机公开,现在我们已经拥有一个非常安全的模型。在这个模型里,容器化的应用程序限制了可被攻击的界面,减少了公开暴露的网络。

出于安全原因(或者其他的原因),还可以强制Docker只允许有连接的容器之间互相通信。需要在启动Docker守护进程时加上一个--icc=false标志,关闭所有没有连接的容器间的通信。

也可以把多个容器连接在一起。比如,如果想让这个Redis实力服务多个Web应用程序,可以把每个Web应用程序的容器和同一个redis容器连接在一起:

# docker run -p 4567 --name webapp2 --link redis:db
...

# docker run -p 4567 --name webapp3 --link redis:db
...

Docker在父容器里的以下两个地方写入了连接信息:

## 这里引入一个知识点,如何进入一个正在运行的后台容器
## 通过attach,但是它有一个缺点,只要这个连接终止,或者使用了exit命令,容器就会退出后台运行
# docker attach webapp


## 通过exec,这个命令使用exit命令后,不会退出后台
# docker exec docker
  • /etc/hosts文件中;
  • 包含连接信息的环境变量中。
root@b3360fcb0cb0:/# cat /etc/hosts 
172.17.0.9      b3360fcb0cb0
...
172.17.0.8      db 6dc87a9b56e0 redis

root@b3360fcb0cb0:/# ping db
PING db (172.17.0.8): 56 data bytes
64 bytes from 172.17.0.8: icmp_seq=0 ttl=64 time=0.234 ms
64 bytes from 172.17.0.8: icmp_seq=1 ttl=64 time=0.052 ms

再来看一下环境变量,其中一些以DB开头。Docker在连接webapp和redis容器时,自动创建了这些以DB开头的环境变量。以DB开头是因为DB是创建连接时使用的别名。

root@b3360fcb0cb0:/# env 
HOSTNAME=b3360fcb0cb0
DB_NAME=/webapp/db
DB_PORT_6379_TCP_PORT=6379
DB_PORT=tcp://172.17.0.8:6379
DB_PORT_6379_TCP=tcp://172.17.0.8:6379
...
DB_ENV_REFRESHED_AT=2017-05-26
DB_PORT_6379_TCP_ADDR=172.17.0.8
DB_PORT_6379_TCP_PROTO=tcp
...

这些自动创建的环境变量包含以下信息。

  • 子容器的名字
  • 容器里运行的服务所使用的协议、IP和端口号
  • 容器里运行的不同服务所指定的协议、IP和端口号
  • 容器里有Docker设置的环境变量的值

这些环境变量会随容器不同而变化,取决于容器是如何配置的(如容器的Dockerfile中里有ENV和EXPOSE指令定义的内容)。更重要的是,这些连接信息可以让容器内的应用程序使用相同的方法与别的容器进行连接,而不用关心被连接的容器的具体细节。

使用容器连接来通信

那么如何使用这个连接呢?给Sinatra应用程序加入一些连接信息,以便与Redis通信。有以下两种方法可以让应用程序连接到Redis。

  • 使用环境变量的一些连接信息。
  • 使用DNS和/etc/hosts信息。

先试试第一种方法,看看Web应用程序lib/app.rb文件是如何利用这些新的环境变量的:

# vim /opt/sinatra/webapp/lib/app.rb 
require "rubygems"
require "sinatra"
require "json"
require "redis"
require "uri"
class App < Sinatra::Application uri=URI.parse(ENV[\'DB_PORT\']) redis = Redis.new(:host => uri.host, :port => uri.port) set :bind, \'0.0.0.0\' get \'/\' do "<h1>DockerBook Test Redis-enabled Sinatra app</h1>" end get \'/json\' do params = redis.get "params" params.to_json end post \'/json/?\' do redis.set "params", [params].to_json params.to_json end end

这里使用Ruby的URI模块来解析DB_PORT环境变量,并使用解析后的结果来配置Redis连接。应用程序现在可以使用这个链接信息找到相连接的Redis容器。通过环境变量,这里不再需要硬编码IP地址和端口来进行连接。这是一种发现服务的方法。

另外一种方法,可以使用本地DNS:

require "rubygems"
require "sinatra"
require "json"
require "redis"

class App < Sinatra::Application

      redis = Redis.new(:host => \'db\', :port => \'6379\')

      set :bind, \'0.0.0.0\'

      get \'/\' do
        "<h1>DockerBook Test Redis-enabled Sinatra app</h1>"
      end

      get \'/json\' do
        params = redis.get "params"
        params.to_json
      end

      post \'/json/?\' do
        redis.set "params", [params].to_json
        params.to_json
      end
end

应用程序会在本地查找名叫db的主机,找到/etc/hosts文件里的项并解析到正确的IP地址。这也解决了硬编码IP地址的问题。

现在在宿主机上再次使用curl命令测试应用程序:

# curl -i -H \'Accept: application/json\' -d \'name=Foo&status=Bar\' http://localhost:32779/json
HTTP/1.1 200 OK 
Content-Type: text/html;charset=utf-8
Content-Length: 29
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Mon, 12 Jun 2017 08:49:36 GMT
Connection: Keep-Alive

{"name":"Foo","status":"Bar"}

现在来确认下Redis实力接收到了这个更新:

# curl -i http://localhost:32779/json
HTTP/1.1 200 OK 
Content-Type: text/html;charset=utf-8
Content-Length: 41
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Mon, 12 Jun 2017 08:51:10 GMT
Connection: Keep-Alive

"[{\\"name\\":\\"Foo\\",\\"status\\":\\"Bar\\"}]"

总结

这个Web应用程序由以下几个部分组成:

  • 一个运行Sinatra的Web服务器容器。
  • 一个Redis数据库容器
  • 这两个容器间的一个安全连接

可以很容易把这个概念扩展到别的应用栈,并用其在本地开发中做复杂的管理,比如:

  • Wordpress、HTML、CSS和Javascript
  • Ruby on Rails
  • Django和Flask。
  • Node.js
  • Play!。
  • 你喜欢的其他框架

这样就可以在本地环境构建、复制、迭代开发用于生产的应用程序,甚至很复杂的多层应用程序。

以上是关于如何 在Docker窗口中部署PHP开发环境的主要内容,如果未能解决你的问题,请参考以下文章

docker-7 docker在阿里云的使用

基于Docker部署PHP7开发环境

dnmp一键部署搞定的php开发环境基于Docker的LNMP一键安装程序

使用 Docker 同时运行多个 Web 开发环境

docker镜像相关原理,镜像构建,Dockerfile常用命令

多个docker镜像部署lnmp开发环境