本文详细介绍了如何配置 Django 以在 Docker 上运行 Postgres。对于生产环境,我们将添加 Nginx 和 Gunicorn。我们还将了解如何通过 Nginx 提供 Django 静态和媒体文件。
建立项目
- 创建一个新的项目目录,然后开始 Django 项目:
$ mkdir django-on-docker && cd django-on-docker
$ mkdir app && cd app
$ python3.9 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install django==3.2.6
(env)$ django-admin.py startproject hello_django .
(env)$ python manage.py migrate
(env)$ python manage.py runserver
导航到 http://localhost:8000/
以查看 Django 欢迎屏幕。一旦完成就关闭服务器。然后,退出并删除虚拟环境。我们现在有一个简单的 Django 项目。
在“app”目录下创建 requirements.txt
文件,并添加 Django 作为依赖项:
Django==3.2.6
由于我们将迁移到 Postgres,因此请继续从“app”目录中删除 db.sqlite3
文件。
您的项目目录应该如下所示:
└── app
├── hello_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── requirements.txt
Docker
安装 Docker,如果你还没有,然后添加一个 Dockerfile 到“app”目录:
# 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 . .
因此,我们从 Python 3.9.6 的基于 Alpine 的 Docker 镜像开始。然后我们设置一个工作目录沿着两个环境变量:
PYTHONDONTWRITEBYTECODE
: 防止 Python 将 pyc 文件写入磁盘(相当于python -B
选项)PYTHONUNBUFFERED
: 防止 Python 将 stdout 和 stderr 缓存(相当于 python -u 选项)
最后,我们更新了 Pip,复制了 requirements.txt 文件,安装了依赖项,并复制了 Django 项目本身。
接下来,将 docker-compose.yml
文件添加到项目根目录:
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
更新 www.example.com
中的 settings.py
文件中的 SECRET_KEY
、 DEBUG
和 ALLOWED_HOSTS
变量 :
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
确保将 os
模块添加到文件:
import os
然后,在项目根目录中创建一个 .env.dev
文件,以存储开发环境变量:
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
构建 image:
$ docker-compose build
建立好 image 之后,运行容器:
$ docker-compose up -d
导航到 http://localhost:8000/
以再次查看 Django 默认欢迎屏幕。
安装 Postgres
要配置 Postgres,我们需要在 docker-compose.yml
文件中添加一个新服务,更新 Django 设置,并安装 Psycopg 2。
首先,在 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:
为了在容器的生命周期之外保持数据,我们配置了一个卷。此配置将 postgres_data
绑定到容器中的“/var/lib/postgresql/data/”目录中。
我们还添加了一个环境密钥来定义默认数据库的名称并设置用户名和密码。
我们还需要为 web 服务添加一些新的环境变量,所以更新 .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
更新 www.example.com
中的 dictsettings.py
的 DATABASES:
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 以安装 Psycopg 2 所需的相应软件包:
# 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 . .
将 Psycopg 2 添加到 requirements.txt
:
Django==3.2.6
psycopg2-binary==2.9.1
构建新映像并启动两个容器:
$ docker-compose up -d --build
运行 migrations :
$ 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 (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 django-on-docker_postgres_data
您应该看到类似于以下内容:
[
{
"CreatedAt": "2021-08-23T15:49:08Z",
"Driver": "local",
"Labels": {
"com.docker.compose.project": "django-on-docker",
"com.docker.compose.version": "1.29.2",
"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”目录中添加一个 www.example.com
文件,以在应用迁移和运行 Django 开发服务器之前验证 Postgres 是否健康:
#!/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.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"]
将 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
再测试一下:
- 重建 image
- 运行容器
- 打开
http://localhost:8000/
其他
首先,尽管添加了 Postgres,我们仍然可以为 Django 创建一个独立的 Docker 镜像,只要 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
上查看欢迎页面。
其次,您可能希望注释掉 www.example.com 脚本中的数据库刷新和迁移命令 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
安装 Gunicorn
对于生产环境,让我们将生产级 WSGI 服务器 Gunicorn 添加到需求文件中:
Django==3.2.6
gunicorn==20.1.0
psycopg2-binary==2.9.1
由于我们仍然希望在开发中使用 Django 的内置服务器,因此创建一个名为 docker-compose.prod.yml
的新 compose 文件用于生产:
version: '3.8'
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:13.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
将这两个文件添加到项目根目录。您可能希望将它们置于版本控制之外,因此将它们添加到 .gitignore
文件中。
关闭开发环境容器(以及带有 -v
标志的关联卷):
$ docker-compose down -v
然后,构建生产 image 并启动容器:
验证 hello_django_prod
数据库是否与默认 Django 表沿着创建。在 http://localhost:8000/admin
测试管理页面。不再加载静态文件。这是合理的,因为调试模式已关闭。
生产 Dockerfile
您是否注意到我们仍然在运行数据库刷新(清除数据库)和每次容器运行时迁移命令?这在开发中很好,但是让我们创建一个新的入口点文件以供生产使用。
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
要使用此文件,请创建一个名为 www.example.com 的新 DockerfileDockerfile.prod
,用于生产版本:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.9.6-alpine as builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install psycopg2 dependencies
RUN apk update \
&& apk add postgresql-dev gcc python3-dev musl-dev
# lint
RUN pip install --upgrade pip
RUN pip install flake8==3.9.2
COPY . .
RUN flake8 --ignore=E501,F401 .
# install dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.9.6-alpine
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup -S app && adduser -S app -G app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
RUN apk update && apk add libpq
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --no-cache /wheels/*
# copy entrypoint.prod.sh
COPY ./entrypoint.prod.sh .
RUN sed -i 's/\r$//g' $APP_HOME/entrypoint.prod.sh
RUN chmod +x $APP_HOME/entrypoint.prod.sh
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
# run entrypoint.prod.sh
ENTRYPOINT ["/home/app/web/entrypoint.prod.sh"]
在这里,我们使用 Docker 多阶段构建来减少最终 image 的大小。本质上, builder
是一个临时映像,用于构建 Python 轮子。然后将轮子复制到最终生成 image,并丢弃 builder
image。
您是否注意到我们创建了一个非 root 用户?默认情况下,Docker 在容器内以 root 身份运行容器进程。这是一个不好的做法,因为如果攻击者设法突破容器,他们可以获得对 Docker 主机的 root 访问权限。如果您是容器中的 root 用户,则您将是主机上的 root 用户。
更新 docker-compose.prod.yml
文件中的 web 服务,以使用 www.example.com 构建 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 的反向代理,处理客户的请求,并提供静态文件。
将服务添加到 docker-compose.prod.yml:
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- web
然后,在本地项目根目录中,创建以下文件和文件夹:
└── nginx
├── Dockerfile
└── nginx.conf
Dockerfile 文件如下:
FROM nginx:1.21-alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
nginx.conf 文件:
upstream 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;
}
}
然后,在 docker-compose.prod.yml
中更新 web
服务,将 ports
替换为 expose
:
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
现在,端口 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 = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
开发环境
现在,对 http://localhost:8000/static/*
的任何请求都将从“staticfiles”目录中提供。
要进行测试,首先重新构建映像,并按照常规启动新容器。确保静态文件仍在 http://localhost:8000/admin
上正确提供。
生产环境
对于生产,在 docker-compose.prod.yml 中为 web
和 nginx
服务添加一个卷,以便每个容器将共享一个名为“staticfiles”的目录:
version: '3.8'
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: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:
我们还需要在 www.example.com
中创建“/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 命令时会出现 permission denied 错误。
要解决此问题,您可以:
- 在 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 /static/ {
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/static/*
的请求将从“staticfiles”目录中提供。
导航到 http://localhost:1337/admin
并确保静态资产正确加载。
你还可以在日志中通过 docker-compose -f docker-compose.prod.yml logs -f
验证静态文件的请求是否通过 Nginx 成功提供:
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/ HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /admin/login/?next=/admin/ HTTP/1.1" 200 2214 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/base.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/nav_sidebar.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/responsive.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/login.css HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/js/nav_sidebar.js HTTP/1.1" 304 0 "http://localhost:1337/admin/login/?next=/admin/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/css/fonts.css HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/base.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Regular-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
nginx_1 | 192.168.144.1 - - [23/Aug/2021:20:11:00 +0000] "GET /static/admin/fonts/Roboto-Light-webfont.woff HTTP/1.1" 304 0 "http://localhost:1337/static/admin/css/fonts.css" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" "-"
完成后,停止所启动的命令:
$ docker-compose -f docker-compose.prod.yml down -v
媒体文件
要测试如何处理媒体文件,首先创建一个新的 Django 应用:
$ docker-compose up -d --build
$ docker-compose exec web python manage.py startapp upload
将新应用程序添加到 www.example.com 中的 settings.py
文件的 INSTALLED_APPS 列表 :
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")
在“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 %}
app/hello_django/urls.py
文件:
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from upload.views import image_upload
urlpatterns = [
path("", image_upload, name="upload"),
path("admin/", admin.site.urls),
]
if bool(settings.DEBUG):
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
app/hello_django/settings.py
文件:
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
开发环境
测试:
$ docker-compose up -d --build
您应该能够在 http://localhost:8000/
上传图像,然后在 http://localhost:8000/media/IMAGE_FILE_NAME
查看图像。
生产环境
对于生产,将另一个卷添加到 web
和 nginx
服务:
version: '3.8'
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: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:
在 www.example.com 中创建“/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 /static/ {
alias /home/app/web/staticfiles/;
}
location /media/ {
alias /home/app/web/mediafiles/;
}
}
重新 build:
$ 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/media/IMAGE_FILE_NAME
查看图像
如果你看到 413 Request Entity Too Large 错误,你需要增加 Nginx 配置中服务器或位置上下文中客户端请求体的最大允许大小。 例如:
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;
}
总结
在本教程中,我们介绍了如何使用 Postgres 容器化 Django Web 应用程序以进行开发。我们还创建了一个生产就绪的 Docker Composose 文件,它将 Gunicorn 和 Nginx 添加到混合中,以处理静态和媒体文件。您现在可以在本地测试生产设置。
在实际部署到生产环境方面,您可能需要使用:
- 完全托管的数据库服务--如 RDS 或 Cloud SQL --而不是在容器中管理自己的 Postgres 实例。
- 非 root 用户 的
db
和nginx
服务