48
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-14

はじめに

『個人開発2年間の軌跡』終盤のDocker学習において、お手本サイト『Dockerizing Django with Postgres, Gunicorn, and Nginx』に取り組んだ時の記録です。強強ではない人間がこのサイトを読んでどこに引っかかったのか、参考にしてもらえればと思います。

対象読者

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

開発環境

  • Windows11Pro(2台!

    • 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』(以下、お手本サイト)はプロジェクトディレクトリを作るところからスタートし、途中で内容ジャンプすることなく最後まで連れて行ってくれます。素晴らしい。
私が学習した際に、お手本とは異なる作業したこと、疑問に思ったこと、問題解決したこと、これらをお手本サイトの進行に沿って記しました。見出しが該当項目へのリンクになっています。
丁寧に説明されている箇所はこちらの記事に再掲していないので、 まずはリンク先のお手本サイトに目を通してください:pray:
とは言いつつ、お手本まんまのことを書いた箇所も多々ありますが、それはあっちとこっちを行ったり来たりせず、作業の流れをつかめるようにするためです。(私自身のブランク明けの作業手順書にもしたかったので。。)

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

Project Setup

ホームディレクトリに作業場を用意(worksという名前にした)

~$ 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

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

Djangoインストール

Djangoのバージョンは現時点で一番新しいものにした。セキュリティの専門知識を持ち合わせていないので、せめてフレームワークのバージョンは新しいものを使おう、という考え。

(.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 で停止。

ディレクトリ名について
今まで『Django for Beginners』の考え方に則り、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 を記入。

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

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

Docker

app ディレクトリ直下に 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 . .

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

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

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

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.pySECRET_KEY の ' 'で囲われた文字列をコピペ。(両端の ' は不要です。)

settings.py に追記・変更

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(" ")

イメージのビルド

プロジェクト直下に移動

~/*/app$ cd ..

docker-compose.yml を使ってイメージをビルド

~/*$ docker-compose build

コンテナを生成し、バックグラウンド動作(デーモン)

~/*$ docker-compose up -d

http://localhost:8000/ にアクセス。

ログ確認
ロケット画面が表示されない場合は、ログ確認。

~/*$ docker-compose logs -f

Postgres

Postgres の導入。docker-compose.yml にdb情報を追記する。

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:

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 の変更

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 の変更

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 . .

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

イメージのビルドとコンテナ起動を同時に実行する書き方はこれ。

~/*$ docker-compose up -d --build

マイグレート

~/*$ docker-compose exec web python manage.py migrate --noinput

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

Postgres が動いていれば、デフォルトの Django テーブルが作成された様子を確認できるはず。

~/*$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev

以下、=# の右辺が入力するコマンド。

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

ボリュームの詳細を確認してみる

~/*$ docker volume inspect hoge-on-docker_postgres_data
                ↑ _postgres_dataの前はプロジェクト名。自分の環境に合わせる。

以下のような表示が含まれていればOK

[
    {
        "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 ファイルの実行権限を変更しておく

~/*$ chmod +x app/entrypoint.sh

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 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

イメージのビルドとコンテナ起動

~/*$ docker-compose up -d --build

ブラウザで http://localhost:8000/ にアクセスしてロケット表示されればOK。

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

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 の書替えを忘れずに)
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 を付ける)

~/*$ docker-compose down -v

本番イメージのビルドとコンテナ起動

~/*$ 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 を忘れずに。

~/*$ docker-compose -f docker-compose.prod.yml logs -f

Production Dockerfile

本番用の Dockerfile を用意する。
現時点の Dockerfile と entrypoint.sh の組み合わせでは、コンテナを起動する度に、

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 "$@"

続けて、ファイルの実行権限を変更する

~/*$ chmod +x app/entrypoint.prod.sh

Dockerfile.prod を用意

本番イメージの容量削減のため、BuilderとFinalの2段構成。マルチステージビルド という手法を取り入れる。

  • Builder

    1. psycopg2を動かすのに必要なアプリのインストール。
    2. flake8(lint)をインストールし実行してDjangoのapp内のコードに問題が無いかチェック。
    3. pip wheel ***** で requirements.txtのパッケージインストールに必要な wheel群を構築。wheel構築の利点やオプションの解説は、https://kurozumi.github.io/pip/reference/pip_wheel.html を参照。
  • Final

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

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

【参考】改行コードの変更

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

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

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

準備ができたので実行してみる。まっさらにしてからビルドして、実行する。

~/*$ docker-compose -f docker-compose.prod.yml down -v
~/*$ docker-compose -f docker-compose.prod.yml up -d --build

このようなエラーが出ると思う。

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分以上かかる。長い。。

~/*$ 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

Nginx の導入
docker-compose.prod.yml に追記

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

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

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

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なし管理画面のログインが表示される。

コンテナをダウンしておこう

~/*$ docker-compose -f docker-compose.prod.yml down -v

Static Files

Static Files の設定

本題に入る前に、忘れないうちにsettings.pyの地域設定を変更。

settings.py の変更点
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

続けて、Static Filesに関する settings.py の 変更・追記

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 は手入力で色々設定をしてあげないといけない。
本番環境用の設定を順番に行う。

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:

理解不足メモ
正直、お手本サイトの以下のテキストが理解できていない。(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)
  2. Change the permissions of the directory after it's mounted (source)
    We used the former.

理解不足メモ
webnginx の両方に

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 を見に行く。
  • nginxvolumes:static_volume:/home/app/web/staticfiles のように紐づけているので、web側からstatic_volumeに格納した静的ファイルが、nginx側の /home/app/web/staticfilesにもコピーされている。
  • 結果として、nginx/home/app/web/staticfiles に静的ファイルが存在するので表示できる。

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

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

Dockerfile.prod でstaticfilesディレクトリ作成の1行を追加

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/;
    }
}

動作確認開始

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

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

エラーが出た。

・・・・・・
 => 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 の編集時、コピペする位置を間違えて、

docker-compose.prod.ymlの一部(誤記あり状態)
RUN mkdir $APP_HOME/staticfiles
RUN mkdir $APP_HOME

という順になっていた。。上下入れ替えたら正常にビルドできた。
エラー対応おわり。

続けてマイグレート

~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput

collectstaticを実施すれば、複数ある staticディレクトリの情報を、staticfiles ディレクトリに集約(コピー)してくれる。

~/*$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear

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

コンテナdownしておこう

~/*$ docker-compose -f docker-compose.prod.yml down -v

Media Files

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

~/*$ 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 を変更して保存する。

settings.py の一部
INSTALLED_APPS = [
    ・・・・・・,
    ・・・,

    'upload',
]

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

次はコンテナ側で生成して、ホスト側に反映された upload の中のファイルを書き換えていく。

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使用)』
こちら記事のWSL2側対処方法 を使わせてもらう。

~/*$ sudo chown -R $USER:$USER .

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

次は app/upload/templates ディレクトリを作って、そこにupload.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 %}

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)

settings.pyの一部
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"

(作業メモ漏れかも。。)
ホスト側で新しいファイルを作ったら、コンテナ側との権限アンマッチがあるはずなので、この段階で再度、

~/*$ sudo chown -R $USER:$USER .

をしなきゃいけないんじゃないかな?

試運転してみる。

~/*$ docker-compose up -d --build

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

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

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:
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/;
    }
}

開発モードのコンテナをダウン

~/*$ docker-compose down -v

本番コンテナをビルド

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

エラー対応(出なければ先に進んでください)

 => [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。

~/*$ 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.pyCSRF_TRUSTED_ORIGINS を追記しないといけない。

settings.py の追加行
CSRF_TRUSTED_ORIGINS = ['http://localhost:1337',]

再ビルド。。(後述:DEBUG=1 にしてエラーの詳細確認すればよかった)

またflake8

 => [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 ',' のお手当。めんどくさいよ。。

settings.py のプチ修正(,の後に半角スペース追加)
CSRF_TRUSTED_ORIGINS = ['http://localhost:1337', ]

今度こそ!

~/*$ 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の一部
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

SSL化の方法が紹介されているが、これだと自分には無理っぽいのであきらめた。調査した結果 https-portal が良いと判断。

最後に

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

↓ 書きました
『Docker学習の終盤、SSL対応とデプロイ』

48
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?