Сьогодні ми створимо новий проект Django, використовуючи Docker та PostgreSQL. Django поставляється з вбудованою підтримкою SQLite, але навіть для локальної розробки краще використовувати "справжню" базу даних, таку як PostgreSQL, яка відповідає виробничій.
Можна запускати PostgreSQL локально, використовуючи такий інструмент, як Postgres.app, проте сьогодні серед багатьох розробників кращим є використання Docker, інструмент для створення ізольованих операційних систем. Найпростіше представляти це як віртуальне середовище, яке містить все необхідне для нашого проекту Django: залежність, бази даних, служби кешування та будь-які інші необхідні інструменти.
Основною причиною використання Docker є те, що він повністю усуває будь-які проблеми, пов'язані з локальною розробкою. Замість того, щоб турбуватися про те, які програмні пакети встановлені або працюють з локальною базою даних разом із проектом, ви просто запускаєте образ Docker всього проекту. Найкраще те, що цим можна поділитись у групах та значно спростити розробку команди.
Інсталяція Docker
Першим кроком є встановлення настільної програми Docker для вашого локального комп'ютера:
Початкове завантаження Docker може тривати деякий час.
Після завершення встановлення Docker ми можемо підтвердити, що запущено правильну версію. У терміналі запустіть команду docker —version.
$ docker --version
Docker version 19.03.2, build 6a30dfc
Docker Compose — це додатковий інструмент, який автоматично включається у завантаження Docker для Mac та Windows. Однак, якщо ви використовуєте Linux, вам потрібно буде додати його вручну. Ви можете це зробити, виконавши команду sudo pip install docker-compose після завершення установки Docker.
Проект Django
Створіть новий каталог проекту разом із новим проектом Django:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.8 -m venv env
$ source env/bin/activate
(env)$ pip install django==3.0.7
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
Перейдіть за адресою http://localhost:8000/, щоб переглянути екран привітання Django. Зупиніть сервер і вийдіть із віртуального середовища. Тепер ми маємо простий проект Django для роботи.
Створіть файл requirements.txt у каталозі app і додайте Django як залежність:
Django==3.0.7
Оскільки ми будемо використовувати Postgres як БД для проекту, видаліть файл db.sqlite3 з каталогу app.
Ваша директорія проекту має виглядати так:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Docker
Docker вже повинен був завершити установку на цей момент. Щоб переконатися, що інсталяція пройшла успішно, закрийте локальний сервер за допомогою Control + c, а потім введіть у командному рядку docker run hello-world. Ви повинні побачити відповідь на кшталт цього:
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Pull complete
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image whi
ch runs the executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client,
which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container
with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Образи та контейнери
У Docker є дві важливі концепції: образи (images) та контейнери (containers).
- Image: список інструкцій для всіх програмних пакетів у ваших проектах
- Container: екземпляр образу під час виконання
Інакше кажучи, образ (image) визначає, що станеться, а контейнер (container) — те, що фактично виконується.
Для налаштування образів та контейнерів у Docker ми використовуємо два файли: Dockerfile та docker-compose.yml.
Dockerfile містить список інструкцій для образу, інакше кажучи, що відбувається в середовищі контейнера.
Створимо новий файл Dockerfile.
(env) $ touch Dockerfile
Потім додайте наступний код до нього:
# pull official base image
FROM python:3.8.3-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 . .
У верхньому рядку ми використовуємо офіційний образ Docker для Python 3.8. Далі ми створюємо два змінні оточення.
Потім ми встановлюємо робочий каталог разом із двома змінними середовищами:
PYTHONUNBUFFERED гарантує, що виведення консолі виглядає знайомим і не буферизується Docker, що нам не потрібно. PYTHONDONTWRITEBYTECODE означає, що Python не намагатиметься створювати файли .pyc, які ми також не бажаємо.
Нарешті ми оновили pip, скопіювали файл requirements.txt, встановили залежності та скопіювали сам проект Django.
Ми не можемо запустити Docker-контейнер, доки у нас не буде створеного образу, тож давайте зробимо це:
(env) $ docker build .
У разі успіху у вас має бути щось таке:
Sending build context to Docker daemon 162.3kB
Step 1/8 : FROM python:3.7
3.7: Pulling from library/python
c7b7d16361e0: Pull complete
b7a128769df1: Pull complete
1128949d0793: Pull complete
667692510b70: Pull complete
bed4ecf88e6a: Pull complete
8a8c75f3996a: Pull complete
10b7379e5573: Pull complete
ca1b6fe24628: Pull complete
9a90211ec083: Pull complete
Digest: sha256:fc0a398e1987fb1e58909053c11630e06adb3df265fe693391779020b9253f5e
Status: Downloaded newer image for python:3.7
---> 9fa56d0addae
Step 2/8 : ENV PYTHONDONTWRITEBYTECODE 1
---> Running in 5e7a7983814d
Removing intermediate container 5e7a7983814d
---> 3aff2533de96
....
Successfully built 98329412f14c
Далі нам потрібний новий файл docker-compose.yml. Він каже Docker, як запустити наші Docker-контейнери. У нас будуть 2 контейнери. Один для бази, інший для програми.
(app) $ touch docker-compose.yml
Спочатку додамо в нього один контейнер для програми:
version: '3.7'
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
Оновіть змінні SECRET_KEY, DEBUG та ALLOWED_HOSTS у settings.py:
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' должен быть в виде одной строки с хостами разделенными символом пробела
# Для примера: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Потім створіть файл .env.dev у корені проекту для зберігання змінних середовища для розробки:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
Збираємо образ командою:
$ docker-compose build
Як тільки образ буде зібрано, запускаємо контейнер:
$ docker-compose up -d
Далі потрібно перейти за адресою http://localhost:8000/, щоб знову побачити екран вітання та переконатися, що все працює.
Перевірте наявність помилок у журналах, якщо це не працює через команду:
docker-compose logs -f
Докер налаштований!
Підключаємо PostgreSQL
Щоб настроїти Postgres, нам потрібно додати новий сервіс до файлу docker-compose.yml, оновити налаштування Django та встановити Psycopg2.
Спочатку додамо новий сервіс db у docker-compose.yml:
version: '3.7'
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:12.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:
Щоб зберегти дані за межами контейнера, ми настроїли том (volume). Цей конфіг буде пов'язувати postgres_data з каталогом /var/lib/postgresql/data/ в контейнері.
Ми також додали ключ середовища, щоб визначити ім'я для бази даних за промовчанням та встановити ім'я користувача та пароль.
Тому внесемо відповідні зміни до файлу .env.dev:
DEBUG=1
SECRET_KEY=foo
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, щоб вказати, що ми будемо використовувати PostgreSQL, а не SQLite.
DATABASES = {
"default": {
"ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("SQL_DATABASE", os.path.join(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, щоб встановити відповідні пакети, необхідні для Psycopg2:
# pull official base image
FROM python:3.8.3-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 у файл requirements.txt:
Django==3.0.7
psycopg2-binary==2.8.5
Зберемо новий образ і запустимо два контейнери:
$ docker-compose up -d --build
Запустимо міграцію:
$ docker-compose exec web python manage.py migrate --noinput
Якщо отримаєте таку помилку:
django.db.utils.OperationalError: FATAL: database "hello_django_dev" does not exist
Зупиніть контейнер командою docker-compose down -v, щоб видалити томи разом із контейнерами. Потім заново створіть образи, запустіть контейнери та застосуйте міграції.
Переконайтеся, що всі таблиці Django були створені за замовчуванням:
$ docker-compose exec db psql --username=hello_django --dbname=hello_django_dev
psql (12.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
Ви також можете перевірити, що том (volume) був створений, запустивши команду:
$ docker volume inspect django-on-docker_postgres_data
Ви повинні побачити щось схоже на:
[
{
"CreatedAt": "2020-06-13T18:43:56Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.25.4",
"com.docker.compose.volume": "postgres_data"
},
"Mountpoint": "/var/lib/docker/volumes/django-on-docker_postgres_data/_data",
"Name": "django-on-docker_postgres_data",
"Options": null,
"Scope": "local"
}
]
Потім додамо файл entrypoint.sh до каталогу проекту app, щоб перевірити працездатність Postgres перед застосуванням міграцій та запуском сервера розробки Django:
#!/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 "$@"
Оновимо локальні права доступу до файлу:
$ chmod +x app/entrypoint.sh
Потім оновимо Dockerfile, щоб скопіювати файл entrypoint.sh і запустити його як команду точки входу Docker:
# pull official base image
FROM python:3.8.3-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 .
# copy project
COPY . .
# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
Додамо змінне середовище DATABASE в .env.dev:
DEBUG=1
SECRET_KEY=foo
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
Перевірте все знову:
- Перезберемо заново образи
- Запустимо контейнери
- Перейдемо на сторінку http://localhost:8000/
Примітка
По-перше, незважаючи на додавання Postgres, ми все одно можемо створити незалежний образ Docker для Django, якщо для змінного середовища DATABASE не встановлено значення postgres.
Щоб перевірити, створіть новий образ, а потім запустіть новий контейнер:
$ docker build -f ./app/Dockerfile -t hello_django:latest ./app
$ docker run -d \
-p 8006:8000 \
-e "SECRET_KEY=please_change_me" -e "DEBUG=1" -e "DJANGO_ALLOWED_HOSTS=*" \
hello_django python /usr/src/app/manage.py runserver 0.0.0.0:8000
Ви повинні побачити сторінку привітання на http://localhost:8006.
По-друге, ви можете закоментувати команди очищення (flush) та міграції (migrate) бази даних у сценарії 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 "$@"
Натомість ви можете запустити їх вручну, після того, як контейнери запустяться, ось так:
$ docker-compose exec web python manage.py flush --no-input
$ docker-compose exec web python manage.py migrate
Невеликий перелік команд Docker
Коли ви закінчите, не можете погасити контейнер: Docker: docker-compose down
Просто зупинити контейнер: docker stop CONTAINER ID
Запустити раніше зупинений контейнер: docker start CONTAINER ID
Перевантажити контейнер: docker restart CONTAINER ID
Побачити працюючі контейнери: docker ps
Побачити взагалі усі контейнери: docker ps -a
Переглянути всі зображення: docker images
Видалити образ: docker rmi CONTAINER ID або docker rmi -f CONTAINER ID
Іноді може знадобитися зайти в працюючий контейнер. Для цього потрібно запустити команду запуску інтерактивної оболонки bash: docker exec -it CONTAINER ID bash
Gunicorn
Рухаючись далі, у виробниче середовище, давайте додамо Gunicorn, сервер WSGI промислового рівня, файл requirements.txt:
Django==3.0.7
gunicorn==20.0.4
psycopg2-binary==2.8.5
Оскільки ми ще хочемо використовувати вбудований сервер Django для розробки, створіть новий файл compose під назвою docker-compose.prod.yml для виробничого середовища:
version: '3.7'
services:
web:
build: ./app
command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000
ports:
- 8000:8000
env_file:
- ./.env.prod
depends_on:
- db
db:
image: postgres:12.0-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- ./.env.prod.db
volumes:
postgres_data:
Якщо у вас є декілька середовищ, ви можете використовувати конфігураційний файл docker-compose.override.yml. При такому підході ви додаєте базову конфігурацію до файлу docker-compose.yml, а потім використовуєте файл docker-compose.override.yml для перевизначення цих параметрів конфігурації залежно від середовища.
Зверніть увагу на команду command. Ми використовуємо Gunicorn, а не сервер розробки Django. Ми також видалили том з web, оскільки він нам не потрібен. Нарешті, ми використовуємо окремі файли змінних середовища для визначення змінних середовищ для обох служб, які будуть передані в контейнер під час виконання.
Файл .env.prod:
DEBUG=0
SECRET_KEY=change_me
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:
POSTGRES_USER=hello_django
POSTGRES_PASSWORD=hello_django
POSTGRES_DB=hello_django_prod
Додамо ці два файли в кореневий каталог проекту. Можливо, ви захочете виключити їх git, тому додайте їх до файлу .gitignore.
Переконаємося, що у нас всі контейнери зупинені (і пов'язані томи з прапором -v):
$ docker-compose down -v
Потім зберемо виробничі образи та запустимо контейнери:
$ docker-compose -f docker-compose.prod.yml up -d --build
Переконайтеся, що база даних hello_django_prod була створена разом із таблицями Django за замовчуванням. Протестуємо сторінку адміністратора за адресою http://localhost:8000/admin. Статичні файли не завантажуються. Це очікується, оскільки режим налагодження вимкнено. Ми виправимо це найближчим часом.
Виробничий Dockerfile
Ви помітили, що ми все ще виконуємо очищення бази даних (flush) (яка очищає базу даних) та переносимо команди під час кожного запуску контейнера? Це добре у розробці, але давайте створимо новий файл точки входу для виробництва.
Файл 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 з ім'ям Dockerfile.prod для використання з виробничими зборками:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.8.3-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
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.8.3-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 $APP_HOME
# 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"]
Тут ми використовували багатоетапне складання (multi-stage build) Docker, щоб зменшити остаточний розмір образу. По суті, builder — це тимчасовий образ, який використовується для збирання Python. Потім він копіюється у кінцевий виробничий образ, а образ builder відкидається.
Ви помітили, що ми створили користувача без повноважень root? За замовчуванням Docker запускає контейнерні процеси, як root всередині контейнера. Це погана практика, оскільки зловмисники можуть отримати root-доступ до хоста Docker, якщо їм вдасться вирватись із контейнера. Якщо ви root у контейнері, ви будете root на хості.
Оновіть web сервіс у файлі docker-compose.prod.yml для збирання за допомогою 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
$ docker-compose -f docker-compose.prod.yml exec web python manage.py migrate --noinput
Nginx
Далі давайте додамо Nginx, щоб він діяв як зворотний проксі-сервер для Gunicorn для обробки запитів клієнтів, а також для обслуговування статичних файлів.
Додамо сервіс nginx у docker-compose.prod.yml:
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
Потім у локальному корені проекту створіть такі файли та папки:
└── nginx
├── Dockerfile
└── nginx.conf
Файл Dockerfile:
FROM nginx:1.19.0-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
Файл nginx.conf:
upstream hello_django {
server web:8000;
}
server {
listen 80;
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;
}
}
Потім оновимо сервіс web у docker-compose.prod.yml, замінити ports на expose:
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
Тепер порт 8000 відкритий лише для інших сервісів Docker. І цей порт більше не буде опублікований на хост-машині.
Перевіряємо, як це працює:
$ 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.
Структура вашого проекту тепер має виглядати так:
├── .env.dev
├── .env.prod
├── .env.prod.db
├── .gitignore
├── app
│ ├── Dockerfile
│ ├── Dockerfile.prod
│ ├── entrypoint.prod.sh
│ ├── entrypoint.sh
│ ├── hello_django
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── requirements.txt
├── docker-compose.prod.yml
├── docker-compose.yml
└── nginx
├── Dockerfile
└── nginx.conf
Тепер знову зупинимо контейнери:
$ docker-compose -f docker-compose.prod.yml down -v
Оскільки Gunicorn є сервером програм, він не обслуговуватиме статичні файли. Отже, налаштуємо обробку статичних та мультимедійних файлів.
Статичні файли
Оновимо settings.py:
STATIC_URL = "/staticfiles/"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
Development
Тепер будь-який запит на http://localhost:8000/staticfiles/ * буде обслуговуватися з каталогу staticfiles.
Щоб перевірити, спочатку перезберемо образи та запустимо нові контейнери у звичайному режимі. Переконаємося, що статичні файли, як і раніше, правильно обслуговуються за адресою http://localhost:8000/admin.
Production
Для виробничого середовища додайте volume у web та служби nginx у docker-compose.prod.yml, щоб кожен контейнер мав спільний каталог з ім'ям «staticfiles»:
version: '3.7'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.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:12.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:
Нам також необхідно створити папку "/home/app/web/staticfiles" у 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
WORKDIR $APP_HOME
...
Чому це потрібно?
Docker Compose зазвичай монтує названі томи як root. І оскільки ми використовуємо користувача без повноважень root, ми отримаємо помилку відмови у дозволі при запуску команди collectstatic, якщо каталог ще не існує.
Щоб обійти це, ви можете:
- Створити папку в Dockerfile
- Змінити права доступу до каталогу після його монтування
Ми використали перше.
Потім оновіть конфігурацію Nginx для маршрутизації запитів статичних файлів у папку staticfiles:
upstream hello_django {
server web:8000;
}
server {
listen 80;
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;
}
location /staticfiles/ {
alias /home/app/web/staticfiles/;
}
}
Перезапустимо контейнери:
$ docker-compose 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
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Тепер усі запити на http://localhost:1337/staticfiles/ * будуть обслуговуватися з каталогу «staticfiles».
Перейдіть за адресою http://localhost:1337/admin та переконайтеся, що статичні ресурси завантажуються правильно.
Ви також можете перевірити у логах командою docker-compose -f docker-compose.prod.yml logs -f, що запити до статичних файлів успішно обробляються через Nginx:
Далі знову зупинимо контейнери:
$ docker-compose -f docker-compose.prod.yml down -v
Media файли
Щоб перевірити обробку мультимедійних файлів, почніть із створення нового модуля Django:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
Додамо новий модуль у INSTALLED_APPS у settings.py:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"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")
Додамо директорію «templates» у каталог «app/upload» та додамо новий шаблон 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/hello_django/urls.py:
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)
Файл app/hello_django/settings.py:
MEDIA_URL = "/mediafiles/"
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")
Development
Запустимо контейнер:
$ docker-compose up -d --build
Тепер у вас має бути можливість загулити файл на http://localhost:8000/, а потім побачити цей файл на http://localhost:8000/mediafiles/IMAGE_FILE_NAME.
Production
Для виробничого середовища додамо новий том volume у послуги web і nginx:
version: '3.7'
services:
web:
build:
context: ./app
dockerfile: Dockerfile.prod
command: gunicorn hello_django.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:12.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:
Створимо каталог /home/app/web/mediafiles у 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:
upstream hello_django {
server web:8000;
}
server {
listen 80;
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;
}
location /staticfiles/ {
alias /home/app/web/staticfiles/;
}
location /mediafiles/ {
alias /home/app/web/mediafiles/;
}
}
Далі перезапустимо контейнери:
$ docker-compose 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
$ docker-compose -f docker-compose.prod.yml exec web python manage.py collectstatic --no-input --clear
Перевіримо, як усе працює:
- Звантажимо файл http://localhost:1337/.
- Потім переконаємось, що файл доступний на http://localhost:1337/mediafiles/IMAGE_FILE_NAME.
Висновок
У цій статті ми розглянули, як створити контейнер для веб-застосунку Django з Postgres. Ми також створили готовий до роботи файл Docker Compose, який додає Gunicorn та Nginx у нашу конфігурацію для обробки статичних та мультимедійних файлів. Тепер ви можете перевірити виробниче налаштування локально.
З погляду фактичного розгортання у виробничому середовищі, ви, ймовірно, захочете використати:
- Повністю керований сервіс бази даних, такий як RDS або Cloud SQL, замість того, щоб керувати власним екземпляром Postgres в контейнері.
- Користувач без повноважень root для db та nginx сервісів.
Працювати з Back-end вчать на курсах Пайтон 👇