読者です 読者をやめる 読者になる 読者になる

SIerだけど技術やりたいブログ

5年目のSIerのブログです

PostgreSQLのDockerイメージの使い方

ホスト環境を汚さずにいろいろなバージョンのミドルウェアを構築するために、Dockerは非常に有効な手段だと思います。
この記事は、PostgreSQLのDockerイメージを使うために試行錯誤したときのメモです。

動作確認環境

$ cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
$ docker --version
Docker version 1.10.3, build d381c64-unsupported

2017/04/03 追記
さすがにdockerのバージョンが古かったので、新しめのCE版を入れて動作確認しました。
動きが違うところはバージョンごとに書いてます。
Get Docker for CentOS - Docker Documentation

$ cat /etc/redhat-release
CentOS Linux release 7.2.1511 (Core)
$ docker --version
Docker version 17.03.1-ce, build c6d412e

PostgreSQLの起動

$ docker run --name my-db -p 5432:5432 -e POSTGRES_USER=dev -e POSTGRES_PASSWORD=secret -d postgres:9.6
--name my-db

--nameでコンテナ名の指定ができる。
起動しているコンテナの名前は docker ps で確認できる。

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
f43f50193fa6        postgres:9.6        "docker-entrypoint..."   40 seconds ago      Up 38 seconds       0.0.0.0:5432->5432/tcp   my-db
-p 5432:5432

ポートフォワーディングしてほしければ、-pでバインディングする。
(-p ホストのポート:コンテナのポート)

# ホスト側の5432ポートに接続するとコンテナにフォワードされる
$ psql -h localhost -U dev
ユーザ dev のパスワード:
psql (9.6.1, サーバー 9.6.2)
"help" でヘルプを表示します.

dev=# \l
                                         データベース一覧
   名前    |  所有者  | エンコーディング |  照合順序  | Ctype(変換演算子) |      アクセス権
-----------+----------+------------------+------------+-------------------+-----------------------
 dev       | postgres | UTF8             | en_US.utf8 | en_US.utf8        |
 postgres  | postgres | UTF8             | en_US.utf8 | en_US.utf8        |
 template0 | postgres | UTF8             | en_US.utf8 | en_US.utf8        | =c/postgres          +

サーバロケールがen_US.utf8になっている。ja_JP.utf8にしたい人は変更が必要。
https://hub.docker.com/_/postgres/

-e POSTGRES_USER

スーパユーザ名(省略時は"postgres")

-e POSTGRES_PASSWORD

スーパユーザのパスワード(省略時はパスワードなしでログイン可)

-e POSTGRES_DB

PostgreSQL上のデータベース名(省略時はPOSTGRES_USERと同じ)
デフォルトだとユーザ名と同じデータベースが存在しないとエラーになるのであんまり変える機会なさそう。

-e PGDATA

PostgreSQLのデータの格納先ディレクトリ(省略時は/var/lib/postgresql/data)

※ -eコマンドでコンテナの環境変数を設定できる仕組みがDockerにあるらしい。
http://docs.docker.jp/engine/reference/run.html#env

以下のようなDockerfileを用意して試してみる。
docker runしたら環境変数が一覧表示されるはず。

$ cat Dockerfile
FROM centos:7
ENV HOGE hoge
ENTRYPOINT ["env"]

ビルドしてイメージを作る。

$ docker build -t xxx .

docker runしたら環境変数を一覧表示する。
ENVで指定したHOGEが出力される。

$ docker run xxx
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=c1844898c4c6
HOGE=hoge
HOME=/root

-eで環境変数を指定してみる。
Dockerfileで指定したENVが上書きされる。
また、-e FUGA=fuga で追加したFUGAも表示される。

$ docker run -e HOGE=fuga -e FUGA=fuga xxx
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d845c635858c
HOGE=fuga
FUGA=fuga
HOME=/root

この仕組みを利用してPostgreSQLのイメージは起動時に実行されるdocker-entrypoint.sh内でふるまいを変えているっぽい。
以下、PostgreSQL9.6イメージのdocker-entrypoint.shのソース抜粋。

file_env() {
        local var="$1"
        local fileVar="${var}_FILE"
        local def="${2:-}"
        if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
                echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
                exit 1
        fi
        local val="$def"
        if [ "${!var:-}" ]; then
                val="${!var}"
        elif [ "${!fileVar:-}" ]; then
                val="$(< "${!fileVar}")"
        fi
        export "$var"="$val"
        unset "$fileVar"
}
   ...

   file_env 'POSTGRES_USER' 'postgres'
   file_env 'POSTGRES_DB' "$POSTGRES_USER"

   psql=( psql -v ON_ERROR_STOP=1 )

   if [ "$POSTGRES_DB" != 'postgres' ]; then
           "${psql[@]}" --username postgres <<-EOSQL
                   CREATE DATABASE "$POSTGRES_DB" ;
           EOSQL
           echo
   fi

   if [ "$POSTGRES_USER" = 'postgres' ]; then
           op='ALTER'
   ...

起動時の動きを確認するときは docker inspect を利用する。

$ docker inspect postgres:9.6
...
  "WorkingDir": "",
  "Entrypoint": [
    "docker-entrypoint.sh"
  ],
...

起動しているコンテナに入れば、スクリプトディレクトリ構成を手軽に確認できる。

$ docker exec -it my-db /bin/bash
root@f43f50193fa6:/# ls
bin   dev                         docker-entrypoint.sh  home  lib64  mnt  proc  run   srv  tmp  var
boot  docker-entrypoint-initdb.d  etc                   lib   media  opt  root  sbin  sys  usr

PostgreSQLへの接続(コンテナを利用してpsqlを実行する)

このほうがDocker使ってるっぽくて個人的に好み。
psqlのバージョンも気軽にサーバ側バージョンに合わせられる。

$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U dev
Password for user dev:
psql (9.6.2)
Type "help" for help.

dev=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 dev       | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
--rm

実行が終わったらpsqlを実行したコンテナを破棄する。

--link my-db:db

--link コンテナ名:ホスト名 の形式で、起動するコンテナの/etc/hostsにホスト名が設定される。(このホスト名はlink先のコンテナのIPアドレスを指す)
https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/

/etc/hostsに設定されたホスト名を利用して psql -h の接続先ホストを指定する。

PostgreSQLの停止

stopするだけ。

$ docker stop my-db

PostgreSQLの再開

コンテナの$PGDATAディレクトリにデータが残っているので前のデータはそのまま。
ただし、コンテナを消すとデータも消える。

$ docker start my-db

データの永続化

データをコンテナと切り離して永続化したい場合、PostgreSQLのデータ保存先をData Volumeに指定するといいらしい。

Data Volumeを作成する。

$ docker volume create --name pgdata
pgdata

作成されたData Volumeを確認する。

$ docker volume inspect pgdata
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Options": {},
        "Scope": "local"
    }
]

起動時にData Volumeを指定する。

$ docker run -it --name my-db -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret -d postgres:9.6
$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U postgres

postgres=# create table book();
CREATE TABLE

こうするとコンテナが消えてもデータは残る。
コンテナを再度作成してData Volumeを割り当てれば昔のデータがそのまま見れる。

$ docker stop my-db
$ docker rm my-db
$ docker run -it --name my-db -v pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=secret -d postgres:9.6
$ docker run -it --rm --link my-db:db postgres psql -h db -U postgres:9.6

postgres=# \d
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+----------
 public | book | table | postgres

設定のカスタマイズ方法

設定ファイルの編集

コンテナからpostgresql.confをコピーする。

$ docker cp my-db:/var/lib/postgresql/data/postgresql.conf .

ホスト側でカスタマイズする。
今回はとりあえずmax_connectionsを100から5にしてみる。

$ cat postgresql.conf
...
max_connections = 5
...

編集した設定ファイルの配置

dockerのバージョンによってとった対処が違うので分けて記述する。

docker 1.10 向け

以下でコンテナ側にファイルを配置できると思ったけど、ダメ。

$ docker run -it --name my-db -v $(pwd)/postgresql.conf:/var/lib/postgresql/data/postgresql.conf -e POSTGRES_PASSWORD=secret postgres:9.6
chown: changing ownership of ‘/var/lib/postgresql/data/postgresql.conf’: Permission denied

なんでなんで?ググるとこんなのが。
DockerのVolumeのアクセス権限の問題について - Qiita

volumeで設定したファイルはuid,gidが1000になるらしい。たしかに。

$ docker run --rm -v $(pwd)/postgresql.conf:/postgresql.conf centos:7 ls -al
total 96
...
drwxr-xr-x.   2 root root  4096 Nov  5 15:38 mnt
drwxr-xr-x.   2 root root  4096 Nov  5 15:38 opt
-rw-------.   1 1000 1000 22203 Mar 31 21:08 postgresql.conf
dr-xr-xr-x. 317 root root     0 Mar 31 21:13 proc
dr-xr-x---.   2 root root  4096 Mar 15 20:00 root
...

じゃあrootで実行すればいいんでしょうね、と思ったらそんなこともなかった。

$ docker run --rm -v $(pwd)/postgresql.conf:/postgresql.conf centos:7 id
uid=0(root) gid=0(root) groups=0(root)
$ docker run --rm -v $(pwd)/postgresql.conf:/postgresql.conf centos:7 cat postgresql.conf
cat: postgresql.conf: Permission denied

ググるとこんなのが。selinuxの問題らしい。
Permission denied on accessing host directory in docker - Stack Overflow

ということで以下のコマンドを実行するとrootでは実行できるようになった。

$ chcon -Rt svirt_sandbox_file_t postgresql.conf
$ docker run --rm -v $(pwd)/postgresql.conf:/postgresql.conf centos:7 cat postgresql.conf
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
#   name = value

ということでselinuxの問題を解決してから再実行。

$ chcon -Rt svirt_sandbox_file_t postgresql.conf
$ docker run -it --name my-db -v $(pwd)/postgresql.conf:/var/lib/postgresql/data/postgresql.conf -e POSTGRES_PASSWORD=secret postgres:9.6
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

initdb: directory "/var/lib/postgresql/data" exists but is not empty
If you want to create a new database system, either remove or empty
the directory "/var/lib/postgresql/data" or run initdb
with an argument other than "/var/lib/postgresql/data".

またエラー。

PostgreSQLのinitdb実行前にPGDATAにファイルがおいてあるのが悪いっぽい。
Dockerがコンテナの/var/lib/postgresql/dataにVolumeを追加したあとにENTRYPOINT(docker-entrypoint.sh)が実行されるため、その中でinitdbした時にこけてると思われる。
https://github.com/docker-library/postgres/issues/105

PostgreSQL側でpostgresql.confを別ディレクトリから参照可能にするパラメータがあるため、それを利用する。

$ chcon -Rt svirt_sandbox_file_t postgresql.conf
$ docker run -it --name my-db -v $(pwd)/postgresql.conf:/etc/postgresql.conf -e POSTGRES_PASSWORD=secret  postgres:9.6 -c config_file=/etc/postgresql.conf
...
LOG:  autovacuum launcher shutting down
LOG:  shutting down
LOG:  database system is shut down
 done
server stopped

PostgreSQL init process complete; ready for start up.

LOG:  could not open configuration file "/etc/postgresql.conf": Permission denied
FATAL:  configuration file "/etc/postgresql.conf" contains errors

またまたエラー。

結局ファイルのuid,gidが1000なのでPostgreSQLを実行するユーザからは権限がない。
そのため、コンテナ内のpostgresユーザがアクセスできるようにファイルのパーミッションを変える必要がある。つまり、Dockerfileを用意する必要がある。

$ cat Dockerfile
FROM postgres
COPY postgresql.conf /etc/postgresql.conf
RUN chown -R postgres /etc/postgresql.conf

Dockerfileをビルドし、起動時に -c config_file=/etc/postgresql.conf を指定する。

$ docker build -t dev-postgres -f Dockerfile .
$ docker run -it --name my-db -e POSTGRES_PASSWORD=secret -d dev-postgres -c config_file=/etc/postgresql.conf

PostgreSQLに接続してmax_connectionsが変わってるか確認してみる。

$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U postgres
postgres=# show max_connections;
 max_connections
-----------------
 5
(1 row)

いけた!!!

Docker version 17.03.1-ce, build c6d412e 向け

以下でコンテナ側にファイルを配置できると思ったけど、ダメ。

$ docker run -it --name my-db -v $(pwd)/postgresql.conf:/var/lib/postgresql/data/postgresql.conf -e POSTGRES_PASSWORD=secret postgres:9.6
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "en_US.utf8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

initdb: directory "/var/lib/postgresql/data" exists but is not empty
If you want to create a new database system, either remove or empty
the directory "/var/lib/postgresql/data" or run initdb
with an argument other than "/var/lib/postgresql/data".

昔のバージョンのようにselinuxの問題はないが、やっぱりPostgreSQLのinitdb実行前にPGDATAにファイルがおいてあるのが悪いっぽい。
Dockerがコンテナの/var/lib/postgresql/dataにVolumeを追加したあとにENTRYPOINT(docker-entrypoint.sh)が実行されるため、その中でinitdbした時にこけてると思われる。
https://github.com/docker-library/postgres/issues/105

PostgreSQL側でpostgresql.confを別ディレクトリから参照可能にするパラメータがあるため、それを利用する。

$ docker run -it --name my-db -v $(pwd)/postgresql.conf:/etc/postgresql.conf -e POSTGRES_PASSWORD=secret -d postgres:9.6 -c config_file=/etc/postgresql.conf
bf55391afe2ae709e84efd81025b4d7f542f3db4bb42d13c348807cc9342b47f
$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U postgres
Password for user postgres:
psql (9.6.2)
Type "help" for help.

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +

特にpostgresql.confファイルをchownしなくても実行できる。昔のバージョンよりだいぶ楽!!!

PostgreSQLの日本語化

ロケールを追加し、LANG環境変数に設定する。

$ cat Dockerfile
FROM postgres:9.6
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.utf8

Dockerfileをビルドし、起動する。

$ docker build -t dev-postgres -f Dockerfile .
$ docker run -it --name my-db -e POSTGRES_PASSWORD=secret -d dev-postgres 

$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U postgres
Password for user postgres:
psql (9.6.2)
Type "help" for help.

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 postgres  | postgres | UTF8     | ja_JP.utf8 | ja_JP.utf8 |
 template0 | postgres | UTF8     | ja_JP.utf8 | ja_JP.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres

永続化やら設定のカスタマイズをまとめると・・・

設定をカスタマイズするときの起動について、最終的には以下のDockerfileを用意しとけばよさそう。
(Docker version 17.03.1-ce, build c6d412e だとpostgresql.confファイルをDockerfile内でCOPYしてchownしなくてもいいが、どうせ日本語ロケールに設定するためにDockerfile作るのでコレで)

$ cat Dockerfile
FROM postgres:9.6
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.utf8
COPY postgresql.conf /etc/postgresql.conf
RUN chown -R postgres /etc/postgresql.conf

そしてこう!

  # postgresql.confを取り出すために一時的なコンテナを起動
$ docker run --name tmp -d postgres:9.6
  # postgresql.confを取り出す
$ docker cp tmp:/var/lib/postgresql/data/postgresql.conf .
  # postgresql.confを適当に編集(例としてmax_connectionsを50にする)
  # 編集したpostgresql.confを配置したイメージを作成
$ docker build -t dev-postgres -f Dockerfile .
  # 永続化するためにData Volume作成
$ docker volume create pgdata
  # コンテナを起動
$ docker run -it --name my-db -e POSTGRES_USER=dev -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data -d dev-postgres -c config_file=/etc/postgresql.conf
  # PostgreSQLに接続
$ docker run -it --rm --link my-db:db postgres:9.6 psql -h db -U dev
Password for user dev:
psql (9.6.2)
Type "help" for help.

dev=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 dev       | postgres | UTF8     | ja_JP.utf8 | ja_JP.utf8 |
 postgres  | postgres | UTF8     | ja_JP.utf8 | ja_JP.utf8 |
 template0 | postgres | UTF8     | ja_JP.utf8 | ja_JP.utf8 | =c/postgres          +

  # 自分で修正したpostgresql.confの内容が反映されている
dev=# show max_connections;
 max_connections
-----------------
 200
(1 row)

手順を簡素化する

上記の例(ビルド~起動)までのコマンドは起動引数が多くてちょっと大変。
docker-compose.ymlを使うと楽だよ、と教えてもらったので使ってみた。

まずインストール
Install Docker Compose - Docker Documentation

$ docker-compose --version
docker-compose version 1.11.2, build dfed245

docker-compose.ymlファイルを用意。

$ cat docker-compose.yml
version: "2"
volumes:
   pgdata:
     driver: 'local'
services:
  my-db:
    build: .
    image: "dev-postgres"
    container_name: "my-db"
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: "dev"
      POSTGRES_PASSWORD: "secret"
    command: 'postgres -c config_file="/etc/postgresql.conf"'
    volumes:
      - pgdata:/var/lib/postgresql/data

postgresql.confをローカルに置いてさえ置けば、以下のコマンドでDockerfileのビルドから起動までやってくれるようになった。引数がコード化されている点がいい。

$ docker-compose up --build -d
docker-compose up --build -d
Building my-db
Step 1/5 : FROM postgres
 ---> 9910dc9f2ac0
Step 2/5 : RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
 ---> Using cache
 ---> 3e2faf03d333
Step 3/5 : ENV LANG ja_JP.utf8
 ---> Using cache
 ---> 4b8b89e1d0f7
Step 4/5 : COPY postgresql.conf /etc/postgresql.conf
 ---> Using cache
 ---> e40af9553b4a
Step 5/5 : RUN chown -R postgres /etc/postgresql.conf
 ---> Using cache
 ---> 70e2b24824d7
Successfully built 70e2b24824d7
my-db is up-to-date

ただし、psql実行時の --linkでコンテナ間リンクを貼れなくなった。docker-composeはコンテナグループごとにネットワーク作るとかなんとか・・・また土日に調べる。

ひとまずホスト側にあるpsqlで接続する。

$ psql -h localhost -U dev
ユーザ dev のパスワード:
psql (9.6.1, サーバー 9.6.2)
"help" でヘルプを表示します.

dev=# \l
                                         データベース一覧
   名前    |  所有者  | エンコーディング |  照合順序  | Ctype(変換演算子) |      アクセス権
-----------+----------+------------------+------------+-------------------+-----------------------
 dev       | postgres | UTF8             | ja_JP.utf8 | ja_JP.utf8        |
 postgres  | postgres | UTF8             | ja_JP.utf8 | ja_JP.utf8        |
 template0 | postgres | UTF8             | ja_JP.utf8 | ja_JP.utf8        | =c/postgres          +

docker-composeで起動した場合、各アプリケーションごとにネットワークが設定される。

$ docker-compose ps
Name               Command               State           Ports
-----------------------------------------------------------------------
my-db   docker-entrypoint.sh postg ...   Up      0.0.0.0:5432->5432/tcp

$ docker inspect my-db
...
      "Networks": {
        "mydocker_default": {
          "IPAMConfig": null,
          "Links": null,
          "Aliases": [
            "84eb940e53ad",
            "my-db"
          ],
          "NetworkID": "8ceeb0c7592109b449fc141b24c19c108d0c5ac6c7fcacc58642d5ce6f8f3a78",
          "EndpointID": "a9e8aa8f3f4ed045ef395220833aabc3eee704731f591f77cdbddd2a2430a6a1",
          "Gateway": "172.19.0.1",
          "IPAddress": "172.19.0.2",
          "IPPrefixLen": 16,
          "IPv6Gateway": "",
          "GlobalIPv6Address": "",
          "GlobalIPv6PrefixLen": 0,
          "MacAddress": "02:42:ac:13:00:02"
        }
      }
...

$ docker network ls
NETWORK ID          NAME                  DRIVER              SCOPE
...
8ceeb0c75921        mydocker_default      bridge              local
...

そのため、psqlをコンテナ間リンクで接続したい場合は以下のようにする。

$ docker run --rm -it --net mydocker_default --link my-db:db postgres:9.6 psql -h db -U dev
Password for user dev:
psql (9.6.2)
Type "help" for help.

dev=# show max_connections;
 max_connections
-----------------
 50
(1 row)

終わりに

Dockerを使いこなすためにも、まずはLinux力をつけていきたい。