Docker学習、私はこう取り組んだ

# はじめに

[『個人開発2年間の軌跡』終盤のDocker学習](https://qiita.com/gmasa/items/fb9a0487bb0a12b91219#web%E3%82%B5%E3%82%A4%E3%83%88dockerizing-django-with-postgres-gunicorn-and-nginx-%E3%81%A7%E5%AD%A6%E7%BF%92)において、お手本サイト[『Dockerizing Django with Postgres, Gunicorn, and Nginx』](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/)に取り組んだ時の記録です。強強ではない人間がこのサイトを読んでどこに引っかかったのか、参考にしてもらえればと思います。


# 対象読者
Djangoユーザー かつ Docker初心者を対象にしています。個人開発でDockerを使えるようになりたい人の助けになれば幸いです。

# 開発環境
- Windows11Pro（**２台！**）
    - Homeでは試していませんが、たぶん問題ないです。
    - 問題解決能力が低い初心者の時こそ、PC複数台持つことをおススメします。コスパ重視、中古でOKです。いつでもOSクリーンインストールして構わないマシンがあると心強いです。
    - この記事は一度で作業完了しているように書いていますが、実際はたくさんハマっています。英語サイトをググって色々試しても解決に至らないケースでは、別のPCに同じ方法で環境設定して、同じ作業を実施したところ正常動作させることができました。もうなんなの:weary:

- WSL2 Ubuntu
    - インストール手順は色々ありそうですが、どれでも大丈夫だと思います。
- VScode
    - VScodeターミナルで WSL2 Ubuntu bashを操作できる状態にしてください。環境設定はググってわかりやすいサイトを探してください。
- Python
    - WSL2 Ubuntu に始めから入っているPythonは使わず、新しいバージョンのPythonを追加インストールしてそちらを使うようにしてください。
    - 私はお手本と同じVer.3.9.6を入れました。

# ゼロから構築する作業を追体験
[『Dockerizing Django with Postgres, Gunicorn, and Nginx』（以下、お手本サイト）](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/)はプロジェクトディレクトリを作るところからスタートし、途中で内容ジャンプすることなく最後まで連れて行ってくれます。素晴らしい。
私が学習した際に、お手本とは異なる作業したこと、疑問に思ったこと、問題解決したこと、これらをお手本サイトの進行に沿って記しました。見出しが該当項目へのリンクになっています。
丁寧に説明されている箇所はこちらの記事に再掲していないので、 **まずはリンク先のお手本サイトに目を通してください**:pray:
とは言いつつ、お手本まんまのことを書いた箇所も多々ありますが、それはあっちとこっちを行ったり来たりせず、作業の流れをつかめるようにするためです。（私自身のブランク明けの作業手順書にもしたかったので。。）

英語サイト読解時は、DeepL アプリ版をおすすめします。文字選択後、Ctrlキー押しながらCキーを2回押すことで、即翻訳してくれます。すごい時代です。お手本サイトはGoogle翻訳でも十分わかりやすいかもしれません。

## [Project Setup](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#project-setup)

ホームディレクトリに作業場を用意（worksという名前にした）
```bash
~$ mkdir works
~$ cd works
```

プロジェクトディレクトリを用意（hoge-on-dockerという名前にした）
```
~/works$ mkdir hoge-on-docker
~/works$ cd hoge-on-docker
~/works/hoge-on-docker$ 
```
PATHが長いので、これ以降の解説では
```
~/*$
```
と表現する。

Djangoアプリを格納するappディレクトリを用意
```
~/*$ mkdir app
~/*$ cd app
```

Pythonの仮想化
```
~/*/app$ python3 -m venv .
~/*/app$ . .venv/bin/activate
```

:::note info
作業再開時の仮想化
おそらく `~/*$` の状態で作業再開することになるので、仮想化のコマンドは `. ./app/.venv/bin/activate` である。もちろん参照元サイトの通り `source ./app/.venv/bin/activate` でも大丈夫。書き方２つあるよね、と記録しておきたいだけ。
:::


#### Djangoインストール
Djangoのバージョンは現時点で一番新しいものにした。セキュリティの専門知識を持ち合わせていないので、せめてフレームワークのバージョンは新しいものを使おう、という考え。
```bash
(.venv)~/*/app$ pip install --upgrade pip
(.venv)~/*/app$ pip install django==4.1
(.venv)~/*/app$ django-admin startproject config .
(.venv)~/*/app$ python manage.py migrate
(.venv)~/*/app$ python manage.py runserver
```

ブラウザで `http://localhost:8000/` にアクセス。ロケットが表示されればOK。`ctrl + c` で停止。

:::note info
ディレクトリ名について
今まで[『Django for Beginners』](https://www.amazon.co.jp/dp/1735467200/)の考え方に則り、settings.py等が格納されるフォルダ名をconfig にしていた。
Docker開発に切り替えるにあたりプロジェクト名（今回でいうとhoge）にすることも考えたが、やっぱりconfigで行くことにした。それと、Djangoアプリを格納するディレクトリ名はお手本通りapp とした。理由は以下の通り。
#### ディレクトリ名を app, configとする理由
- Docker開発になると、プロジェクトのディレクトリ直下にdocker-compose.yml 等のファイルがたくさん並ぶ。「Djangoアプリは appフォルダ直下とする」を自分ルールにした方が見つけやすい。
- docker-compose.yml 等の記述を統一できる。
- appディレクトリ、その下にconfig となると、このDjangoアプリは何者なのか分かりにくいかもしれないが、プロジェクトのディレクトリ名を見ればわかるので良しとした。
- configで統一すれば、後に出てくる nginx.conf の記述も固定できる。
:::


`app` ディレクトリ直下に `requirements.txt` を用意して、中に `django==4.1` を記入。

:::note info
`pip freeze > requirements.txt` を使わないの？
pythonのalpineイメージをベースにして、イメージを構築する過程でDjangoの環境をインストールするので、余計なパッケージ情報を requirements.txt に含めたくない。欲しいのは **インストール指示のリスト** であり、 **インストールされたパッケージリスト** ではないということ。
:::

データベースはPostgresを使うので、`db.sqlite3` は削除しておく。イメージ構築時にapp内をコピーするので、余計なものは予め消しておく、という考え。後の作業で復活する気がするけど、また消せばいい。
(`.dockerignore`ファイルの扱い方を調べて、組み込んでみるのが良いかもしれません。君はやらないんかいってツッコミが痛い:persevere: )


## [Docker](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#docker)

`app` ディレクトリ直下に `Dockerfile` を作成。（詳細は参照元サイトを確認してほしい）
```Dockerfile
# pull official base image
FROM python:3.9.6-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy project
COPY . .
```

:::note info
ファイル名の誤記に注意
ファイル名が d**co**kerfile になっていてハマった。VScodeであればファイル名が正しければアイコンがクジラになる。よく見ないと。。
:::

:::note info
ベースイメージのバージョン
最新バージョンのイメージを使った時にバインドマウント（ ホストとコンテナの連動 ）ができなかったので、お手本通りにした。やり直してうまくいっただけで原因は他にあると思う。検証はしていない。。
:::


プロジェクトルート(hoge-on-docker直下) に `docker-compose.yml` を作成（appと同じ階層）
```yml:docker-compose.yml
version: '3.8'

services:
  web:
    build: ./app
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app/:/usr/src/app/
    ports:
      - 8000:8000
    env_file:
      - ./.env.dev
```

続けて、プロジェクトルートに `.env.dev` を作成。

```.env.dev
DEBUG=1
SECRET_KEY=〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
```
SECRET_KEY の右辺には、`app/config`ディレクトリの下にいる `settings.py` の `SECRET_KEY` の ' 'で囲われた文字列をコピペ。（両端の ' は不要です。）

`settings.py` に追記・変更
```python: settings.py の追記・変更（他の内容は省略）
# 冒頭に追記
import os

# 下記3項目の右辺を変更
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
```

イメージのビルド

プロジェクト直下に移動
```bash
~/*/app$ cd ..
```
docker-compose.yml を使ってイメージをビルド
```bash
~/*$ docker-compose build
```

コンテナを生成し、バックグラウンド動作（デーモン）
```bash
~/*$ docker-compose up -d
```
http://localhost:8000/ にアクセス。

:::note info
ログ確認
ロケット画面が表示されない場合は、ログ確認。
```bash
~/*$ docker-compose logs -f
```
:::



## [Postgres](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#postgres)

Postgres の導入。docker-compose.yml にdb情報を追記する。
```yml: docker-compose.yml
version: '3.8'

services:
  web:
    build: ./app
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./app/:/usr/src/app/
    ports:
      - 8000:8000
    env_file:
      - ./.env.dev
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=hello_django
      - POSTGRES_PASSWORD=hello_django
      - POSTGRES_DB=hello_django_dev

volumes:
  postgres_data:
```

:::note info
DBのユーザー名など
サンプルに則って hello_django のままだが、とりあえずこのまま進める。将来問題があると感じたらその時に変更。
:::


.env.dev に追記
```.env.dev
DEBUG=1
SECRET_KEY=〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
```

settings.py の変更
```python: settings.pyの一部
DATABASES = {
    "default": {
        "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
        "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"),
        "USER": os.environ.get("SQL_USER", "user"),
        "PASSWORD": os.environ.get("SQL_PASSWORD", "password"),
        "HOST": os.environ.get("SQL_HOST", "localhost"),
        "PORT": os.environ.get("SQL_PORT", "5432"),
    }
}
```

Dockerfile の変更
```docker: Dockerfile
# pull official base image
FROM python:3.9.6-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy project
COPY . .
```

:::note info
psycopg2 インストールに必要な作業として
    ```
    RUN apk update \
        && apk add postgresql-dev gcc python3-dev musl-dev
    ```
を実施しているが、これは alpineベースのイメージを使っているためである。 最初にチャレンジした時に、slimなんちゃらってイメージをベースにした時は不要だった気がする。（記憶が。。。）
余計なことするとハマるので、慣れていない人はお手本通り alpine で行こう:muscle:
:::

requirements.txt に追記
```requirements.txt
django==4.1
psycopg2-binary==2.9.1
```

イメージのビルドとコンテナ起動を同時に実行する書き方はこれ。
```bash
~/*$ docker-compose up -d --build
```

マイグレート
```bash
~/*$ docker-compose exec web python manage.py migrate --noinput
```

ここでエラーが出たらお手本サイトをチェックしよう。

Postgres が動いていれば、デフォルトの Django テーブルが作成された様子を確認できるはず。
```bash
~/*$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
```
以下、`=#` の右辺が入力するコマンド。
```bash
psql (13.0)
Type "help" for help.

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

hello_django_dev=# \c hello_django_dev
You are now connected to database "hello_django_dev" as user "hello_django".

hello_django_dev=# \dt
                     List of relations
 Schema |            Name            | Type  |    Owner
--------+----------------------------+-------+--------------
 public | auth_group                 | table | hello_django
 public | auth_group_permissions     | table | hello_django
 public | auth_permission            | table | hello_django
 public | auth_user                  | table | hello_django
 public | auth_user_groups           | table | hello_django
 public | auth_user_user_permissions | table | hello_django
 public | django_admin_log           | table | hello_django
 public | django_content_type        | table | hello_django
 public | django_migrations          | table | hello_django
 public | django_session             | table | hello_django
(10 rows)

hello_django_dev=# \q
```

ボリュームの詳細を確認してみる
```bash
~/*$ docker volume inspect hoge-on-docker_postgres_data
　　　　　　　　　　　　　　　　↑ _postgres_dataの前はプロジェクト名。自分の環境に合わせる。
```

以下のような表示が含まれていればOK
```bash
[
    {
        "CreatedAt": "2021-08-23T15:49:08Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "hoge-on-docker",
            "com.docker.compose.version": "1.29.2",
            "com.docker.compose.volume": "postgres_data"
        },
        "Mountpoint": "/var/lib/docker/volumes/hoge-on-docker_postgres_data/_data",
        "Name": "hoge-on-docker_postgres_data",
        "Options": null,
        "Scope": "local"
    }
]
```

appディレクトリ直下に `entrypoint.sh` を追加

```entrypoint.sh
#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

python manage.py flush --no-input
python manage.py migrate

exec "$@"
```

すぐに `entrypoint.sh` ファイルの実行権限を変更しておく
```bash
~/*$ chmod +x app/entrypoint.sh
```

Dockerfile の変更
```docker: Dockerfile
# pull official base image
FROM python:3.9.6-alpine

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# copy entrypoint.sh
COPY ./entrypoint.sh .
RUN sed -i 's/\r$//g' /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh

# copy project
COPY . .

# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
```

.env.dev に追記
```.env.dev
DEBUG=1
SECRET_KEY=〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_dev
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
```

イメージのビルドとコンテナ起動
```bash
~/*$ docker-compose up -d --build
```
ブラウザで `http://localhost:8000/` にアクセスしてロケット表示されればOK。

:::note info
[Postgres Notes](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#notes) に対するメモ書き
Django単独でコンテナ生成・起動する方法を紹介している。環境変数 DATABASE=postgres を設定しないようにビルドすれば可能。コマンドの書き方はお手本サイトを確認。確かにこれで実行すると、settings.py はsqlite3を用意する形になっている。
あえてこういう起動方法を採りたい場面はあるのだろうか。今のところ思いつかない。一から開発する時に必要な場面が訪れるのかな？今は構わず先に行こう。
:::


## [Gunicorn](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#gunicorn)

nginx と Django の連絡役に Gunicorn を使う。requirements.txt に追記。

```requirements.txt
django==4.1
gunicorn==20.1.0
psycopg2-binary==2.9.1
```

本番サーバーは nginx だけど、開発中は Django 組み込みサーバーを使いたい。開発と本番でdocker-compose を分ける。
- 本番用の `docker-compose.prod.yml` を作成。`docker-compose.yml` を複製・変更して作る
    - 本番では、runserverではなくgunicornを起動
    - 本番では、webコンテナでホストとコンテナの連動（バインドマウント）は不要なので削除

- 本番用設定ファイル  `.env.prod` を用意。開発用の設定ファイル`.env.dev` を複製・変更して作る
    - DEBUG を 1 から 0 に変更
    - SQL_DATABASE を hello_django_**dev** から hello_django_**prod** に変更

- 本番 db用の設定ファイル `.env.prod.db` を用意
    - docker-compose.yml 複製直後の docker-compose.prod.yml に含まれている、db イメージ生成に必要なPOSTGRES環境変数を、この `.env.prod.db` に移行する。（_dev → _prod の書替えを忘れずに）

```yml: docker-compose.prod.yml
version: '3.8'

services:
  web:
    build: ./app
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    ports:
      - 8000:8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db

volumes:
  postgres_data:
```

```.env.prod
DEBUG=0
SECRET_KEY=〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SQL_ENGINE=django.db.backends.postgresql
SQL_DATABASE=hello_django_prod
SQL_USER=hello_django
SQL_PASSWORD=hello_django
SQL_HOST=db
SQL_PORT=5432
DATABASE=postgres
```

.env.prod.db (db名が_devじゃなくて_prod。コピペを使って書く時は注意)
```.env.prod.db
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
```

コンテナ削除（関連ボリュームも削除したいので -v を付ける）
```bash
~/*$ docker-compose down -v
```

本番イメージのビルドとコンテナ起動
```bash
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
```

ブラウザで `http://localhost:8000/admin` にアクセスし、admin管理ログイン画面が表示されればOK。
今はCSSの装飾がない状態でOK。この先でstaticfileの設定が済めば改善される。


もし正常動作しなければログ確認。ファイル名を指定しないと docker-compose.yml を読みに行くので注意。本番用のコンテナの詳細を調べたい時は、ファイル名指定 `-f docker-compose.prod.yml` を忘れずに。
```bash
~/*$ docker-compose -f docker-compose.prod.yml logs -f
```


## [Production Dockerfile](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#production-dockerfile)

本番用の Dockerfile を用意する。
現時点の Dockerfile と entrypoint.sh の組み合わせでは、コンテナを起動する度に、
```bash
python manage.py flush --no-input　　データベースのクリア
python manage.py migrate　　　　　　　マイグレート
```
を実行してしまう。開発中はこれでかまわないが、本番でこれをやられるとまずい。マイグレートは『変更ないので必要ありません』で終わるが、データベースクリアは大問題。

app直下に entrypoint.prod.sh を用意
```entrypoint.prod.sh
#!/bin/sh

if [ "$DATABASE" = "postgres" ]
then
    echo "Waiting for postgres..."

    while ! nc -z $SQL_HOST $SQL_PORT; do
      sleep 0.1
    done

    echo "PostgreSQL started"
fi

exec "$@"
```

続けて、ファイルの実行権限を変更する
```bash
~/*$ chmod +x app/entrypoint.prod.sh
```


#### Dockerfile.prod を用意
本番イメージの容量削減のため、BuilderとFinalの２段構成。マルチステージビルド という手法を取り入れる。
- Builder
    1. psycopg2を動かすのに必要なアプリのインストール。
    1. flake8(lint)をインストールし実行してDjangoのapp内のコードに問題が無いかチェック。
    1. pip wheel ***** で requirements.txtのパッケージインストールに必要な wheel群を構築。wheel構築の利点やオプションの解説は、https://kurozumi.github.io/pip/reference/pip_wheel.html を参照。

- Final
    1. libpq(PostgreSQLのC言語インタフェース)のインストール。
    1. Builderで使ったrequirements.txtと、Builder で構築したwheel群の取り込み。これを使ってインストール。
    1. entrypoint.prod.shのコピーなど

```docker: Dockerfile.prod
###########
# BUILDER #
###########

# pull official base image
FROM python:3.9.6-alpine as builder

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install psycopg2 dependencies
RUN apk update \
    && apk add postgresql-dev gcc python3-dev musl-dev

# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .

# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt


#########
# FINAL #
#########

# pull official base image
FROM python:3.9.6-alpine

# create directory for the app user
RUN mkdir -p /home/app

# create the app user
RUN addgroup -S app && adduser -S app -G app

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*

# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g'  $APP_HOME/entrypoint.prod.sh
RUN chmod +x  $APP_HOME/entrypoint.prod.sh

# copy project
COPY . $APP_HOME

# chown all the files to the app user
RUN chown -R app:app $APP_HOME

# change to the app user
USER app

# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
```

:::note info
RUN sed **** でやっていること （正直理解しきれていない。メモを残しておく感じ・・。）
sedコマンドとは「stream editor」の略称。指定したファイルをコマンドに従って処理。入力を行単位で読み取り、テキスト変換などの編集をおこない行単位で出力。正規表現に対応。

【参考】[改行コードの変更](https://qiita.com/keniooi/items/289db04addd2cfcf6fdd) 

今回はファイルを直接上書きしたいので、`-i` オプションを付けて
`RUN sed -i 's/\r$//g'  $APP_HOME/entrypoint.prod.sh`
としている。正規表現の箇所は、修行が足りていない:cry:ということで先に進む。
:::


今の `docker-compose.prod.yml` の `web` は `Dockerfile` を読む状態になっているので、`Dockerfile.prod` を読み込むように変更する。

```yml: docker-compose.prod.yml (webの箇所のみ掲載)
web:
  build:
    context: ./app
    dockerfile: Dockerfile.prod
  command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
  ports:
    - 8000:8000
  env_file:
    - ./.env.prod
  depends_on:
    - db
```

準備ができたので実行してみる。まっさらにしてからビルドして、実行する。
```bash
~/*$ docker-compose -f docker-compose.prod.yml down -v
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
```

このようなエラーが出ると思う。
```bash
executor failed running [/bin/sh -c flake8 --ignore=E501,F401 .]: exit code: 1
ERROR: Service 'web' failed to build : Build failed
```

flake8はコードチェッカーツール。Djangoが自動生成する箇所も抵触するみたい。さすがにそれは知ったこっちゃないので、config 内だけをチェック対象にする。
`Dockerfile.prod` の
`RUN flake8 --ignore=E501,F401 .`
を
`RUN flake8 --ignore=E501,F401 ./config`
に変更。
後々、Django app 追加したらそれらのディレクトリもチェックすべきだけど、必要性を感じるまでは config だけでいいや、ってことで先に進む。（このテキトウすぎる態度はマネしないでください:see_no_evil:） 

再ビルドしてマイグレート。10分以上かかる。長い。。
```bash
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
```

ブラウザで `http://localhost:8000/admin` にアクセスして表示されればOK。（まだCSS無し）
マルチステージビルドのおかげで、イメージのデータ容量はかなり小さくなるはず。（前の状態を記録するの忘れた。）



## [Nginx](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#nginx)

Nginx の導入
docker-compose.prod.yml に追記
```yml:docker-compose.prod.yml の追記箇所
nginx:
  build: ./nginx
  ports:
    - 1337:80
  depends_on:
    - web
```

プロジェクトルート（つまりhoge-on-docker/）に `nginx` ディレクトリを作成し、その下に `Dockerfile` と `nginx.conf` を作成する。

```docker: Dockerfile
FROM nginx:1.21-alpine

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

```nginx.conf
upstream config {
    server web:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://config;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }
}
```

`docker-compose.prod.yml` の `web` のポート設定をいじる。`ports` で外部につながるようにしていたが、`nginx` 経由になると `web` につなげるのはコンテナだけなので、`expose` に変更する。
どこかのタイミングで `http://localhost:8000` に直接アクセスできないことを確認しておくといい。

```yml: docker-compose.prod.yml の変更箇所
web:
  build:
    context: ./app
    dockerfile: Dockerfile.prod
  command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
  expose:
    - 8000
  env_file:
    - ./.env.prod
  depends_on:
    - db
```

動作確認
```
~/*$ docker-compose -f docker-compose.prod.yml down -v
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
```

ブラウザで `http://localhost:1337/` にアクセスして
```
Not Found
The requested resource was not found on this server.
```
と表示されればOK。 `http://localhost:1337/admin` なら、CSSなし管理画面のログインが表示される。

コンテナをダウンしておこう
```bash
~/*$ docker-compose -f docker-compose.prod.yml down -v
```


## [Static Files](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#static-files)

Static Files の設定

本題に入る前に、忘れないうちにsettings.pyの地域設定を変更。
```python: settings.py の変更点
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
```
続けて、Static Filesに関する settings.py の 変更・追記
```python: settings.py の 変更・追記
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
```

これで **開発モード（つまり DEBUG=1 の時）** であれば、 `http://localhost:8000/static/*` にアクセスした時に、 `app/staticfiles/*` を探すようになった。まだ staticfilesディレクトリを用意していないが。。
`python manage.py runserver` で起動するDjango組み込みサーバーはそういう仕組みになっている。
しかし、本番サーバーで使う `nginx` は手入力で色々設定をしてあげないといけない。
本番環境用の設定を順番に行う。

```yml: docker-compose.prod.yml
version: '3.8'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
    expose:
      - 8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db
  nginx:
    build: ./nginx
    volumes:
      - static_volume:/home/app/web/staticfiles
    ports:
      - 1337:80
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
```

:::note info
理解不足メモ
正直、お手本サイトの以下のテキストが理解できていない。(source)のリンク先のFAQを読み解ければスッキリするのかもしれない。そこまでのパワーがないので今は保留。。

Why is this necessary?
Docker Compose normally mounts named volumes as root. And since we're using a non-root user, we'll get a permission denied error when the collectstatic command is run if the directory does not already exist

To get around this, you can either:
1. Create the folder in the Dockerfile [(source)](https://github.com/docker/compose/issues/3270#issuecomment-206214034)
2. Change the permissions of the directory after it's mounted [(source)](https://stackoverflow.com/questions/40462189/docker-compose-set-user-and-group-on-mounted-volume/40510068#40510068)
We used the former.
:::

:::note info
理解不足メモ
`web` と `nginx` の両方に
```
volumes:
  - static_volume:/home/app/web/staticfiles
```
がある理由を自信を持って理解していると言えない状態。。
現時点の理解を記しておく。
- collectstaticで静的ファイルを集約して格納する先は、`web`コンテナ の `/home/app/web/staticfiles` 。
- `web` の volumes: で紐づけされた Docker管理領域の static_volume には、同じ静的ファイルが保管される。
- `nginx` は（後々追記する）`nginx.conf`によって、`http://〇〇〇〇〇/static/*` のアクセス要求に対し、 `nginx`コンテナの`/home/app/web/staticfiles` を見に行く。
- `nginx` の `volumes:` で `static_volume:/home/app/web/staticfiles` のように紐づけているので、`web`側からstatic_volumeに格納した静的ファイルが、`nginx`側の `/home/app/web/staticfiles`にもコピーされている。
- 結果として、`nginx` の `/home/app/web/staticfiles` に静的ファイルが存在するので表示できる。  

という流れであっているのだろうか。
:::

不明瞭な点はあるけど、お手本を信じて先に進もう！

Dockerfile.prod でstaticfilesディレクトリ作成の１行を追加
```docker: Dockerfile.prod の追加箇所
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
```

nginx がstaticfiles を見に行くようにルーティング
``` nginx.conf
upstream config {
    server web:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://config;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /home/app/web/staticfiles/;
    }
}
```

動作確認開始

```bash
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
```

#### ポカミスによるエラー対応（皆さんは出ないと思うので先に進んでください）


エラーが出た。
```bash
・・・・・・
 => CACHED [builder 5/9] RUN pip install flake8==3.9.2      0.0s
 => CANCELED [builder 6/9] COPY . .　　　　　　　　　　　　　　2.5s
------
 > [stage-1  4/15] RUN mkdir /home/app/web/staticfiles:
#7 1.632 mkdir: can't create directory '/home/app/web/staticfiles': No such file or directory
------
executor failed running [/bin/sh -c mkdir $APP_HOME/staticfiles]: exit code: 1
ERROR: Service 'web' failed to build : Build failed
```

`docker-compose.prod.yml` の編集時、コピペする位置を間違えて、
```yml: docker-compose.prod.ymlの一部（誤記あり状態）
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME
```
という順になっていた。。上下入れ替えたら正常にビルドできた。
**エラー対応おわり。**


続けてマイグレート
```bash
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
```

collectstaticを実施すれば、複数ある staticディレクトリの情報を、staticfiles ディレクトリに集約（コピー）してくれる。
```bash
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
```

admin管理画面を確認しよう。`http://localhost:1337/admin` にアクセスしてcssのお飾りが効いていれば成功だ！

コンテナdownしておこう
```bash
~/*$ docker-compose -f docker-compose.prod.yml down -v
```



## [Media Files](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#media-files)

Media Filesの動作検証のためには、実際に画像をアップロードして、その画像を表示するアプリを用意しなければならない。
コンテナでの編集内容をホスト側にも反映させたいので、バインドマウント設定のある開発モードでビルドする。
ホスト側というのは、WSL2 Ubuntu に作成しているデータのこと。
```bash
~/*$ docker-compose up -d --build
```

webコンテナでDjangoのstartappを実行
```
~/*$ docker-compose exec web python manage.py startapp upload
```

ここで、ホスト側のappディレクトリにもuploadが生成されれば、バインドマウントが働いていることになる。
上の方で少し触れているが、私はバインドマウントが全く働かないパターンに遭遇。別PCでプロジェクトを作り直してうまくいったので、原因は不明なまま。。

先ほどとは逆に、ホスト側で変更した内容が、コンテナにも反映されるはずである。
ホスト側 WSL2 Ubuntu の $~/works/hoge-on-docker/app/config/ のsettings.py を変更して保存する。 

```python: settings.py の一部
INSTALLED_APPS = [
    ・・・・・・,
    ・・・,

    'upload',
]
```

上書き保存。特に問題なし。

次はコンテナ側で生成して、ホスト側に反映された upload の中のファイルを書き換えていく。
```python: app/upload/views.py 
from django.shortcuts import render
from django.core.files.storage import FileSystemStorage

def image_upload(request):
    if request.method == "POST" and request.FILES["image_file"]:
        image_file = request.FILES["image_file"]
        fs = FileSystemStorage()
        filename = fs.save(image_file.name, image_file)
        image_url = fs.url(filename)
        print(image_url)
        return render(request, "upload.html", {
            "image_url": image_url
        })
    return render(request, "upload.html")

```

上書き保存できない。。VScodeエラーメッセージが出る。
```
'views.py' を保存できませんでした。
ファイル 'vscode-remote://wsl+ubuntu/home/●●●/works/hoge-on-docker/app/upload/views.py' を
書き込むことができません (NoPermissions (FileSystemError): 
Error: EACCES: permission denied, open '/home/●●●/works/hoge-on-docker/app/upload/views.py')
```

[『WSL2でDockerを使用する際の権限問題を解決するシンプルな方法（docker-compose.yml使用）』](https://qiita.com/twu_go/items/a449e3006bd74fc7d10d)
こちら記事のWSL2側対処方法 を使わせてもらう。
```bash
~/*$ sudo chown -R $USER:$USER .
```

再試行ボタンが押せる。何も反応なければ無事上書き保存できたことになる。それは同時にコンテナ側にも変更を反映できたことになる。

次は app/upload/templates ディレクトリを作って、そこにupload.htmlを作成
```html: upload.html
{% block content %}

  <form action="{% url "upload" %}" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <input type="file" name="image_file">
    <input type="submit" value="submit" />
  </form>

  {% if image_url %}
    <p>File uploaded at: <a href="{{ image_url }}">{{ image_url }}</a></p>
  {% endif %}

{% endblock %}

```

```python: app/config/urls.py
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static

from upload.views import image_upload

urlpatterns = [
    path("", image_upload, name="upload"),
    path("admin/", admin.site.urls),
]

if bool(settings.DEBUG):
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

```

```python: settings.pyの一部
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
```
:::note info
（作業メモ漏れかも。。）
ホスト側で新しいファイルを作ったら、コンテナ側との権限アンマッチがあるはずなので、この段階で再度、
```bash
~/*$ sudo chown -R $USER:$USER .
```
をしなきゃいけないんじゃないかな？
:::

試運転してみる。
```bash
~/*$ docker-compose up -d --build
```

ブラウザで `http://localhost:8000/` にアクセス。画像を用意して、ファイル選択してsubmit。表示されたリンクを踏んで、画像を表示できればOK。


これまで開発モードで進めてきたので、本番環境側にも変更を加えていく。
docker-compose.prod.yml のwebとnginxのボリュームに追記
```yml: docker-compose.prod.yml
version: '3.8'

services:
  web:
    build:
      context: ./app
      dockerfile: Dockerfile.prod
    command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    expose:
      - 8000
    env_file:
      - ./.env.prod
    depends_on:
      - db
  db:
    image: postgres:13.0-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    env_file:
      - ./.env.prod.db
  nginx:
    build: ./nginx
    volumes:
      - static_volume:/home/app/web/staticfiles
      - media_volume:/home/app/web/mediafiles
    ports:
      - 1337:80
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:
  media_volume:
```

```docker: Dockerfile.prodの一部

# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME/mediafiles
WORKDIR $APP_HOME

```

```nginx.conf
upstream config {
    server web:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://config;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /home/app/web/staticfiles/;
    }

    location /media/ {
        alias /home/app/web/mediafiles/;
    }
}
```

開発モードのコンテナをダウン
```bash
~/*$ docker-compose down -v
```

本番コンテナをビルド
```bash
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
```

#### エラー対応（出なければ先に進んでください）
```bash
 => [builder 6/9] COPY . .                                            3.6s
 => ERROR [builder 7/9] RUN flake8 --ignore=E501,F401 ./config        3.9s
------
 > [builder 7/9] RUN flake8 --ignore=E501,F401 ./config:
#18 2.898 ./config/urls.py:14:81: W292 no newline at end of file
------
executor failed running [/bin/sh -c flake8 --ignore=E501,F401 ./config]: exit code: 1
ERROR: Service 'web' failed to build : Build failed
```

flake8からurls.pyの最後がno newline で怒られた。コピペした時は注意しよう。
最終行を追加してもタブがある状態はダメ。最終行は何もない空白行にしておこう。
ビルドしなおして通った。

続けて migrate と collectstatic。
```bash
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
```

ビルドは通って、`http://localhost:1337/` にもアクセスできたが、画像ファイル選択後、画像表示ページのリンクを押したらエラー
```
Forbidden (403)
CSRF verification failed. Request aborted.

More information is available with DEBUG=True.
```

Django4（4.1？）から、`settings.py` に `CSRF_TRUSTED_ORIGINS` を追記しないといけない。
```python: settings.py の追加行
CSRF_TRUSTED_ORIGINS = ['http://localhost:1337',]
```
再ビルド。。（後述：DEBUG=1 にしてエラーの詳細確認すればよかった）


またflake8
```bash
 => [builder 6/9] COPY . .                                                          2.6s
 => ERROR [builder 7/9] RUN flake8 --ignore=E501,F401 ./config                      4.3s
------
 > [builder 7/9] RUN flake8 --ignore=E501,F401 ./config:
#18 3.614 ./config/settings.py:134:48: E231 missing whitespace after ','
------
executor failed running [/bin/sh -c flake8 --ignore=E501,F401 ./config]: exit code: 1
ERROR: Service 'web' failed to build : Build failed
```

`missing whitespace after ','` のお手当。めんどくさいよ。。
```python: settings.py のプチ修正（,の後に半角スペース追加）
CSRF_TRUSTED_ORIGINS = ['http://localhost:1337', ]
```

今度こそ！
```bash
~/*$ docker-compose -f docker-compose.prod.yml up -d --build
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
```
できた。
お手本サイトに、今のままだとアップロードファイル容量の限界が1MBとある。nginx.conf を変更しておく。
```nginx.conf: nginx.confの一部
location / {
    proxy_pass http://hello_django;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_redirect off;
    client_max_body_size 100M;
}
```

これにて、お手本サイトで進められるところは終了。


## [Conclusion](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/#conclusion)
SSL化の方法が紹介されているが、これだと自分には無理っぽいのであきらめた。調査した結果 https-portal が良いと判断。



# 最後に
記事を書いている最中に[『Dockerイメージの理解を目指すチュートリアル』](https://qiita.com/zembutsu/items/24558f9d0d254e33088f) に検索ヒットしました。すごい。勉強させてもらおう。SSL化であるhttps-portal 追加は次の機会にします。元気が湧いたら書きます。

↓ 書きました
[『Docker学習の終盤、SSL対応とデプロイ』](https://qiita.com/gmasa/items/638167249813d014b482)