Dockerで古いLAMPサーバーを稼働させたまま安全にアップグレードしよう


# やりたいこと

わりと古いLAMP(Linux / Apache / MySQL / PHP)サーバーが現役稼働中なのでアップグレードしたい、でも新しいサーバーを買う金は無い、どうしよう？という状況があり、Dockerを使えば安全に素早くアップグレードできるのではないかと思いついて、やってみました。

# Dockerの導入

ここが一番難関です。古いサーバーにはDockerが導入できない場合があります。

以下面倒なのでrootで作業します。

## Dockerのインストール

まず[Install Docker](https://docs.docker.com/linux/step_one/)を見てサーバー上にDockerをインストールします。Ubuntuの場合、64bitでカーネルのバージョンが3.10以上でないとインストールできないようです。

Dockerは以下の一行だけでインストールできました。

```bash
curl -fsSL https://get.docker.com/ | sh
```

## 確認

```bash
docker run hello-world
```

と入力して`Hello from Docker.`以下ズラズラっとメッセージが表示されればOKです。

## イメージ操作

以下のように入力すると現在のDockerイメージ一覧を確認できます。

```bash
docker images
```

出力例です。

```
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-world         latest              690ed74de00f        5 months ago        960 B
```

先ほどの`docker run`コマンドにより、[Docker Hub](https://hub.docker.com/)からダウンロードされた[hello-worldイメージ](https://hub.docker.com/_/hello-world/)が表示されています。

## コンテナ操作

以下のように入力すると現在のDockerコンテナ一覧を確認できます。`-a`オプションは終了したコンテナも含めて表示します。

```bash
docker ps -a
```

出力例です。

```
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
0d000cf23953        hello-world         "/hello"            26 minutes ago      Exited (0) 26 minutes ago                       agitated_boyd
```

メッセージを表示してコンテナは終了しています。

## 不要なコンテナ＆イメージの削除

hello-worldコンテナとイメージはもう不要なので消しましょう。

以下のコマンドでコンテナを消すことができます。

```bash
docker rm CONTAINER ID or NAMES
```

以下のコマンドでイメージを消すことができます。

```bash
docker rmi IMAGE ID or REPOSITORY:TAG
```

面倒なら以下のようにすると全てのコンテナとイメージを削除できます。

```bash
docker rm -f $(docker ps -a -q) && docker rmi -f $(docker images -a -q)
```

# nginxコンテナの導入

元のWebサーバーはApacheでしたが、せっかくアップグレードするのでついでにカッコイイnginxにリプレースしてみましょう。

[Docker Hub](https://hub.docker.com/)から[nginxイメージ](https://hub.docker.com/_/hello-world/)をダウンロードしてコンテナを起動してみます。

```bash
docker run -p 8888:80 nginx
```

Webサーバーがフォアグラウンドで起動するのでコンソールは止まります。

コンテナ側では80番ポートでウェブサーバーが公開されます。ホスト側ではLAMPが稼働中で80番ポートは塞がっているので、`-p 8888:80`によって8888番ポートを公開するようにしています。

ブラウザで`http://Webサーバーのホスト:8888/`を開いてnginxの画面が表示されるか確認してください。コンソール画面にはnginxのアクセスログが表示されます。確認したら`Ctrl+C`で終了します。

# Docker Composeの導入

続いてnginxコンテナとphpコンテナを連携させるのですが、Docker単体だと面倒なので、[Docker Compose](https://docs.docker.com/compose/)という複数コンテナを簡単に連携できるツールを使います。

## Docker Composeのインストール

[Install Docker Compose](https://docs.docker.com/compose/install/)を参考にしてインストールします。以下の2行だけでいけました。

```bash
curl -L https://github.com/docker/compose/releases/download/1.6.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
```

## 定義ファイル用ディレクトリの作成

Docker Composeでは、いくつかの定義ファイルが必要になるので、ひとつ適当にディレクトリを作成します。場所はどこでも構いませんが、ディレクトリ名はコンテナ名のプレフィックスなどになるので気にしたほうが良いかもしれません。

以下、定義ファイル用のディレクトリを`/root/lamp/`とします。

```bash
mkdir /root/lamp
cd /root/lamp/
```

## docker-compose.yml の作成

Docker Composeの定義ファイルを作ります。`/root/lamp/`に以下のファイルを`docker-compose.yml`という名前で作成してください。

```yaml:docker-compose.yml
version: '2'
services:
  nginx:
    image: nginx
    ports:
      - '8888:80'
```

YAMLファイルなのでインデントは正確にしてください。

### docker-compose.ymlの解説

```
    image: nginx
```

Docker Hubよりnginxイメージをダウンロードします。

```
    ports:
      - '8888:80'
```

コンテナ側のポート80番をホスト側ポート8888番で公開します。

## Docker Composeの起動

```bash
docker-compose up
```

これで`docker-compose.yml`の定義に従ってコンテナが起動します。先ほどと同じようにブラウザで`http://Webサーバーのホスト:8888/`を開いてnginxの画面が表示されるか確認してください。確認したら`Ctrl+C`で終了します。

# nginxコンテナとphpコンテナの連携

次に、Docker Composeを使ってnginxコンテナとphpコンテナを連携してみましょう。

## Dockerfileを使ったnginxイメージのカスタマイズ

phpと連携するためにnginxイメージをカスタマイズします。Dockerfileを使うと既存のイメージをカスタマイズして新しいイメージを作成することができます。

### docker-compose.yml

`docker-compose.yml`を以下のように変更してください。

```yaml:docker-compose.yml
version: '2'
services:
  nginx:
    build: ./nginx
    ports:
      - '8888:80'
    depends_on:
      - php
  php:
    image: php:fpm
```

### docker-compose.ymlの解説

```
    build: ./nginx
```

nginxディレクトリにあるDockerfileを使いイメージを生成します。

```
    depends_on:
      - php
```

nginxサービスがphpサービスに依存していることを示しています。

```
  php:
    image: php:fpm
```

Docker Hubから[php-fpmイメージ](https://hub.docker.com/_/php/)を取得しています。

### nginx/Dockerfile

`/root/lamp/`内に`nginx`ディレクトリを作成して、その中に以下のファイルを作成してください。

```:nginx/Dockerfile
FROM nginx
COPY default.conf /etc/nginx/conf.d/
```

### nginx/Dockerfileの解説

```
FROM nginx
```

カスタマイズ元となるDocker Hubのイメージを指定します。

```
COPY default.conf /etc/nginx/conf.d/
```

新しく生成するイメージ内にファイルをコピーします。ここではnginxイメージに元々存在する`default.conf`を上書きしています。

### nginx/default.conf

`/root/lamp/nginx`ディレクトリに以下のファイルを作成してください。

```:nginx/default.conf
server {
    listen       80;
    server_name  _;
    root   /usr/share/nginx/html;

    location / {
        index  index.html;
    }

    location ~ \.php$ {
        fastcgi_pass   php:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /var/www/html/$fastcgi_script_name;
        include        fastcgi_params;
    }
}
```

もともと存在するdefault.confを上書きして、PHP-FPMとの連携を記述しています。

### nginx/default.confの解説

```
        fastcgi_pass   php:9000;
```

この「php」というのはphp:fpmコンテナの内部アドレスです。Docker Networkという機能を使って名前解決しています。

```
        fastcgi_param  SCRIPT_FILENAME  /var/www/html/$fastcgi_script_name;
```

`/var/www/html/`のphpファイルを読みに行くようにしています。

## 起動

```bash
docker-compose up -d
```

`-d`オプションはバックグラウンドでコンテナを起動します。

### コンテナ内にphpファイルを作成する

このままではphpファイルが無いので確認できません。そこで起動しているコンテナに入り、phpファイルを作成してみます。

まず起動しているコンテナを確認します。

```bash
docker ps
```

以下のような出力になります。

```
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                           NAMES
8b2c60187fac        php:fpm             "php-fpm"                3 minutes ago       Up 3 minutes        9000/tcp                        lamp_php_1
a6af857414c5        lamp_nginx          "nginx -g 'daemon off"   3 minutes ago       Up 3 minutes        443/tcp, 0.0.0.0:8888->80/tcp   lamp_nginx_1
```

最後のNAMESがコンテナ名です。
以下のコマンドでphpコンテナ内に入ります。

```bash
docker exec -ti lamp_php_1 bash
```

カレントディレクトリが`/var/www/html`であることを確認してphpファイルを作ります。

```bash
echo '<?php phpinfo();' > info.php
```

exitでコンテナから抜け、ブラウザで`http://Webサーバーのホスト:8888/info.php`を開き、phpinfoの画面が表示されることを確認してください。

## ホストのドキュメントルートディレクトリをコンテナにマウントする

以前のウェブサーバーは稼働中なので、ホスト側に`/var/www/html`等のドキュメントルートディレクトリがあると思います。そのディレクトリをコンテナにマウントしてみます。

```yaml:docker-compose.yml
version: '2'
services:
  nginx:
    build: ./nginx
    ports:
      - '8888:80'
    depends_on:
      - php
    volumes:
      - /var/www/html:/usr/share/nginx/html
  php:
    image: php:fpm
    volumes:
      - /var/www/html:/var/www/html
```

`volumes`でホスト側のディレクトリをコンテナ側にマウントすることができます。再度

```bash
docker-compose up -d
```

して、ブラウザで開いて確認してみてください。

# phpコンテナとmysqlコンテナの連携

同じようにして、mysqlコンテナを起動してphpコンテナからアクセスできるようにします。

## docker-compose.yml

`docker-compose.yml`を以下のように変更してください。

```yaml:docker-compose.yml
version: '2'
services:
  nginx:
    build: ./nginx
    ports:
      - '8888:80'
    depends_on:
      - php
    volumes:
      - /var/www/html:/usr/share/nginx/html
  php:
    build: ./php
    depends_on:
      - mysql
    volumes:
      - /var/www/html:/var/www/html
  mysql:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: pass
    volumes:
      - db:/var/lib/mysql
volumes:
  db:
```

## docker-compose.ymlの解説

```
    image: mysql
```

Docker Hubの公式のMySQLイメージを取得しています。

```
    environment:
      MYSQL_ROOT_PASSWORD: pass
```

rootのパスワードを「pass」に設定しています。

```
    volumes:
      - db:/var/lib/mysql
```

名前付きボリューム「db」にMySQLのデータが入るようにしています。

```
volumes:
  db:
```

名前付きボリューム「db」を定義しています。

## ボリュームについて

ボリュームはコンテナとは分離されています。

```bash
docker volume ls
```

でボリューム一覧を表示します。上で定義した名前付きボリュームは`lamp_db`という名前が付くと思いますので、

```bash
docker volume inspect lamp_db
```

で実体の場所が分かります。

## php/Dockerfile

`php`ディレクトリを作成し、その中にDockerfileを作成してください。

```:Dockerfile
FROM php:fpm

RUN apt-get update \
  && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng12-dev libmcrypt-dev \
  && docker-php-ext-install pdo_mysql mysqli mbstring gd iconv mcrypt json ftp zip
```

必要そうなPHPの拡張機能を追加しています。

## 確認

コンテナを起動してphpからmysqlに接続できるか確認します。

```bash
docker-compose up -d
```

/var/www/html/test.php あたりに以下のようなファイルを作ってエラーが出なければ接続されているでしょう。

```php
<?php new PDO('mysql:host=mysql;dbname=mysql', 'root', 'pass');
```

ホスト名が「mysql」になることに注意してください。

# 本番サーバーのリプレース

上記の設定にさらに細かい設定を追加したものが以下になります。

https://github.com/naga3/docker-nginx-php

こちらを元に本番サーバーをリプレースしてみましょう。

その前に、ポート8888番で本番同様に動くことをしっかり確認しておきます。
また、MySQLの認証や、phpinfo等の不要なファイルを消すなど、セキュリティ対策をしっかり行ってください。

## ポートを変更する

`docker-compose.yml`の

```
    ports:
      - '8888:80'
```

これを

```
    ports:
      - '80:80'
```

と変更します。

## Webサーバーを止める

ここからはスピード勝負になります。

例えばAPTでインストールしたなら以下のようにして止め、80番ポートを開放します。

```bash
service apache2 stop
```

## データベースのバックアップ

元のデータベースをmysqldumpなどでバックアップします。

## コンテナを再起動する

```bash
docker-compose up -d
```

でコンテナを再起動します。

## データベースのリストア

データベースをリストアします。

## 確認する

元のURLでサイトの不具合がないことを確認する。

# 最後に

Dockerを使えば一台のサーバーでもほぼダウンタイム無しでアップグレードできました。