├── .env ├── .remarkrc ├── Dockerfile.nginx ├── Dockerfile.php ├── README.md ├── docker-compose.yml ├── nginx.conf ├── php.conf ├── run1.sh ├── run2.sh ├── site └── index.php └── sources.list /.env: -------------------------------------------------------------------------------- 1 | DOCKER_USER=twang2218 2 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "remark-lint": { 4 | "no-multiple-toplevel-headings": false, 5 | "list-item-indent": false, 6 | "maximum-line-length": false 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile.nginx: -------------------------------------------------------------------------------- 1 | FROM nginx:1.11 2 | ENV TZ=Asia/Shanghai 3 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 4 | COPY ./site /usr/share/nginx/html 5 | -------------------------------------------------------------------------------- /Dockerfile.php: -------------------------------------------------------------------------------- 1 | FROM php:7-fpm 2 | 3 | ENV TZ=Asia/Shanghai 4 | 5 | COPY sources.list /etc/apt/sources.list 6 | 7 | RUN set -xe \ 8 | && echo "构建依赖" \ 9 | && buildDeps=" \ 10 | build-essential \ 11 | php5-dev \ 12 | libfreetype6-dev \ 13 | libjpeg62-turbo-dev \ 14 | libmcrypt-dev \ 15 | libpng12-dev \ 16 | " \ 17 | && echo "运行依赖" \ 18 | && runtimeDeps=" \ 19 | libfreetype6 \ 20 | libjpeg62-turbo \ 21 | libmcrypt4 \ 22 | libpng12-0 \ 23 | " \ 24 | && echo "安装 php 以及编译构建组件所需包" \ 25 | && DEBIAN_FRONTEND=noninteractive \ 26 | && apt-get update \ 27 | && apt-get install -y ${runtimeDeps} ${buildDeps} --no-install-recommends \ 28 | && echo "编译安装 php 组件" \ 29 | && docker-php-ext-install iconv mcrypt mysqli pdo pdo_mysql zip \ 30 | && docker-php-ext-configure gd \ 31 | --with-freetype-dir=/usr/include/ \ 32 | --with-jpeg-dir=/usr/include/ \ 33 | && docker-php-ext-install gd \ 34 | && echo "清理" \ 35 | && apt-get purge -y --auto-remove \ 36 | -o APT::AutoRemove::RecommendsImportant=false \ 37 | -o APT::AutoRemove::SuggestsImportant=false \ 38 | $buildDeps \ 39 | && rm -rf /var/cache/apt/* \ 40 | && rm -rf /var/lib/apt/lists/* 41 | 42 | COPY ./php.conf /usr/local/etc/php/conf.d/php.conf 43 | COPY ./site /usr/share/nginx/html 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LNMP - Docker 多容器间协作互连 2 | 3 | # 说明 4 | 5 | 这是一个 Docker 多容器间协作互连的例子。使用的是最常见的 LNMP 的技术栈,既 `Nginx` + `PHP` + `MySQL`。 6 | 7 | 在这个例子中,我使用的是 Docker Compose,这样比较简洁,如果使用 `docker` 命令也可以做到同样的效果,当然,过程要相对繁琐一些。 8 | 9 | ## 服务 10 | 11 | 在 `docker-compose.yml` 文件中,定义了3个**服务**,分别是 `nginx`, `php` 和 `mysql`。 12 | 13 | ```yml 14 | services: 15 | nginx: 16 | image: "${DOCKER_USER}/lnmp-nginx:v1.2" 17 | build: 18 | context: . 19 | dockerfile: Dockerfile.nginx 20 | ... 21 | php: 22 | image: "${DOCKER_USER}/lnmp-php:v1.2" 23 | build: 24 | context: . 25 | dockerfile: Dockerfile.php 26 | ... 27 | mysql: 28 | image: mysql:5.7 29 | ... 30 | ``` 31 | 32 | 其中 `mysql` 服务中的 `image: mysql:5.7` 是表明使用的是 `mysql:5.7` 这个镜像。而 `nginx` 和 `php` 服务中的 `image` 含义更为复杂。一方面是说,要使用其中名字的镜像,另一方面,如果这个镜像不存在,则利用其下方指定的 `build` 指令进行构建。在单机环境,这里的 `image` 并非必须,只保留 `build` 就可以。但是在 Swarm 环境中,需要集群中全体主机使用同一个镜像,每个机器自己构建就不合适了,指定了 `image` 后,就可以在单机 `build` 并 `push` 到 registry,然后在集群中执行 `up` 的时候,才可以自动从 registry 下载所需镜像。 33 | 34 | 这里的镜像名看起来也有些不同: 35 | 36 | ```bash 37 | image: "${DOCKER_USER}/lnmp-nginx:v1.2" 38 | ``` 39 | 40 | 其中的 `${DOCKER_USER}` 这种用法是环境变量替换,当存在环境变量 `DOCKER_USER` 时,将会用其值替换 `${DOCKER_USER}`。而环境变量从哪里来呢?除了在 Shell 中 `export` 对应的环境变量外,Docker Compose 还支持一个默认的环境变量文件,既 `.env` 文件。你可以在项目中看到,`docker-compose.yml` 的同级目录下,存在一个 `.env` 文件,里面定义了环境变量。 41 | 42 | ```bash 43 | DOCKER_USER=twang2218 44 | ``` 45 | 46 | 每次执行 `docker-compose` 命令的时候,这个 `.env` 文件就会自动被加载,所以是一个用来定制 compose 文件非常方便的地方。这里我只定义了一个环境变量 `DOCKER_USER`,当然,可以继续一行一个定义更多的环境变量。 47 | 48 | 初次之外,还可以明确指定环境变量文件。具体的配置请查看 [`docker-compose` 官方文档](https://docs.docker.com/compose/compose-file/#envfile)。 49 | 50 | ## 镜像 51 | 52 | ### mysql 服务镜像 53 | 54 | `mysql` 服务均直接使用的是 Docker 官方镜像。使用官方镜像并非意味着无法定制,Docker 官方提供的镜像,一般都具有一定的定制能力。 55 | 56 | ```yml 57 | mysql: 58 | image: mysql:5.7 59 | ... 60 | environment: 61 | TZ: 'Asia/Shanghai' 62 | MYSQL_ROOT_PASSWORD: Passw0rd 63 | command: ['mysqld', '--character-set-server=utf8'] 64 | ... 65 | ``` 66 | 67 | 在这个例子中,`mysql` 服务就通过环境变量 `MYSQL_ROOT_PASSWORD`,设定了 MySQL 数据库初始密码为 `Passw0rd`,并且通过 `TZ` 环境变量指定了国内时区。 68 | 69 | 并且,我重新指定了启动容器的命令,在 `command` 中,添加了额外的参数。`--character-set-server=utf8`,指定了默认字符集。 70 | 71 | ### nginx 服务镜像 72 | 73 | `nginx` 官方镜像基本满足需求,但是我们需要添加默认网站的配置文件、以及网站页面目录。 74 | 75 | ```Dockerfile 76 | FROM nginx:1.11 77 | ENV TZ=Asia/Shanghai 78 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 79 | COPY ./site /usr/share/nginx/html 80 | ``` 81 | 82 | 镜像定制很简单,就是指定时区后,将配置文件、网站页面目录复制到指定位置。 83 | 84 | ### php 服务镜像 85 | 86 | `php` 服务较为特殊,由于官方 `php` 镜像未提供连接 `mysql` 所需的插件,所以 `php` 服务无法直接使用官方镜像。在这里,正好用其作为例子,演示如何基于官方镜像,安装插件,定制自己所需的镜像。 87 | 88 | 对应的[`Dockerfile.php`](https://coding.net/u/twang2218/p/docker-lnmp/git/blob/master/Dockerfile.php): 89 | 90 | ```Dockerfile 91 | FROM php:7-fpm 92 | 93 | ENV TZ=Asia/Shanghai 94 | 95 | COPY sources.list /etc/apt/sources.list 96 | 97 | RUN set -xe \ 98 | && echo "构建依赖" \ 99 | && buildDeps=" \ 100 | build-essential \ 101 | php5-dev \ 102 | libfreetype6-dev \ 103 | libjpeg62-turbo-dev \ 104 | libmcrypt-dev \ 105 | libpng12-dev \ 106 | " \ 107 | && echo "运行依赖" \ 108 | && runtimeDeps=" \ 109 | libfreetype6 \ 110 | libjpeg62-turbo \ 111 | libmcrypt4 \ 112 | libpng12-0 \ 113 | " \ 114 | && echo "安装 php 以及编译构建组件所需包" \ 115 | && apt-get update \ 116 | && apt-get install -y ${runtimeDeps} ${buildDeps} --no-install-recommends \ 117 | && echo "编译安装 php 组件" \ 118 | && docker-php-ext-install iconv mcrypt mysqli pdo pdo_mysql zip \ 119 | && docker-php-ext-configure gd \ 120 | --with-freetype-dir=/usr/include/ \ 121 | --with-jpeg-dir=/usr/include/ \ 122 | && docker-php-ext-install gd \ 123 | && echo "清理" \ 124 | && apt-get purge -y --auto-remove \ 125 | -o APT::AutoRemove::RecommendsImportant=false \ 126 | -o APT::AutoRemove::SuggestsImportant=false \ 127 | $buildDeps \ 128 | && rm -rf /var/cache/apt/* \ 129 | && rm -rf /var/lib/apt/lists/* 130 | 131 | COPY ./php.conf /usr/local/etc/php/conf.d/php.conf 132 | COPY ./site /usr/share/nginx/html 133 | ``` 134 | 135 | 前面几行很简单,指定了基础镜像为 [`php:7-fpm`](https://hub.docker.com/_/php/),并且设定时区为中国时区,然后用[网易的 Debian 源](http://mirrors.163.com/.help/debian.html)替代默认的源,避免伟大的墙影响普通的包下载。接下来的那一个很多行的 `RUN` 需要特别的说一下。 136 | 137 | 初学 Docker,不少人会误以为 `Dockerfile` 等同于 Shell 脚本,于是错误的用了很多个 `RUN`,每个 `RUN` 对应一个命令。这是错误用法,会导致最终镜像极为臃肿。`Dockerfile` 是镜像定制文件,其中每一个命令都是在定义这一层该如何改变,因此应该[遵循最佳实践](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/),将同一类的东西写入一层,并且在结束时清理任何无关的文件。 138 | 139 | 这一层的目的是安装、构建 PHP 插件,因此真正所需要的是构建好的插件、以及插件运行所需要的依赖库,其它任何多余的文件都不应该存在。所以,在这里可以看到,依赖部分划分为了“构建依赖”以及“运行依赖”,这样在安装后,可以把不再需要的“构建依赖”删除掉,避免因为构建而导致这层多了一些不需要的文件。 140 | 141 | 这里使用的是官方 `php` 镜像中所带的 `docker-php-ext-install` 来安装 php 的插件,并且在需要时,使用 `docker-php-ext-configure` 来配置构建参数。这两个脚本是官方镜像中为了帮助镜像定制所提供的,很多官方镜像都有这类为镜像定制特意制作的脚本或者程序。这也是官方镜像易于扩展复用的原因之一,他们在尽可能的帮助使用、定制镜像。 142 | 143 | 更多关于如何定制镜像的信息可以从 Docker Hub 官方镜像的文档中看到: 144 | 145 | 最后的清理过程中,可以看到除了清除“构建依赖”、以及相关无用软件外,还彻底清空了 `apt` 的缓存。任何不需要的东西,都应该清理掉,确保这一层构建完毕后,仅剩所需的文件。 146 | 147 | 在 `Dockerfile` 的最后,复制配置文件和网页目录到指定位置。 148 | 149 | ## 网络 150 | 151 | 在这个例子中,演示了如何使用自定义网络,并利用服务名通讯。 152 | 153 | 首先,在 `docker-compose.yml` 文件尾部,全局 `networks` 部分定义了两个自定义网络,分别名为 `frontend`,`backend`。 154 | 155 | ```yml 156 | networks: 157 | frontend: 158 | backend: 159 | ``` 160 | 161 | 每个自定义网络都可以配置很多东西,包括网络所使用的驱动、网络地址范围等设置。但是,你可能会注意到这里 `frontend`、`backend` 后面是空的,这是指一切都使用默认,换句话说,在单机环境中,将意味着使用 `bridge` 驱动;而在 Swarm 环境中,使用 `overlay` 驱动,而且地址范围完全交给 Docker 引擎决定。 162 | 163 | 然后,在前面`services`中,每个服务下面的也有一个 `networks` 部分,这部分是用于定义这个服务要连接到哪些网络上。 164 | 165 | ```yml 166 | services: 167 | nginx: 168 | ... 169 | networks: 170 | - frontend 171 | php: 172 | ... 173 | networks: 174 | - frontend 175 | - backend 176 | mysql: 177 | ... 178 | networks: 179 | - backend 180 | 181 | ``` 182 | 183 | 在这个例子中, 184 | 185 | * `nginx` 接到了名为 `frontend` 的前端网络; 186 | * `mysql` 接到了名为 `backend` 的后端网络; 187 | * 而作为中间的 `php` 同时连接了 `frontend` 和 `backend` 网络上。 188 | 189 | 连接到同一个网络的容器,可以进行互连;而不同网络的容器则会被隔离。 190 | 所以在这个例子中,`nginx` 可以和 `php` 服务进行互连,`php` 也可以和 `mysql` 服务互连,因为它们连接到了同一个网络中; 191 | 而 `nginx` 和 `mysql` 并不处于同一网络,所以二者无法通讯,这起到了隔离的作用。 192 | 193 | 处于同一网络的容器,可以使用**服务名**访问对方。比如,在这个例子中的 `./site/index.php` 里,就是使用的 `mysql` 这个服务名去连接的数据库服务器。 194 | 195 | ```php 196 | 201 | ``` 202 | 203 | 可以注意到,在这段数据库连接的代码里,数据库密码是通过环境变量,`$_ENV["MYSQL_PASSWORD"]`,读取的,因此密码并非写死于代码中。在运行时,可以通过环境变量将实际环境的密码传入容器。在这个例子里,就是在 `docker-compose.yml` 文件中指定的环境变量: 204 | 205 | ```yml 206 | version: '2' 207 | services: 208 | ... 209 | php: 210 | ... 211 | environment: 212 | MYSQL_PASSWORD: Passw0rd 213 | ... 214 | ``` 215 | 216 | 关于 Docker 自定义网络,可以看一下官方文档的介绍: 217 | 218 | 219 | 关于在 Docker Compose 中使用自定义网络的部分,可以看官方这部分文档: 220 | 221 | 222 | ## 存储 223 | 224 | 在这三个服务中,`nginx` 和 `php` 都是无状态服务,它们都不需要本地存储。但是,`mysql` 是数据库,需要存储动态数据文件。我们知道 Docker 是要求容器存储层里不放状态,所有的状态(也就是动态的数据)的持久化都应该使用卷,在这里就是使用命名卷保存数据的。 225 | 226 | ```yaml 227 | volumes: 228 | mysql-data: 229 | ``` 230 | 231 | 在 `docker-compose.yml` 文件的后面,有一个全局的 `volumes` 配置部分,用于定义的是命名卷,这里我们定义了一个名为 `mysql-data` 的命名卷。这里卷的定义后还可以加一些卷的参数,比如卷驱动、卷的一些配置,而这里省略,意味着都使用默认值。也就是说使用 `local` 也就是最简单的本地卷驱动,将来建立的命名卷可能会位于 `/var/lib/docker/volumes` 下,不过不需要、也不应该直接去这个位置访问其内容。 232 | 233 | 在 `mysql` 服务的部分,同样有一个 `volumes` 配置,这里配置的是容器运行时需要挂载什么卷、或绑定宿主的目录。在这里,我们使用了之前定义的命名卷 `mysql-data`,挂载到容器的 `/var/lib/mysql`。 234 | 235 | ```yaml 236 | mysql: 237 | image: mysql:5.7 238 | volumes: 239 | - mysql-data:/var/lib/mysql 240 | ... 241 | ``` 242 | 243 | ## 依赖 244 | 245 | 服务的启动顺序有时候比较关键,Compose 在这里可以提供一定程度的启动控制。比如这个例子中,我是用了依赖关系 `depends_on` 来进行配置。 246 | 247 | ```yml 248 | 249 | services: 250 | nginx: 251 | ... 252 | depends_on: 253 | - php 254 | php: 255 | ... 256 | depends_on: 257 | - mysql 258 | mysql: 259 | ... 260 | ``` 261 | 262 | 在这里,`nginx` 需要使用 `php` 服务,所以这里依赖关系上设置了 `php`,而 `php` 服务则需要操作 `mysql`,所以它依赖了 `mysql`。 263 | 264 | 在 `docker-compose up -d` 的时候,会根据依赖控制服务间的启动顺序,对于这个例子,则会以 `mysql` → `php` → `nginx` 的顺序启动服务。 265 | 266 | 需要注意的是,这里的启动顺序的控制是有限度的,并非彻底等到所依赖的服务可以工作后,才会启动下一个服务。而是确定容器启动后,则开始启动下一个服务。因此,这里的顺序控制可能依旧会导致某项服务启动时,它所依赖的服务并未准备好。比如 `php` 启动后,有可能会出现 `mysql` 服务的数据库尚未初始化完。对于某些应用来说,这个控制,依旧可能导致报错说无法连接所需服务。 267 | 268 | 如果需要应用级别的服务依赖等待,需要在 `entrypoint.sh` 这类脚本中,加入服务等待的部分。而且,也可以通过 `restart: always` 这种设置,让应用启动过程中,如果依赖服务尚未准备好,而报错退出后,有再一次尝试的机会。 269 | 270 | 此外,Docker 支持健康检查,在 docker-compose.yml `v2` 的格式下,可以要求依赖条件对方服务启动完成: 271 | 272 | ```yaml 273 | depends_on: 274 | condition: service_healthy 275 | ``` 276 | 277 | 进一步信息,请参考官网文档:https://docs.docker.com/compose/compose-file/compose-file-v2/#depends_on 278 | 279 | # 单机操作 280 | 281 | ## 启动 282 | 283 | ```bash 284 | docker-compose up -d 285 | ``` 286 | 287 | *如果构建过程中,发现镜像下载极为缓慢、甚至失败。这是伟大的墙在捣乱。你需要去配置加速器,具体文章可以参看我的 [Docker 问答录](http://blog.lab99.org/post/docker-2016-07-14-faq.html#docker-pull-hao-man-a-zen-me-ban)。* 288 | 289 | 如果修改了配置文件,可能需要明确重新构建,可以使用命令 `docker-compose build`。 290 | 291 | ## 查看服务状态 292 | 293 | ```bash 294 | docker-compose ps 295 | ``` 296 | 297 | ## 查看服务日志 298 | 299 | ```bash 300 | docker-compose logs 301 | ``` 302 | 303 | ## 访问服务 304 | 305 | `nginx` 将会守候 `80` 端口, 306 | 307 | * 如果使用的 Linux 或者 `Docker for Mac`,可以直接在本机访问 308 | * 如果是使用 `Docker Toolbox` 的话,则应该使用虚拟机地址,如 ,具体虚拟机地址查询使用命令 `docker-machine ip default`。 309 | * 如果是自己安装的 Ubuntu、CentOS 类的虚拟机,直接进虚拟机查看地址。 310 | 311 | 如果访问后,看到了 `成功连接 MySQL 服务器` 就说明数据库连接正常。 312 | 313 | 314 | 315 | ## 停止服务 316 | 317 | ```bash 318 | docker-compose down 319 | ``` 320 | 321 | # Swarm 集群编排 322 | 323 | 在单机环境中使用容器,可能经常会用到绑定宿主目录的情况,这在开发时很方便。但是在集群环境中部署应用的时候,挂载宿主目录就变得非常不方便了。 324 | 325 | 在集群环境中,Swarm 可能会调度容器运行于任何一台主机上,如果一个主机失败后,可能还会再次调度到别的主机上,确保服务可以继续。在这种情况下,如果使用绑定宿主目录的形式,就必须同时在所有主机上的相同位置,事先准备好其内容,并且要保持同步。这并不是一个好的解决方案。 326 | 327 | 因此为了在集群环境中部署方便,比较好的做法是,将应用代码、配置文件等直接放入镜像。就如同这个例子中我们看到的 `nginx`、`php` 服务的镜像一样,在使用 `Dockerfile` 定制的过程中,将配置和应用代码放入镜像。 328 | 329 | `nginx` 的服务镜像 `Dockerfile` 330 | 331 | ```Dockerfile 332 | ... 333 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 334 | COPY ./site /usr/share/nginx/html 335 | ``` 336 | 337 | `php` 的服务镜像 `Dockerfile` 338 | 339 | ```Dockerfile 340 | ... 341 | COPY ./php.conf /usr/local/etc/php/conf.d/php.conf 342 | COPY ./site /usr/share/nginx/html 343 | ``` 344 | 345 | Docker Swarm 目前分为两代。第一代是以容器形式运行,被称为 Docker Swarm;而第二代是自 `1.12` 以后以 `SwarmKit` 为基础集成进 `docker` 的 Swarm,被称为 Docker Swarm Mode。 346 | 347 | ## 一代 Swarm 348 | 349 | [一代 Swarm](https://docs.docker.com/swarm/) 是 Docker 团队最早的集群编排的尝试,以容器形式运行,需要外置键值库(如 etcd, consul, zookeeper),需要手动配置 `overlay` 网络。其配置比 `kubernetes` 要简单,但是相比后面的第二代来说还是稍显复杂。 350 | 351 | 这里提供了一个脚本,`run1.sh`,用于建立一代 Swarm,以及启动服务、横向扩展。 352 | 353 | ### 建立 swarm 集群 354 | 355 | 在安装有 `docker-machine` 以及 VirtualBox 的虚拟机上(比如装有 Docker Toolbox 的Mac/Windows),使用 `run1.sh` 脚本即可创建集群: 356 | 357 | ```bash 358 | ./run1.sh create 359 | ``` 360 | 361 | ### 启动 362 | 363 | ```bash 364 | ./run1.sh up 365 | ``` 366 | 367 | ### 横向扩展 368 | 369 | ```bash 370 | ./run1.sh scale 3 5 371 | ``` 372 | 373 | 这里第一个参数是 nginx 容器的数量,第二个参数是 php 容器的数量。 374 | 375 | ### 访问服务 376 | 377 | `nginx` 将会守候 80 端口。利用 `docker ps` 可以查看具体集群哪个节点在跑 nginx 以及 IP 地址。如 378 | 379 | ```bash 380 | $ eval $(./run1.sh env) 381 | $ docker ps 382 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 383 | d85a2c26dd7d twang2218/lnmp-php:v1.2 "php-fpm" 9 minutes ago Up 9 minutes 9000/tcp node1/dockerlnmp_php_5 384 | c81e169c164d twang2218/lnmp-php:v1.2 "php-fpm" 9 minutes ago Up 9 minutes 9000/tcp node1/dockerlnmp_php_2 385 | b43de77c9340 twang2218/lnmp-php:v1.2 "php-fpm" 9 minutes ago Up 9 minutes 9000/tcp master/dockerlnmp_php_4 386 | fdcb718b6183 twang2218/lnmp-php:v1.2 "php-fpm" 9 minutes ago Up 9 minutes 9000/tcp node3/dockerlnmp_php_3 387 | 764b10b17dc4 twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off" 9 minutes ago Up 9 minutes 192.168.99.104:80->80/tcp, 443/tcp master/dockerlnmp_nginx_3 388 | e92b34f998bf twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off" 9 minutes ago Up 9 minutes 192.168.99.106:80->80/tcp, 443/tcp node2/dockerlnmp_nginx_2 389 | 077ee73c8148 twang2218/lnmp-nginx:v1.2 "nginx -g 'daemon off" 22 minutes ago Up 22 minutes 192.168.99.105:80->80/tcp, 443/tcp node3/dockerlnmp_nginx_1 390 | 1931249a66c1 e8920543aee8 "php-fpm" 22 minutes ago Up 22 minutes 9000/tcp node2/dockerlnmp_php_1 391 | cf71bca309dd mysql:5.7 "docker-entrypoint.sh" 22 minutes ago Up 22 minutes 3306/tcp node1/dockerlnmp_mysql_1 392 | ``` 393 | 394 | 如这种情况,就可以使用 , , 来访问服务。 395 | 396 | ### 停止服务 397 | 398 | ```bash 399 | ./run1.sh down 400 | ``` 401 | 402 | ### 销毁集群 403 | 404 | ```bash 405 | ./run1.sh remove 406 | ``` 407 | 408 | ## 二代 Swarm (Swarm Mode) 409 | 410 | [二代 Swarm](https://docs.docker.com/engine/swarm/),既 Docker Swarm Mode,是自 1.12 之后引入的原生的 Docker 集群编排机制。吸取一代 Swarm 的问题,大幅改变了架构,并且大大简化了集群构建。内置了分布式数据库,不在需要配置外置键值库;内置了内核级负载均衡;内置了边界负载均衡。 411 | 412 | 和一代 Swarm 的例子一样,为了方便说明,这里提供了一个 `run2.sh` 来帮助建立集群、运行服务。 413 | 414 | ### 建立 swarm 集群 415 | 416 | 在安装有 `docker-machine` 以及 VirtualBox 的虚拟机上(比如装有 Docker Toolbox 的Mac/Windows),使用 `run2.sh` 脚本即可创建集群: 417 | 418 | ```bash 419 | ./run2.sh create 420 | ``` 421 | 422 | *使用 Digital Ocean, AWS之类的云服务的话,就没必要本地使用 VirtualBox,不过需要事先配置好对应的 `docker-machine` 所需的环境变量。* 423 | 424 | ### 启动 425 | 426 | ```bash 427 | ./run2.sh up 428 | ``` 429 | 430 | ### 横向扩展 431 | 432 | ```bash 433 | ./run2.sh scale 10 5 434 | ``` 435 | 436 | 这里第一个参数是 nginx 容器的数量,第二个参数是 php 容器的数量。 437 | 438 | ### 列出服务状态 439 | 440 | 我们可以使用标准的命令列出所有服务以及状态: 441 | 442 | ```bash 443 | $ docker service ls 444 | ID NAME REPLICAS IMAGE COMMAND 445 | 2lnqjas6rov4 mysql 1/1 mysql:5.7 mysqld --character-set-server=utf8 446 | ahqktnscjlkl php 5/5 twang2218/lnmp-php:v1.2 447 | bhoodda99ebt nginx 10/10 twang2218/lnmp-nginx:v1.2 448 | ``` 449 | 450 | 我们也可以通过下面的命令列出具体的每个服务对应的每个容器状态: 451 | 452 | ```bash 453 | $ ./run2.sh ps 454 | + docker service ps -f desired-state=running nginx 455 | ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR 456 | 87xr5oa577hl9amelznpy7s7z nginx.1 twang2218/lnmp-nginx:v1.2 node2 Running Running 3 hours ago 457 | 7dwmc22qaftz0xrvijij9dnuw nginx.2 twang2218/lnmp-nginx:v1.2 node3 Running Running 22 minutes ago 458 | 00rus0xed3y851pcwkbybop80 nginx.3 twang2218/lnmp-nginx:v1.2 manager Running Running 22 minutes ago 459 | 5ypct2dnfu6ducnokdlk82dne nginx.4 twang2218/lnmp-nginx:v1.2 manager Running Running 22 minutes ago 460 | 7qshykjq8cqju0zt6yb9dkktq nginx.5 twang2218/lnmp-nginx:v1.2 node2 Running Running 22 minutes ago 461 | e2cux4vj2femrb3wc33cvm70n nginx.6 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 minutes ago 462 | 9uwbn5tm49k7vxesucym4plct nginx.7 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 minutes ago 463 | 6d8v5asrqwnz03hvm2jh96rq3 nginx.8 twang2218/lnmp-nginx:v1.2 node1 Running Running 22 minutes ago 464 | eh44qdsiv7wq8jbwh2sr30ada nginx.9 twang2218/lnmp-nginx:v1.2 node3 Running Running 22 minutes ago 465 | 51l7nirwtv4gxnzbhkx6juvko nginx.10 twang2218/lnmp-nginx:v1.2 node2 Running Running 22 minutes ago 466 | + docker service ps -f desired-state=running php 467 | ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR 468 | 4o3pqdva92vjdbfygdn0agp32 php.1 twang2218/lnmp-php:v1.2 manager Running Running 3 hours ago 469 | bf3d6g4rr8cax4wucu9lixgmh php.2 twang2218/lnmp-php:v1.2 node3 Running Running 22 minutes ago 470 | 9xq9ozbpea7evllttvyxk7qtf php.3 twang2218/lnmp-php:v1.2 manager Running Running 22 minutes ago 471 | 8umths3p8rqib0max6b6wiszv php.4 twang2218/lnmp-php:v1.2 node2 Running Running 22 minutes ago 472 | 0fxe0i1n2sp9nlvfgu4xlc0fx php.5 twang2218/lnmp-php:v1.2 node1 Running Running 22 minutes ago 473 | + docker service ps -f desired-state=running mysql 474 | ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR 475 | 3ozjwfgwfcq89mu7tqzi1hqeu mysql.1 mysql:5.7 node3 Running Running 3 hours ago 476 | ``` 477 | 478 | ### 访问服务 479 | 480 | `nginx` 将会守候 80 端口,由于二代 Swarm 具有边界负载均衡 (Routing Mesh, Ingress Load balance),因此,集群内所有节点都会守护 80 端口,无论是 Manager 还是 Worker,无论是否有 `nginx` 容器在其上运行。当某个节点接到 80 端口服务请求后,会自动根据容器所在位置,利用 overlay 网络将请求转发过去。因此,访问任意节点的 80 端口都应该可以看到服务。 481 | 482 | 通过下面的命令可以列出所有节点,访问其中任意地址都应该可以看到应用页面: 483 | 484 | ```bash 485 | $ ./run2.sh nodes 486 | manager http://192.168.99.101 487 | node1 http://192.168.99.103 488 | node2 http://192.168.99.102 489 | node3 http://192.168.99.104 490 | ``` 491 | 492 | ### 停止服务 493 | 494 | ```bash 495 | ./run2.sh down 496 | ``` 497 | 498 | ### 销毁集群 499 | 500 | ```bash 501 | ./run2.sh remove 502 | ``` 503 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | nginx: 4 | image: "${DOCKER_USER}/lnmp-nginx:v1.2" 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.nginx 8 | ports: 9 | - "80:80" 10 | networks: 11 | - frontend 12 | depends_on: 13 | - php 14 | php: 15 | image: "${DOCKER_USER}/lnmp-php:v1.2" 16 | build: 17 | context: . 18 | dockerfile: Dockerfile.php 19 | networks: 20 | - frontend 21 | - backend 22 | environment: 23 | MYSQL_PASSWORD: Passw0rd 24 | depends_on: 25 | - mysql 26 | mysql: 27 | image: mysql:5.7 28 | volumes: 29 | - mysql-data:/var/lib/mysql 30 | environment: 31 | TZ: 'Asia/Shanghai' 32 | MYSQL_ROOT_PASSWORD: Passw0rd 33 | command: ['mysqld', '--character-set-server=utf8'] 34 | networks: 35 | - backend 36 | volumes: 37 | mysql-data: 38 | 39 | networks: 40 | frontend: 41 | backend: 42 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | server_name _; 5 | 6 | charset utf-8; 7 | 8 | root /usr/share/nginx/html; 9 | index index.php index.html index.htm; 10 | 11 | gzip on; 12 | gzip_disable "msie6"; 13 | gzip_types text/plain text/css text/xml text/javascript application/json 14 | application/x-javascript application/xml application/xml+rss application/javascript; 15 | 16 | error_page 404 = /index.php; 17 | 18 | access_log off; 19 | error_log /var/log/nginx/error.log crit; 20 | 21 | client_max_body_size 64m; 22 | 23 | location / { 24 | try_files $uri $uri/ /index.php?$args; 25 | } 26 | 27 | location /. { 28 | return 404; 29 | } 30 | 31 | location ~ \.php$ { 32 | include fastcgi_params; 33 | try_files $uri =404; 34 | 35 | fastcgi_pass php:9000; 36 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 37 | fastcgi_read_timeout 300; 38 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 39 | fastcgi_index index.php; 40 | } 41 | 42 | location ~ /\.ht { 43 | deny all; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /php.conf: -------------------------------------------------------------------------------- 1 | [Date] 2 | date.timezone = Asia/Shanghai 3 | -------------------------------------------------------------------------------- /run1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Swarm Size. (default is 3) 4 | if [ -z "${SWARM_SIZE}" ]; then 5 | SWARM_SIZE=3 6 | fi 7 | 8 | # By default, 'virtualbox' will be used, you can set 'MACHINE_DRIVER' to override it. 9 | if [ -z "${MACHINE_DRIVER}" ]; then 10 | export MACHINE_DRIVER=virtualbox 11 | fi 12 | 13 | # REGISTRY_MIRROR_OPTS="--engine-registry-mirror https://jxus37ac.mirror.aliyuncs.com" 14 | INSECURE_OPTS="--engine-insecure-registry 192.168.99.0/24" 15 | # STORAGE_OPTS="--engine-storage-driver overlay2" 16 | 17 | MACHINE_OPTS="${STORAGE_OPTS} ${INSECURE_OPTS} ${REGISTRY_MIRROR_OPTS}" 18 | 19 | ############################## 20 | # Image Management # 21 | ############################## 22 | 23 | function publish() { 24 | # Get username 25 | REGISTRY_USER=$(docker info | awk '/Username/ { print $2 }') 26 | 27 | if [ -z "${REGISTRY_USER}" ]; then 28 | # Login first, so we can get the user name directly 29 | echo "Please login first: 'docker login'" 30 | exit 1 31 | fi 32 | 33 | # Build & Push 34 | # Just remember replace the 'twang2218' in the '.env' with your hub username. 35 | docker-compose build && docker-compose push 36 | } 37 | 38 | ############################## 39 | # Swarm Cluster Preparation # 40 | ############################## 41 | 42 | function create_assistant() { 43 | NAME=$1 44 | docker-machine create ${MACHINE_OPTS} ${NAME} 45 | eval "$(docker-machine env ${NAME})" 46 | HostIP="$(docker-machine ip ${NAME})" 47 | 48 | echo "Create etcd as a Key-value store" 49 | export KVSTORE="etcd://${HostIP}:2379" 50 | docker run -d \ 51 | -p 4001:4001 -p 2380:2380 -p 2379:2379 \ 52 | --restart=always \ 53 | --name etcd \ 54 | twang2218/etcd:v2.3.7 \ 55 | --initial-advertise-peer-urls http://${HostIP}:2380 \ 56 | --initial-cluster default=http://${HostIP}:2380 \ 57 | --advertise-client-urls http://${HostIP}:2379,http://${HostIP}:4001 \ 58 | --listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \ 59 | --listen-peer-urls http://0.0.0.0:2380 60 | 61 | echo "Create a registry mirror" 62 | docker run -d \ 63 | -p 5000:5000 \ 64 | -e REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory \ 65 | -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \ 66 | --name registry \ 67 | registry 68 | REGISTRY_MIRROR_OPTS="--engine-registry-mirror http://${HostIP}:5000" 69 | export MACHINE_OPTS="${MACHINE_OPTS} ${REGISTRY_MIRROR_OPTS}" 70 | } 71 | 72 | function create_master() { 73 | NAME=$1 74 | echo "kvstore is ${KVSTORE}" 75 | # eth1 on virtualbox, eth0 on digitalocean 76 | docker-machine create ${MACHINE_OPTS} \ 77 | --swarm \ 78 | --swarm-discovery=${KVSTORE} \ 79 | --swarm-master \ 80 | --engine-opt="cluster-store=${KVSTORE}" \ 81 | --engine-opt="cluster-advertise=eth1:2376" \ 82 | ${NAME} 83 | } 84 | 85 | function create_node() { 86 | NAME=$1 87 | echo "kvstore is ${KVSTORE}" 88 | # eth1 on virtualbox, eth0 on digitalocean 89 | docker-machine create ${MACHINE_OPTS} \ 90 | --swarm \ 91 | --swarm-discovery=${KVSTORE} \ 92 | --engine-opt="cluster-store=${KVSTORE}" \ 93 | --engine-opt="cluster-advertise=eth1:2376" \ 94 | ${NAME} 95 | } 96 | 97 | function create() { 98 | create_assistant assistant 99 | create_master master 100 | for i in $(seq 1 ${SWARM_SIZE}) 101 | do 102 | create_node node${i} & 103 | done 104 | 105 | wait 106 | } 107 | 108 | function remove() { 109 | for i in $(seq 1 ${SWARM_SIZE}) 110 | do 111 | docker-machine rm -y node${i} || true 112 | done 113 | docker-machine rm -y master || true 114 | docker-machine rm -y assistant || true 115 | } 116 | 117 | ############################## 118 | # Service Management # 119 | ############################## 120 | 121 | function up() { 122 | eval "$(docker-machine env --swarm master)" 123 | # Pull Images from hub 124 | docker-compose pull 125 | # Start Image 126 | docker-compose up -d 127 | } 128 | 129 | function scale() { 130 | NGINX_SIZE=$1 131 | PHP_SIZE=$2 132 | 133 | if [ -z "${NGINX_SIZE}" ]; then 134 | echo "Usage: scale [php_size]"; exit 1 135 | elif [ "${NGINX_SIZE}" -gt "${SWARM_SIZE}" ]; then 136 | SCALE_NGINX="nginx=${SWARM_SIZE}" 137 | else 138 | SCALE_NGINX="nginx=${NGINX_SIZE}" 139 | fi 140 | 141 | if [ "${PHP_SIZE}" -gt 1 ]; then 142 | SCALE_PHP="php=${PHP_SIZE}" 143 | fi 144 | 145 | eval "$(docker-machine env --swarm master)" 146 | set -xe 147 | docker-compose scale ${SCALE_NGINX} ${SCALE_PHP} 148 | set +xe 149 | } 150 | 151 | function down() { 152 | eval "$(docker-machine env --swarm master)" 153 | docker-compose down 154 | } 155 | 156 | ############################## 157 | # Entrypoint # 158 | ############################## 159 | 160 | function main() { 161 | Command=$1 162 | shift 163 | case "${Command}" in 164 | create) create ;; 165 | remove) remove ;; 166 | up) up ;; 167 | scale) scale "$@" ;; 168 | env) docker-machine env --swarm master ;; 169 | down) down ;; 170 | publish) publish ;; 171 | *) echo "Usage: $0 "; exit 1 ;; 172 | esac 173 | } 174 | 175 | main "$@" 176 | -------------------------------------------------------------------------------- /run2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Swarm Size. (default is 3) 4 | if [ -z "${SWARM_SIZE}" ]; then 5 | SWARM_SIZE=3 6 | fi 7 | 8 | # By default, 'virtualbox' will be used, you can set 'MACHINE_DRIVER' to override it. 9 | if [ -z "${MACHINE_DRIVER}" ]; then 10 | export MACHINE_DRIVER=virtualbox 11 | fi 12 | 13 | # REGISTRY_MIRROR_OPTS="--engine-registry-mirror https://jxus37ac.mirror.aliyuncs.com" 14 | INSECURE_OPTS="--engine-insecure-registry 192.168.99.0/24" 15 | # STORAGE_OPTS="--engine-storage-driver overlay2" 16 | 17 | MACHINE_OPTS="${STORAGE_OPTS} ${INSECURE_OPTS} ${REGISTRY_MIRROR_OPTS}" 18 | 19 | ############################## 20 | # Image Management # 21 | ############################## 22 | 23 | function publish() { 24 | # Get username 25 | REGISTRY_USER=$(docker info | awk '/Username/ { print $2 }') 26 | 27 | if [ -z "${REGISTRY_USER}" ]; then 28 | # Login first, so we can get the user name directly 29 | echo "Please login first: 'docker login'" 30 | exit 1 31 | fi 32 | 33 | # Build & Push 34 | # Just remember replace the 'twang2218' in the '.env' with your hub username. 35 | docker-compose build && docker-compose push 36 | } 37 | 38 | ##################################### 39 | # Swarm Mode Cluster Preparation # 40 | ##################################### 41 | 42 | function create_assistant() { 43 | NAME=$1 44 | docker-machine create ${MACHINE_OPTS} ${NAME} 45 | eval "$(docker-machine env ${NAME})" 46 | HostIP="$(docker-machine ip ${NAME})" 47 | 48 | echo "Create a registry mirror" 49 | docker run -d \ 50 | -p 5000:5000 \ 51 | -e REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory \ 52 | -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \ 53 | --name registry \ 54 | registry 55 | REGISTRY_MIRROR_OPTS="--engine-registry-mirror http://${HostIP}:5000" 56 | export MACHINE_OPTS="${MACHINE_OPTS} ${REGISTRY_MIRROR_OPTS}" 57 | } 58 | 59 | function create_manager() { 60 | # The first argument is manager node name 61 | NAME=$1 62 | # Using docker-machine create a docker host for swarm manager 63 | docker-machine create ${MACHINE_OPTS} ${NAME} 64 | # Load manager docker host environment 65 | eval "$(docker-machine env ${NAME})" 66 | # Get manager IP 67 | ManagerIP=`docker-machine ip ${NAME}` 68 | # Initialize a Swarm 69 | docker swarm init --advertise-addr ${ManagerIP} 70 | # Get Worker Token 71 | WorkerToken=`docker swarm join-token worker | grep token | awk '{ print $2 }'` 72 | echo "Worker's Token is: '${WorkerToken}'" 73 | # Exports 74 | export ManagerIP 75 | export WorkerToken 76 | } 77 | 78 | function create_node() { 79 | # The first argument is the node name 80 | NAME=$1 81 | # Using docker-machine create a docker host for swarm worker 82 | docker-machine create ${MACHINE_OPTS} ${NAME} 83 | # Load the worker docker host environment 84 | eval "$(docker-machine env ${NAME})" 85 | # Get the Worker IP 86 | WorkerIP=`docker-machine ip ${NAME}` 87 | # Join the Swarm as a Worker 88 | docker swarm join \ 89 | --token ${WorkerToken} \ 90 | --advertise-addr ${WorkerIP} \ 91 | ${ManagerIP}:2377 92 | } 93 | 94 | function create() { 95 | create_assistant assistant 96 | create_manager manager 97 | for i in $(seq 1 ${SWARM_SIZE}) 98 | do 99 | create_node node${i} & 100 | done 101 | 102 | wait 103 | } 104 | 105 | function remove() { 106 | for i in $(seq 1 ${SWARM_SIZE}) 107 | do 108 | docker-machine rm -y node${i} || true 109 | done 110 | docker-machine rm -y manager || true 111 | docker-machine rm -y assistant || true 112 | } 113 | 114 | ############################## 115 | # Service Management # 116 | ############################## 117 | 118 | function up() { 119 | # Load '.env' environment variables 120 | export $(cat .env | xargs) 121 | # Load Swarm Manager docker host environment 122 | eval "$(docker-machine env manager)" 123 | set -xe 124 | # Create Networks 125 | docker network create -d overlay frontend 126 | docker network create -d overlay backend 127 | # Start 'mysql' Service 128 | docker service create \ 129 | --name mysql \ 130 | -e TZ=Asia/Shanghai \ 131 | -e MYSQL_ROOT_PASSWORD=Passw0rd \ 132 | --mount src=mysql-data,dst=/var/lib/mysql \ 133 | --network backend \ 134 | mysql:5.7 \ 135 | mysqld --character-set-server=utf8 136 | # Start 'php' Service 137 | docker service create \ 138 | --name php \ 139 | -e MYSQL_PASSWORD=Passw0rd \ 140 | --network frontend \ 141 | --network backend \ 142 | "${DOCKER_USER}/lnmp-php:v1.2" 143 | # Start 'nginx' Service 144 | docker service create \ 145 | --name nginx \ 146 | --network frontend \ 147 | -p 80:80 \ 148 | "${DOCKER_USER}/lnmp-nginx:v1.2" 149 | # List Created Service 150 | docker service ls 151 | } 152 | 153 | function scale() { 154 | # The first argument is 'nginx' service replica number. 155 | NGINX_SIZE=$1 156 | # The second argument is 'php' service replica number. 157 | PHP_SIZE=$2 158 | 159 | # Load Swarm Manager Docker host environment 160 | eval "$(docker-machine env manager)" 161 | 162 | if [ -z "${NGINX_SIZE}" ]; then 163 | # We need at least 'nginx_size' to scale 164 | echo "Usage: scale [php_size]"; exit 1 165 | else 166 | echo "Scaling 'nginx' service to ${NGINX_SIZE} replicas ..." 167 | docker service update \ 168 | --replicas "${NGINX_SIZE}" \ 169 | nginx 170 | fi 171 | 172 | # We need at least 1 'php' replica. 173 | if [ "${PHP_SIZE}" -ge 1 ]; then 174 | echo "Scaling 'php' service to ${PHP_SIZE} replicas ..." 175 | docker service update \ 176 | --replicas "${PHP_SIZE}" \ 177 | php 178 | fi 179 | } 180 | 181 | function down() { 182 | # Load Swarm Manager Docker host environment 183 | eval "$(docker-machine env manager)" 184 | set -xe 185 | # Remove services 186 | docker service rm nginx php mysql 187 | # Remove networks 188 | docker network rm frontend backend 189 | } 190 | 191 | function ps() { 192 | # Load Swarm Manager Docker host environment 193 | eval "$(docker-machine env manager)" 194 | set -xe 195 | # List 'nginx' service tasks 196 | docker service ps -f desired-state=running nginx 197 | # List 'php' service tasks 198 | docker service ps -f desired-state=running php 199 | # List 'mysql' service tasks 200 | docker service ps -f desired-state=running mysql 201 | } 202 | 203 | function list_nodes() { 204 | echo "manager http://$(docker-machine ip manager)" 205 | for i in $(seq 1 ${SWARM_SIZE}) 206 | do 207 | echo "node${i} http://$(docker-machine ip node${i})" 208 | done 209 | } 210 | 211 | ############################## 212 | # Entrypoint # 213 | ############################## 214 | 215 | function main() { 216 | Command=$1 217 | shift 218 | case "${Command}" in 219 | create) create ;; 220 | remove) remove ;; 221 | up) up ;; 222 | scale) scale "$@" ;; 223 | env) docker-machine env manager ;; 224 | down) down ;; 225 | ps) ps ;; 226 | nodes) list_nodes ;; 227 | publish) publish ;; 228 | *) echo "Usage: $0 "; exit 1 ;; 229 | esac 230 | } 231 | 232 | main "$@" 233 | -------------------------------------------------------------------------------- /site/index.php: -------------------------------------------------------------------------------- 1 | 成功连接 MySQL 服务器" . PHP_EOL; 11 | mysqli_close($conn); 12 | 13 | // 使用 phpinfo() 显示完整服务端信息 14 | phpinfo(); 15 | ?> 16 | -------------------------------------------------------------------------------- /sources.list: -------------------------------------------------------------------------------- 1 | deb http://mirrors.163.com/debian/ jessie main 2 | deb http://mirrors.163.com/debian/ jessie-updates main 3 | deb http://mirrors.163.com/debian-security/ jessie/updates main 4 | --------------------------------------------------------------------------------