はじめに
『個人開発2年間の軌跡』終盤のDocker学習において、お手本サイト『Dockerizing Django with Postgres, Gunicorn, and Nginx』に取り組んだ時の記録です。強強ではない人間がこのサイトを読んでどこに引っかかったのか、参考にしてもらえればと思います。
対象読者
Djangoユーザー かつ Docker初心者を対象にしています。個人開発でDockerを使えるようになりたい人の助けになれば幸いです。
開発環境
-
Windows11Pro(2台!)
- Homeでは試していませんが、たぶん問題ないです。
- 問題解決能力が低い初心者の時こそ、PC複数台持つことをおススメします。コスパ重視、中古でOKです。いつでもOSクリーンインストールして構わないマシンがあると心強いです。
- この記事は一度で作業完了しているように書いていますが、実際はたくさんハマっています。英語サイトをググって色々試しても解決に至らないケースでは、別のPCに同じ方法で環境設定して、同じ作業を実施したところ正常動作させることができました。もうなんなの
-
WSL2 Ubuntu
- インストール手順は色々ありそうですが、どれでも大丈夫だと思います。
-
VScode
- VScodeターミナルで WSL2 Ubuntu bashを操作できる状態にしてください。環境設定はググってわかりやすいサイトを探してください。
-
Python
- WSL2 Ubuntu に始めから入っているPythonは使わず、新しいバージョンのPythonを追加インストールしてそちらを使うようにしてください。
- 私はお手本と同じVer.3.9.6を入れました。
ゼロから構築する作業を追体験
『Dockerizing Django with Postgres, Gunicorn, and Nginx』(以下、お手本サイト)はプロジェクトディレクトリを作るところからスタートし、途中で内容ジャンプすることなく最後まで連れて行ってくれます。素晴らしい。
私が学習した際に、お手本とは異なる作業したこと、疑問に思ったこと、問題解決したこと、これらをお手本サイトの進行に沿って記しました。見出しが該当項目へのリンクになっています。
丁寧に説明されている箇所はこちらの記事に再掲していないので、 まずはリンク先のお手本サイトに目を通してください
とは言いつつ、お手本まんまのことを書いた箇所も多々ありますが、それはあっちとこっちを行ったり来たりせず、作業の流れをつかめるようにするためです。(私自身のブランク明けの作業手順書にもしたかったので。。)
英語サイト読解時は、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
ファイルの扱い方を調べて、組み込んでみるのが良いかもしれません。君はやらないんかいってツッコミが痛い )
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と同じ階層)
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
を作成。
DEBUG=1
SECRET_KEY=〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇〇
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
SECRET_KEY の右辺には、app/config
ディレクトリの下にいる settings.py
の SECRET_KEY
の ' 'で囲われた文字列をコピペ。(両端の ' は不要です。)
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情報を追記する。
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 に追記
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 の変更
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 の変更
# 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 で行こう
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
を追加
#!/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 の変更
# 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 に追記
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 に追記。
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.yml 複製直後の docker-compose.prod.yml に含まれている、db イメージ生成に必要なPOSTGRES環境変数を、この
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:
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。コピペを使って書く時は注意)
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 を用意
#!/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
- psycopg2を動かすのに必要なアプリのインストール。
- flake8(lint)をインストールし実行してDjangoのapp内のコードに問題が無いかチェック。
- pip wheel ***** で requirements.txtのパッケージインストールに必要な wheel群を構築。wheel構築の利点やオプションの解説は、https://kurozumi.github.io/pip/reference/pip_wheel.html を参照。
-
Final
- libpq(PostgreSQLのC言語インタフェース)のインストール。
- Builderで使ったrequirements.txtと、Builder で構築したwheel群の取り込み。これを使ってインストール。
- entrypoint.prod.shのコピーなど
###########
# 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
としている。正規表現の箇所は、修行が足りていないということで先に進む。
今の docker-compose.prod.yml
の web
は Dockerfile
を読む状態になっているので、Dockerfile.prod
を読み込むように変更する。
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 だけでいいや、ってことで先に進む。(このテキトウすぎる態度はマネしないでください)
再ビルドしてマイグレート。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 に追記
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
プロジェクトルート(つまりhoge-on-docker/)に nginx
ディレクトリを作成し、その下に Dockerfile
と nginx.conf
を作成する。
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
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
に直接アクセスできないことを確認しておくといい。
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の地域設定を変更。
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
続けて、Static Filesに関する 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
は手入力で色々設定をしてあげないといけない。
本番環境用の設定を順番に行う。
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:
理解不足メモ
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ディレクトリ作成の1行を追加
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
RUN mkdir $APP_HOME/staticfiles
WORKDIR $APP_HOME
nginx がstaticfiles を見に行くようにルーティング
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
の編集時、コピペする位置を間違えて、
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 を変更して保存する。
INSTALLED_APPS = [
・・・・・・,
・・・,
'upload',
]
上書き保存。特に問題なし。
次はコンテナ側で生成して、ホスト側に反映された upload の中のファイルを書き換えていく。
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を作成
{% 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 %}
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)
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のボリュームに追記
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:
# 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
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.py
に CSRF_TRUSTED_ORIGINS
を追記しないといけない。
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 ','
のお手当。めんどくさいよ。。
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 を変更しておく。
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対応とデプロイ』