Djangoの開発環境をdocker-composeを利用してコンテナで起動&DockerにPostgres環境を構築する方法

# やってみたこと
* 前回、djangoチュートリアルで作成したpollsアプリをdockerコンテナから立ち上げてみました。今回は、`docker-compose.yml`にDB情報を追記して実際にテーブル作成まで行う手順を記します。

## 前提
* 今回も敢えて、`Dockerfile`と`docker-compose.yml`の両方を最低限レベルで使ってdjangoのアプリケーションを構築します。これらのファイルを使った方が、今後汎用的に覚えられるだろうという自分への期待も込めています(笑)
* 前回までの記事はこちらから：
    * チュートリアル作成における記事はこちら：[【初めてのDjango】インストール手順：MVTモデル、ルーティング、DBマイグレーション、views.py、テンプレート、名前空間について](https://qiita.com/Ryo-0131/items/ded7590e372f2d8e796c)
    * DjangoチュートリアルをDockerから立ち上げてみた記事はこちら：[Djangoの開発環境をdocker-composeを利用してコンテナで起動し、runserverコマンドでホスト側からアクセスする方法](https://qiita.com/Ryo-0131/items/522c4e9c0cb4c539eb37)
* djangoチュートリアルでpollsアプリケーションを制作したmysiteプロジェクトを、以下ディレクトリ構造で配置(前回と同様)。

```
.
├── docker
│   ├── docker-compose.yml
│   └── Dockerfile
└── web
    ├── mysite
    │   ├── mysite
    │   ├── polls
    │   ├── db.sqlite3
    │   └── manage.py
    └── requirements.txt
```

* djangoチュートリアルで作成したものをそのまま使っていますので、`settings.py`の`DAEABASES`もそのままになっています。

```py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
```

# 手順
* docker関連ファイルの準備
* ビルド・コンテナ起動
* コンテナの中に入って`manage.py`を確認
* Dockerコンテナから開発サーバを起動

## docker関連ファイルの準備
* `Dockerfile`は、前回記事と同様です。

```Dockerfile
# python:3.8の公式 image をベースの image として設定
FROM python:3.8

# 作業ディレクトリの作成
RUN mkdir /mysite

# 作業ディレクトリの設定（以後の RUN は WORKDIR で実行）
WORKDIR /mysite

# カレントディレクトリにある資産をコンテナ上の指定のディレクトリにコピーする
ADD web /mysite

# pipでrequirements.txtに指定されているパッケージを追加する
RUN pip install -r requirements.txt

# 起動(今回はなし)
# CMD python3 manage.py runserver 0.0.0.0:8000
```

* `requirements.txt`も前回記事同様、必要最低限のままにしています。
```text
Django==4.1
requests
```


* `docker-compose.yml`に以下のように記載します。
    * 今回も必要最低限のものしか記載していません。前回から`db-sample1`以下を加えています。
* それぞれが意味する詳細は以下記事が非常にわかりやすかったのでご参考までにぜひ。
   * [Docker で作る postgres 環境](https://crudzoo.com/blog/docker-postgres)

```yaml
version: '3'
services:
  mysite:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile # Dockerfileのディレクトリを指定します
    volumes:
      - '../web:/mysite'
    ports:
      - "8000:8000" # 左側が外部からのアクセス時のポート番号:右側がdockerコンテナからアクセスする時のポート番号。
    container_name: docker_mysite
    tty: true　 # ポート待受とかしていないコンテナの場合、trueに設定しておくとコンテナが起動し続けます
    working_dir: '/mysite'
  db-sample1: # 任意の名前を指定します
    build:
      context: ../
      dockerfile: ./docker/sample1/Dockerfile # Dockerfileのディレクトリを指定します
    ports:
      - "5438:5432" # 左側が外部からのアクセス時のポート番号:右側がdockerコンテナからアクセスする時のポート番号
    container_name: mysite_sample1_db
    environment:
      POSTGRES_USER: user
      POSTGRES_DB: sample
      POSTGRES_PASSWORD: pass
    volumes:
        - postgres-data-volume:/var/lib/postgresql/data # <名前付きvolume名>:マウントしたいコンテナ内のPATH

volumes:
    postgres-data-volume:
```

* portsフィールドは、ホスト・コンテナ間のポートのマッピングを指定します。
    * 上記のservice`mysite`の例だと、コンテナポートの8000番（右側）をホストポートの8000番(左側)にマッピングすることで、ブラウザで`https://localhost:8000/`へアクセスできるようになります。これを「ホストの8000番からコンテナの8000番へポートフォワードする」とも言います。`db-sample1`も同様に、ホストの5438からコンテナの5432にポートフォワードしています。
    * デフォルトでは、5432がPostgreSQLの利用するポートになります。

### environmentフィールド
* `environment:`フィールドで一点注意があります。上記のようにUSER名やDB名を指定するか、以下のように記載するか、どちらかを記載しないと`Error: Database is uninitialized and superuser password is not specified.`エラーになります。以下のように記載した場合、デフォルトで与えられている、postgresというUSER名やDB名でログインできます。（パスワードなしで接続できるのであまり推奨されていません）

```yaml
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust
```

* ちなみに、以下のようにどちらも記載した場合は、`POSTGRES_USER`など指定したものでログインができませんでしたので、ご注意。「あれ、ちゃんと記述したのに`psql: FATAL:  role "user" does not exist`って出てくるぞ？」という場合は、誤って重複した書き方をしていました。最初はデフォルトの方でやろうと思って記述していたので、後から`POSTGRES_USER`など指定した時に消すのを忘れてしまっていたんでしょうね。

```yaml
    environment:
      POSTGRES_USER: user
      POSTGRES_DB: sample
      POSTGRES_PASSWORD: pass
    volumes:
        - postgres-data-volume:/var/lib/postgresql/data
    environment:
      POSTGRES_HOST_AUTH_METHOD: trust # こちらが優先されるのでご注意
```

### volumesフィールド（名前付きボリュームの作成）
* `volumes:`フィールドでは、トップレベルで定義したvolume名(今回でいえば一番下段に記載しているvolumesのこと)を指定してあげます。
    * このマウントの手法を「名前付きボリュームの作成」や「ボリュームマウント」と言います。(「ディレクトリのマウント(同期)」については後述）

```yaml
  db-sample1: 
    # 省略
    volumes:
        - postgres-data-volume:/var/lib/postgresql/data # データボリューム名:コンテナディレクトリ
volumes:
    postgres-data-volume: # ここで名前付きボリューム名を指定します
```

* 上記により、`postgres-data-volume`をマウントしてデータベースを永続化しています。
    * 「データの永続化」とは、Dockerコンテナ上で作成されたデータがコンテナ削除後も残り続けている状態のこと。コンテナが削除されてもコンテナ上で作成されたデータを保持し続ける方法として知られています。
    * 上記で定義した「データボリューム」(単にボリュームともいいます)とは、１つまたは複数のDockerコンテナ内でデータの共有・再利用をするために設計された特別なディレクトリです。コンテナのライフサイクルとは別なので、コンテナを削除しても残ります。（データボリュームを削除すると永続化したデータは削除されます）
    * 何も指定しなければ『匿名（無名）ボリューム』、指定すれば『名前付きボリューム』と呼ばれます。ただし、匿名ボリュームはコンテナが新しく作成されるたびに新たに作成されるので注意が必要な時があります。
* ちなみに、マウントで指定したのに、トップレベルのvolumesで該当のvolume定義がない場合、以下のエラーが返ってきます。

```terminal
service "db-sample1" refers to undefined volume postgres-data-volume: invalid compose project
```

* 今回の場合、アプリケーション名_postgres-data-volumeのボリュームの下記の名前で作成されます。
```terminal
docker_postgres-data-volume
```

* `/var/lib/postgresql/data`はpostgresコンテナでのデフォルトのDBパスです。
    * コンテナに入って`ls -a`すれば、どんなデータが保管されているのか確認できます。
    * なお、mysqlコンテナならば、`/var/lib/mysql`となりますので、コンテナごとに異なります。
    * https://hub.docker.com/_/postgres
        * "The default is /var/lib/postgresql/data"と記載があります
    * https://hub.docker.com/_/mysql
        * "...(略).../var/lib/mysql inside the container, where MySQL by default will write its data files."と記載があります
* 補足【ディレクトリのマウント（同期）】：
    * 以下のように記載しておくと、Dockerコンテナ上の`/var/lib/postgresql/data`をローカルの`/postgres/data`にマウントできます。コンテナ上のDBを誤って消してしまった場合やイメージを削除してしまった場合などのために、ローカルに保存しておくと後で復旧ができます。上記の例は「名前付きボリュームの作成」でしたが、下記の場合を「ディレクトリのマウント（同期）」と言います。


```yaml
  db-sample1: 
    # 省略
    volumes:
      - ./postgres/data:/var/lib/postgresql/data
```


 
## ビルド・コンテナ起動
* `docker-compose.yml`があるディレクトリで以下のコマンドを実行すると、ビルド(イメージ作成)＆コンテナ起動します。

```terminal
$ docker compose up -d
```

* 起動しているコンテナを以下のコマンドで確認できます。


```terminal
$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS         PORTS                    NAMES
a88071064847   docker-mysite       "python3"                10 seconds ago   Up 9 seconds   0.0.0.0:8000->8000/tcp   docker_mysite
5192dce52ff6   docker-db-sample1   "docker-entrypoint.s…"   10 seconds ago   Up 9 seconds   0.0.0.0:5438->5432/tcp   mysite_sample1_db
```

* `-d`オプションはデーモンモードで起動し、プロセスをバックグラウンドで動作させることができます。
    * アクセスログの内容やエラーなどを確認したい場合は、`-d`を外して実行します。
* `$ docker compose ps`でもコンテナがRUNNINGかどうか分かります。 
* `mysite`が8000ポートに、dbの`sample1`が5432ポートに、それぞれ`docker-compose.yml`で指定したポートにつながっていることがわかります。

* また、以下のコマンドにより、`requirement.txt`に記述したモジュールがインストールされているかどうか確認できます。


```terminal
$ docker exec -it docker_mysite pip list
Package            Version
------------------ ---------
asgiref            3.6.0
backports.zoneinfo 0.2.1
certifi            2022.12.7
charset-normalizer 3.1.0
Django             4.1
idna               3.4
pip                22.0.4
requests           2.28.2
setuptools         57.5.0
sqlparse           0.4.3
urllib3            1.26.14
wheel              0.38.4
```

## コンテナの中に入って`postgresql`を確認
* 以下のコマンドで`mysite_sample1_db`コンテナに入ります。

```terminal
$ docker exec -it mysite_sample1_db bash
```

* CONTAINER IDでも入れるので、以下のコマンドでもOKです。

```terminal
$ docker exec -it 5192dce52ff6 bash
```

* コンテナの中に入って、`/var/lib/postgresql/data`を確認してみましょう。先ほどマウントしたのは、ここにあるpostgre関連ファイルを、指定したボリュームにマウントしたという意味になります。

```terminal
5192dce52ff6:/# cd /var/lib/postgresql/data
5192dce52ff6:/var/lib/postgresql/data# ls -a
.                     base                  pg_dynshmem           pg_logical            pg_replslot           pg_stat               pg_tblspc             pg_xact               postmaster.opts
..                    global                pg_hba.conf           pg_multixact          pg_serial             pg_stat_tmp           pg_twophase           postgresql.auto.conf  postmaster.pid
PG_VERSION            pg_commit_ts          pg_ident.conf         pg_notify             pg_snapshots          pg_subtrans           pg_wal                postgresql.conf
5192dce52ff6:/var/lib/postgresql/data# 
```


## ボリューム確認
* では、作成したボリュームが動いているか確認してみます。
    * コンテナから出る場合は`exit`と入力してください。
* ボリュームの確認は、以下のコマンドで可能です。

```terminal
$ docker volume ls
DRIVER    VOLUME NAME
local     docker_postgres-data-volume
```

* 指定したボリュームが作成されていることがわかりました。
* ボリュームは一度作成すれば、`docker compose down`された後でも残ります。上述したように、永続化しているからですね。
* `docker inspect [データボリューム名]`でデータボリュームの詳細情報が確認できます。
* また、`docker inspect [コンテナ名]`でコンテナの詳細情報が確認できます。
    * Mountsフィールドを見ると、Dockerコンテナのマウント対象なども確認できます。

## データベースに接続
* それではデータベースを確認していきましょう。先ほどと同じようにまずは起動中のコンテナの中に入ります。

```terminal
$ docker exec -it mysite_sample1_db bash
```

* 以下psqlコマンドで`user`ユーザーによる`sample`データベースへのログインができます。

```sql
5192dce52ff6:/# psql -U user sample
sample=# 
```

* 以下、いずれでもログイン可能です。
    * いずれもホストやポートを指定する場合のログイン方法です。

```sql
psql -h localhost sample -U user
psql sample -U user -p 5432
psql -U user -d sample
```

* ちなみに、`psql -U user`だけだと、データベースの指定がないのでエラーになります。

```sql
psql -U user
psql: FATAL:  database "user" does not exist
```

* 上記のエラーは、postgresではデフォルトではユーザー名と同じデータベースが作成されるためです。つまり、`docker-compose.yml`でdbに`sample`と指定していない場合は、`psql -U user`でもログインができます。

* また、postgresではデフォルトで`postgres`というデータベース名であればログインができます。

```sql
psql -U user postgres
postgres=# 
```

* プロンプトが、`sample`ではなく`postgres`になっていることがわかります。これは接続しているデータベースが`postgres`のためです。
* `sample`に切り替えたい場合は`\c`コマンドで可能です。

```sql
postgres=# \c sample
You are now connected to database "sample" as user "user".
sample=# 
```

* `\d`コマンドでテーブルを確認できますが、現状まだ何もないことがわかります。
```sql
sample=# \d
Did not find any relations.
```

* ちなみに上記で`user`というユーザー名を作成しこれで接続しました。これはロール（データーベースを管理するもの）といい、以下コマンドで確認できます。

```sql
sample=# \du
                                   List of roles
 Role name |                         Attributes                         | Member of 
-----------+------------------------------------------------------------+-----------
 user      | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
```


## テーブル作成
* テーブルを作成してみましょう。
* 以下のコマンドで`public`スキーマの中に`hoge`テーブルを作成できます。

```sql
create table public.hoge( id integer, name varchar(10), address varchar(10) );
CREATE TABLE
```

* ちなみに`public`スキーマを選択せずに`hogeaaa`テーブルを作成するとどうなるでしょうか。

```sql
sample=# create table hogeaaa( id integer, name varchar(10), address varchar(10) );
CREATE TABLE
sample=# \d
        List of relations
 Schema |  Name   | Type  | Owner 
--------+---------+-------+-------
 public | hoge    | table | user
 public | hogeaaa | table | user
(2 rows)
```

* 上記のように作成できていることがわかります。指定しない場合、自動的に「public」という名前のスキーマに入れられます。詳しく知りたい方は以下リンクをご参照ください。
    * [5.9. スキーマ](https://www.postgresql.jp/document/12/html/ddl-schemas.html)
* 以下記事では、スキーマという概念について図解があります。PostgreSQLは三層、MySQLは二層なのでちょっとたまに混乱します汗
    * [MySQL しか使ったことないアプリケーション開発者が PostgreSQL の Docker 環境を立てるまで](https://pf-siedler.hatenablog.com/entry/2022/02/09/090757)
       

## データベースを別途作成
* `sample`や`postgres`以外にもデータベースを作成することができます。その場合は以下コマンドを実行します。

```sql
sample=# create database hogedb;
CREATE DATABASE
```

* 以下の通り、DBが新しく加わったことが確認できますね。

```sql
sample=# \l
                             List of databases
   Name    | Owner | Encoding |  Collate   |   Ctype    | Access privileges 
-----------+-------+----------+------------+------------+-------------------
 hogedb    | user  | UTF8     | ja_JP.utf8 | ja_JP.utf8 | 
 postgres  | user  | UTF8     | ja_JP.utf8 | ja_JP.utf8 | 
 sample    | user  | UTF8     | ja_JP.utf8 | ja_JP.utf8 | 
 template0 | user  | UTF8     | ja_JP.utf8 | ja_JP.utf8 | =c/user          +
           |       |          |            |            | user=CTc/user
 template1 | user  | UTF8     | ja_JP.utf8 | ja_JP.utf8 | =c/user          +
           |       |          |            |            | user=CTc/user
(5 rows)
```

* データの取得したり保存したり確認したりするのも、同様にSQLコマンドで可能ですので、適当にINSERTなどしてみるのも良いと思います。

## 参考記事
* [Dockerのデータを永続化！Data Volume（データボリューム）の理解から始める環境構築入門](https://nishinatoshiharu.com/docker-volume-tutorial/)
* [Docker Postgres DB コンテナが起動しないにハマる](https://chaika.hatenablog.com/entry/2021/12/31/120000)
* [docker-compose.yml個人的Tips集](https://qiita.com/yuki_0920/items/dc3f32667d004979cc5a)

## 次回記事
* 初期データ登録用のスクリプトを作成し、SQL実行する手順の記事を作成しました。
    * [Djangoの開発環境をコンテナで起動&Postgresに初期データを自動でSQL実行する方法](https://qiita.com/Ryo-0131/items/e2ef20668d5f2bd1accf)