将网站迁移到 Docker

Docker 是一个 Linux 下的容器管理软件。每个容器某种意义上相当于一个 OpenVZ VPS,可以将服务器上的各个应用隔离开来。这种隔离有助于同一软件不同版本,或是互相冲突的软件在同一服务器上运行,比如 MySQL 5.7,MySQL 5.6 和 MariaDB 10.1 可以在同一台服务器上的三个 Docker 容器中运行。

但是 Docker 比 OpenVZ 优秀的地方在于,它对 Linux 内核的版本要求要宽松的多。OpenVZ 的内核至今为止停留在 2.6.32(稳定版)和 3.10(开发版),但是 Docker 可以在 3.10 以上的任何版本 Linux 内核运行。我的服务器现在运行 Linux 4.9 内核(为了 BBR),明显不能运行 OpenVZ,但是可以运行 Docker。

Docker 另一个优点是提供了一套非常完整的镜像仓库和自动化工具。在 OpenVZ 上,我必须分别登录每台 VPS,设置网络,apt-get,还要定期去每台 VPS 上备份数据。但是在 Docker 上,我可以直接使用现有的软件镜像(不用再 apt-get),并将数据文件夹直接映射到主机上(不用分别备份)。

Docker 更是提供 Docker Compose,以配置文件的形式设置多个 Docker 容器,并且快速部署、删除。

安装 Docker 以及 Docker Compose

我个人使用的系统是 Debian 8,可以直接从 Docker 官方的软件源安装。

apt-get install apt-transport-https ca-certificates gnupg2
apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb https://apt.dockerproject.org/repo debian-jessie main" >> /etc/apt/sources.list
apt-get update
apt-get install docker-engine
curl -L "https://github.com/docker/compose/releases/download/1.9.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

使用现有镜像迁移

我服务器上原来运行的软件是:Nginx,PHP7.0-FPM,MariaDB,Memcached,Redis 和 SS。为了统一管理,我使用 Docker 镜像的方式部署。

我决定把数据全部存在 /srv 文件夹中,因此 cd 进这个文件夹,并创建 docker-compose.yml 文件。这个就是 Docker Compose 的配置文件。其基本格式如下:(# 号后的内容为我添加的注释,在原文件中不存在)

version: '2'
services:
  容器 1:
    image: 容器的镜像名称,如果本地没有这个镜像,Docker 会自动去镜像仓库下载
    container_name: 容器名称
    environment: # 环境变量
      - 环境变量名称=环境变量值
      - PASSWORD=123456
    restart: always # 容器崩了就立即重启
    volumes: # 将服务器上的文件夹映射到 Docker 容器中,用于存储和统一管理数据
      - '服务器上存储数据的文件夹:Docker 容器中对应的文件夹'
      - '/var/lib/mysql:/var/lib/mysql'
    ports: # 端口映射
      - '服务器上监听的端口:Docker 容器内的端口' # 此例为将端口开放供任何人访问
      - '80:80'
      - '服务器上监听的地址:服务器上监听的端口:Docker 容器内的端口' # 此例为该服务仅允许在服务器上访问
      - '127.0.0.1:11211:11211'
  容器2: ...(和容器 1 相同)

接下来我们就要写一个 docker-compose.yml,运行 MariaDB 的镜像并将数据导入。(#号后的内容为我添加的注释,在原文件中不存在)

version: '2'
services:
  lantian-mariadb:
    image: mariadb:latest
    container_name: lantian-mariadb
    restart: always
    volumes:
      - '/srv/mysql:/var/lib/mysql'
      - '/etc/timezone:/etc/timezone' # 将服务器的时区设置应用到 Docker 容器中
      - '/etc/localtime:/etc/localtime'
    ports:
      - '127.0.0.1:3306:3306'

首先停止原来的 MariaDB:service mysql stop(我不考虑无缝切换问题)

然后把数据文件夹移过来:mv /var/lib/mysql /srv/mysql

最后 docker-compose up -d 加载这个配置文件

MariaDB 就在 Docker 里运行起来了!

同理,Redis 和 Memcached 都有现成的镜像,可以如法炮制。

从 apt-get 安装软件的镜像

对于 PHP7.0-FPM,问题要复杂一些。镜像仓库里的官方 PHP-FPM 加了奇怪的编译参数,配置文件的放置方式和我从 DotDeb 安装的不同。为了防止以后出现奇怪的问题,我决定做一个简单的 Docker 镜像。

做 Docker 镜像不需要你有什么高深的编程技巧。相反,你只要会基本的 bash 命令就行。

首先新建一个文件夹,cd 进去,创建一个 Dockerfile,这就是你的新镜像的构建配置文件。

Dockerfile 的基本格式如下:(# 号后面的是我自己加的)

FROM debian:jessie # 引用官方的 Debian 8 镜像,在此基础上执行下面的操作
MAINTAINER Lan Tian "lantian@lantian.pub" # 说明 Dockerfile 作者,这行删掉没关系
ADD somefile.txt /somefile.txt # 把一个文件添加进镜像,此例将当前文件夹下的 somefile.txt 添加进去。
RUN apt-get update # 在容器构建时运行一条命令,此例为 apt-get update
EXPOSE 80 # 指示这个 Docker 容器要开放某个端口供其它容器访问
ENTRYPOINT ["php-fpm7.0"] # 以后启动这个容器执行的命令,这个命令执行完成(程序退出)容器即停止

在这个新镜像的构建过程中,需要在 Debian 8 的基础上,添加 DotDeb 软件源,apt-get,添加自己的配置文件,开放端口供其它容器访问,设置启动命令。我的 Dockerfile 是这样的:

FROM debian:jessie
MAINTAINER Lan Tian "lantian@lantian.pub"
ADD dotdeb.gpg /dotdeb.gpg
RUN echo "deb http://packages.dotdeb.org jessie all" >> /etc/apt/sources.list \
    && echo "deb-src http://packages.dotdeb.org jessie all" >> /etc/apt/sources.list \
    && apt-key add /dotdeb.gpg \
    && apt-get update \
    && apt-get install -y php7.0-fpm php7.0-bz2 php7.0-curl php7.0-gd php7.0-json php7.0-mbstring php7.0-mcrypt php7.0-memcached php7.0-mysql php7.0-redis php7.0-sqlite3 php7.0-xml php7.0-xmlrpc php7.0-zip \
    && apt-get clean
ADD www.conf /etc/php/7.0/fpm/pool.d/www.conf
ADD php.ini /etc/php/7.0/fpm/php.ini
ADD php-fpm.conf /etc/php/7.0/fpm/php-fpm.conf
EXPOSE 9000
ENTRYPOINT ["php-fpm7.0"]

dotdeb.gpg 是 DotDeb 软件源的密钥,因为 Docker 官方的 Debian 镜像居然连 wget 都没有,我又不想浪费一次 apt-get update,就直接把密钥提前下载下来放在 Dockerfile 同文件夹里;www.conf,php.ini 和 php-fpm.conf 是我自己的配置文件,你在构建时,如果直接用默认配置,把对应的 ADD 行删掉即可;如果你要自定义配置,就把对应配置文件放在 Dockerfile 同文件夹里。

wget https://www.dotdeb.org/dotdeb.gpg
cp /etc/php/7.0/fpm/pool.d/www.conf ./
cp /etc/php/7.0/fpm/php.ini ./
cp /etc/php/7.0/fpm/php-fpm.conf ./

在当前文件夹开始构建:

docker build -t lt-php7-fpm .

docker-compose.yml 里加上这些内容:(注意空格,# 号后删掉)

lt-php-fpm:
  image: lt-php7-fpm
  container_name: lt-php-fpm
  restart: always
  volumes:
    - './www:/srv/www' # 将你的 www 文件夹映射进去
    - './owncloud:/srv/owncloud' # OwnCloud 数据文件夹,没有 OwnCloud 就删掉这行
    - '/etc/timezone:/etc/timezone' # 时区
    - '/etc/localtime:/etc/localtime' # 时区

自己编译软件镜像

我的 Nginx 要更麻烦一些,因为我给 Nginx 加了一堆奇怪的东西,包括 HTTP2+SPDY 补丁、Google PageSpeed、Certificate Transparency 补丁、Brotli 压缩格式之类的。很明显,我找不到现有的镜像,所以我只能自己编译了。

要把这些做成镜像,原理其实很简单:把你在编译过程中输的命令全部写进 Dockerfile 里。但是在你编译的时候,你是不是要输好几次软件的版本号,比如 cd nginx-1.11.8 之类的?在 Dockerfile 里,类似的版本号都可以写成变量,在以后升级时可以方便地修改。

在 Dockerfile 中可以如此使用变量:

ENV NGINX_VERSION=1.11.8 # 设置一个名为 NGINX_VERSION 的变量,值是 1.11.8
RUN cd nginx-${NGINX_VERSION} # ${NGINX_VERSION} 就等同于 1.11.8,此例中相当于 cd nginx-1.11.8

我们要做的事情:安装编译环境,下载 Nginx 和各个补丁的源代码,编译安装。我的 Dockerfile 如下:

FROM debian
MAINTAINER Lan Tian "lantian@lantian.pub"

ENV NGINX_VERSION=1.11.8 OPENSSL_VERSION=1.1.0c NPS_VERSION=1.12.34.2
RUN mkdir /tmp/build && cd /tmp/build \
    && apt-get update -q\
    && apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip \
    && wget http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
      && tar xvf nginx-${NGINX_VERSION}.tar.gz \
    && git clone https://github.com/bagder/libbrotli.git \
      && cd /tmp/build/libbrotli && ./autogen.sh && ./configure && make && make install && cd /tmp/build \
      && ln -s /usr/local/lib/libbrotlienc.so.1 /usr/lib/libbrotlienc.so.1 \
    && git clone https://github.com/google/ngx_brotli.git \
      && cd /tmp/build/ngx_brotli && git submodule update --init && cd /tmp/build/ \
    && git clone https://github.com/grahamedgecombe/nginx-ct.git \
    && wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \
      && tar xvf openssl-${OPENSSL_VERSION}.tar.gz \
    && wget https://github.com/pagespeed/ngx_pagespeed/archive/v${NPS_VERSION}-beta.zip \
      && unzip v${NPS_VERSION}-beta.zip \
      && cd /tmp/build/ngx_pagespeed-${NPS_VERSION}-beta/ \
      && wget https://dl.google.com/dl/page-speed/psol/${NPS_VERSION}-x64.tar.gz \
      && tar -xvf ${NPS_VERSION}-x64.tar.gz \
      && cd /tmp/build \
    && wget https://raw.githubusercontent.com/cujanovic/nginx-http2-spdy-patch/master/nginx-spdy-1.11.5%2B.patch -O nginx-spdy.patch \
    && cd nginx-${NGINX_VERSION} \
    && patch -p1 < /tmp/build/nginx-spdy.patch \
    && ./configure \
       --with-threads \
       --with-file-aio \
       --with-ipv6 \
       --with-http_ssl_module \
       --with-http_spdy_module \
       --with-http_v2_module \
       --with-http_gzip_static_module \
       --with-http_gunzip_module \
       --with-http_stub_status_module \
       --with-http_sub_module \
       --add-module=/tmp/build/ngx_pagespeed-${NPS_VERSION}-beta \
       --add-module=/tmp/build/nginx-ct \
       --add-module=/tmp/build/ngx_brotli \
       --with-openssl=/tmp/build/openssl-${OPENSSL_VERSION} \
       --with-cc-opt="-O3 -fPIE -fstack-protector-strong -Wformat -Werror=format-security -Wno-deprecated-declarations" \
    && make \
    && make install \
    && cd / && rm -rf /tmp/build \
    && apt-get purge -y unzip git autoconf libtool wget automake build-essential \
    && apt-get autoremove -y --purge \
    && ln -sf /dev/stdout /usr/local/nginx/logs/access.log \
      && ln -sf /dev/stderr /usr/local/nginx/logs/error.log
EXPOSE 80 443
ADD start.sh /start.sh
ENTRYPOINT /bin/bash /start.sh

这里涉及到一个问题,就是 nginx 默认后台运行。但是,一旦它后台运行,Docker 就认为它的主进程已经结束了,就把这个容器停掉了!因此,我们要用点小技巧。

start.sh 的内容如下:

#!/bin/bash
/usr/local/nginx/sbin/nginx
tail -F /usr/local/nginx/logs/access.log

tail -F 会不断地加载这个文件,并显示它的新内容。这个 start.sh 就是启动 nginx 并不断显示它的内容。只要 start.sh 不停,容器就能一直保持运行。

另外一点,在编译完之后,最好把编译环境和源代码都删掉,否则会无谓地增大镜像的占用空间。

开始构建:

docker build -t lt-nginx .

向 docker-compose.yml 添加:

lt-nginx:
  image: lt-nginx
  container_name: lt-nginx
  restart: always
  volumes:
    - './nginx:/usr/local/nginx/conf' # 配置文件
    - './www:/srv/www' # www 文件夹
    - '/etc/timezone:/etc/timezone' # 时区
    - '/etc/localtime:/etc/localtime' # 时区
  ports:
    - '80:80'
    - '443:443'

结语

在上面,我们建立了 MariaDB、PHP-FPM、Nginx 三个容器。以后它们可以用 docker-compose up -d 统一启动,docker-compose down 统一关闭并删除,极大地方便了管理和部署。