Как использовать Django, PostgreSQL и Docker
В этом уроке мы создадим новый проект 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
сервисов
Для других советов работы с производственной средой, см эту дисскусию.
Спасибо за чтение
Источники используемые для этой статьи
- William Vincent — How to use Django, PostgreSQL, and Docker
- Michael Herman — Dockerizing Django with Postgres, Gunicorn, and Nginx
Годная статья. Мне как раз надо было обернуть Django/Postgres в Docker.
Кстати, пропущена одна строка в файле app/hello_django/urls.py:
Из-за отсутствия этой строки возникала ошибка:
Со статиками не понятно, static_volume это вообще откуда? Нужно в корень проекта закинуть статики в эту папку? Но на гите нет такого, да и вообще этой папки нет. Не ясно в общем что делать дальше.
Статья очень слабая, хоть объемная потерял время и не более
При создании каталога проекта вместе с новым проектом Django возникают ошибки
Подскажите как исправить ошибки
Александр, команда sourse это для линукс.
Под Windows нужно писать без sourse, т.е. env\Scripts\activate, либо если назвали не env, то имя вирт.окружения.
python manage.py migrate нужно запускать из созданного каталога проекта hello_django, для этого сделай cd hello_django и уже после сделай migrate.
После команды docker-compose up -d —build, контейнер с postgresql работает исправно, а контейнер с приложением выводит ошибку
https://stackoverflow.com/questions/51508150/standard-init-linux-go190-exec-user-process-caused-no-such-file-or-directory
В первом случае (dev) сразу поменял т.к. нет bash’a. Но второй убил минут 40, просто скопировал с сайта. Лучше все делать за раз)
Шикарная статья! Лучшее, что мне попадалось по теме Django+Docker. Все подробно, поэтапно и не упущен момент настройки прод среды, что часто делают в подобных туториалах.
З.Ы. Было бы удобно иметь это руководство в виде репозитория — чтобы можно было использовать как заготовку для шаблона. Также было бы удобно тем, у кого возникают проблемы и ошибки в процессе проверки примеров из статьи.
Автору огромное спасибо! Статья реально помогла разобраться в теме. После того, как ручками прошел все этапы от начала до конца, смог наконец-то сдать свой учебный проект на котором застрял.
Объясните,пожалуйста,что за переменная os в файле settings.py?
в начале файла напиши
import os
Подскажите,пожалуйста,как создать файл .env.dev в pycharm?
Правой кнопкой кликнуть на папке и вбрать пункт с созданием нового файла. Или прописать «touch .env.dev» в терминале
Какие альтернативные способы существуют для хранения media-файлов тому, который предложен в статье?
Буду рад полезным ссылкам)
у docker-compose.override есть некоторые особенности, какие-то параметры он переопределяет, какие-то мёрджит. Подробнее на сайте в документации. Жаль они отказались от extends.
Отличная статья. Просто повторяя за автором развернул и dev и prod. Спасибо!)
Ссылку на гитхаб можно было оставить
Отличная статья, лучшее из всего что смог найти по данной теме, все получилось, спасибо!
набираю docker build .
получаю ошибку
что с этим можно сделать?
=> ERROR [5/6] RUN pip install -r requirements.txt 3.8s
——
> [5/6] RUN pip install -r requirements.txt:
#9 1.532 Collecting asgiref==3.5.2
#9 1.700 Downloading asgiref-3.5.2-py3-none-any.whl (22 kB)
#9 1.873 Collecting Django==4.1
#9 1.914 Downloading Django-4.1-py3-none-any.whl (8.1 MB)
#9 2.716 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.1/8.1 MB 10.1 MB/s eta 0:00:00
#9 2.844 Collecting psycopg2==2.9.3
#9 2.888 Downloading psycopg2-2.9.3.tar.gz (380 kB)
#9 2.923 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 380.6/380.6 kB 11.9 MB/s eta 0:00:00
#9 2.992 Preparing metadata (setup.py): started
#9 3.669 Preparing metadata (setup.py): finished with status ‘error’
#9 3.676 error: subprocess-exited-with-error
#9 3.676
#9 3.676 × python setup.py egg_info did not run successfully.
#9 3.676 │ exit code: 1
#9 3.676 ╰─> [23 lines of output]
#9 3.676 running egg_info
#9 3.676 creating /tmp/pip-pip-egg-info-8xv9u3bg/psycopg2.egg-info
#9 3.676 writing /tmp/pip-pip-egg-info-8xv9u3bg/psycopg2.egg-info/PKG-INFO
#9 3.676 writing dependency_links to /tmp/pip-pip-egg-info-8xv9u3bg/psycopg2.egg-info/dependency_links.txt
#9 3.676 writing top-level names to /tmp/pip-pip-egg-info-8xv9u3bg/psycopg2.egg-info/top_level.txt
#9 3.676 writing manifest file ‘/tmp/pip-pip-egg-info-8xv9u3bg/psycopg2.egg-info/SOURCES.txt’
#9 3.676
#9 3.676 Error: pg_config executable not found.
#9 3.676
#9 3.676 pg_config is required to build psycopg2 from source. Please add the directory
#9 3.676 containing pg_config to the $PATH or specify the full executable path with the
#9 3.676 option:
#9 3.676
#9 3.676 python setup.py build_ext —pg-config /path/to/pg_config build …
#9 3.676
#9 3.676 or with the pg_config option in ‘setup.cfg’.
#9 3.676
#9 3.676 If you prefer to avoid building psycopg2 from source, please install the PyPI
#9 3.676 ‘psycopg2-binary’ package instead.
#9 3.676
#9 3.676 For further information please check the ‘doc/src/install.rst’ file (also at
#9 3.676 <https://www.psycopg.org/docs/install.html>).
#9 3.676
#9 3.676 [end of output]
#9 3.676
#9 3.676 note: This error originates from a subprocess, and is likely not a problem with pip.
#9 3.678 error: metadata-generation-failed
#9 3.678
#9 3.678 × Encountered error while generating package metadata.
#9 3.678 ╰─> See above for output.
#9 3.678
#9 3.678 note: This is an issue with the package mentioned above, not pip.
#9 3.678 hint: See above for details.
——
executor failed running [/bin/sh -c pip install -r requirements.txt]: exit code: 1
В Dockerfile FROM python:3.9