Django 项目初学者教程:设置、Docker-Compose、Postgres 和 Redis

2025-06-07

Django 项目初学者教程:设置、Docker-Compose、Postgres 和 Redis

本文由Appliku.com推出,这是首个专为 Python 和 Django 打造的部署服务
无需再管理服务器,
5 分钟即可部署您的 Django 应用。

在本文中:

Django 教程源代码
为项目创建环境
Django 项目的要求
Django 项目的 .gitignoreDockerfile
docker
-compose.yml
关于 Django 和 Docker 的说明
Django 设置
Django 自定义用户模型
Django 项目的 ProcfileDjango 项目
的结构
将您的 Django 应用程序推送到 GitHub
部署 Django 项目
应用程序进程部分
配置变量部分
Heroku 配置变量同步
数据库

Django 教程源代码

您可以在此处找到项目源代码:https://github.com/appliku/django_appliku_tutorial

为项目创建环境

让我们在主目录中创建一个目录来保存不同项目的虚拟环境。

然后我们将在其中创建一个环境,激活它,安装 Django,然后创建我们的新项目。

mkdir -p ~/envs
python3 -m venv ~/envs/tutorial
source ~/envs/tutorial/bin/activate
pip install -U pip
pip install Django
Enter fullscreen mode Exit fullscreen mode

我倾向于将所有代码目录都保存在~/src一个目录中。如果你没有这个目录,我们可以创建一个,然后切换到这个目录。

mkdir -p ~/src
cd ~/src/
django-admin startproject tutorial
cd tutorial
Enter fullscreen mode Exit fullscreen mode

在这个阶段我通常会打开我最喜欢的IDE:PyCharm。

open -a pycharm .
Enter fullscreen mode Exit fullscreen mode

现在我们需要在项目的根目录中创建文件。

requirements.txt将保存我们项目所需的所有依赖项。

.gitignore将告诉 git 哪些文件不应该添加到存储库。

Django 项目要求

打开创建并打开requirements.txt并将这些行放入文件中:

Django==3.1.7
Pillow==7.2.0
gunicorn==20.0.4
requests==2.25.1
django-redis==4.12.1
pytz==2021.1
psycopg2-binary==2.8.6
arrow==1.0.3
djangorestframework==3.12.2
djangorestframework-simplejwt==4.6.0
django-allauth==0.44.0
django-environ==0.4.5
django-storages==1.11.1
django-cors-headers==3.7.0
django-braces==1.14.0
django-extensions==3.1.1
django-post-office==3.5.3
django-crispy-forms==1.11.1
boto3==1.17.22
boto3-stubs==1.17.22.0
django-import-export==2.5.0
honeybadger==0.4.2
django-ses==1.0.3
djangoql==0.14.3
flake8==3.8.4
whitenoise==5.2.0
Enter fullscreen mode Exit fullscreen mode

关于这些要求和版本的几句话。

这些是我几乎所有项目中都需要的包,因此我建议将它们包含在教程项目中。

您可能对旧版 Pillow 库有疑问。我遇到了一些问题,新版本在上传图片时与 Django 不兼容,我找到的唯一解决方案是降级到 7.2.0。

Django 项目的 .gitignore

这是最重要的.gitignore文件记录。

env/
venv/
.idea
.env

**/__pycache__/

.DS_Store
Enter fullscreen mode Exit fullscreen mode

我的操作系统是 Mac,所以.DS_StoreFinder 中的文件也是,我不想将其放入存储库中。

envvenv是项目目录内创建的环境的典型名称。我们现在没有这个名称,但是当你在另一台机器上克隆项目或其他开发人员加入项目时,他们会希望这些名称被忽略。

.idea是 PyCharm 创建的用于存储项目特定设置的目录。

.env是本地环境变量,我们永远不应该将其包含在存储库中。

__pycache__是文件的“编译”字节码版本.py。您的解释器可能会创建它。将它们放在存储库中是不好的,因为当您尝试在 Python 版本稍有不同的情况下运行应用程序时,它们会导致问题。

Dockerfile

我们将使用 Docker 运行我们的项目。

为什么?

因为您确实想要可重现的环境并避免弄乱主机。

为了做到这一点,让我们创建两个文件。

第一的,Dockerfile

FROM python:3.8
ENV PIP_NO_CACHE_DIR off
ENV PIP_DISABLE_PIP_VERSION_CHECK on
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 0
ENV COLUMNS 80
RUN apt-get update \
 && apt-get install -y --force-yes \
 nano python-pip gettext chrpath libssl-dev libxft-dev \
 libfreetype6 libfreetype6-dev  libfontconfig1 libfontconfig1-dev\
  && rm -rf /var/lib/apt/lists/*
WORKDIR /code/
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

第二,docker-compose.yml

version: '3.3'
services:
  redis:
    image: redis
    command: redis-server
    ports:
      - "14000:6379"
  db:
    image: postgres
    environment:
      - POSTGRES_USER=tutorial
      - POSTGRES_PASSWORD=tutorial
      - POSTGRES_DB=tutorial
    ports:
      - "127.0.0.1:21003:5432"
  web:
    build: .
    restart: always
    command: python manage.py runserver 0.0.0.0:8600
    env_file:
      - .env
    ports:
      - "127.0.0.1:8600:8600"
    volumes:
      - .:/code
    links:
      - db
      - redis
    depends_on:
      - db
      - redis
Enter fullscreen mode Exit fullscreen mode

关于 Django 和 Docker 的说明

Dockerfile定义应用程序将要执行的操作系统和文件系统的状态。这个解释过于简单,而且完全不枯燥 :)

根据Dockerfiledocker 的指令构建图像

如果Dockerfile是一组指令,那么图像就是包含可用于在容器中执行应用程序的文件的实际存档。

对于开发人员环境,我们需要 Django 开发服务器来运行某个命令,并且我们需要一个 postgres DB 和一个 Redis 实例。

为了用代码定义它,我们将使用docker-compose.yml文件。

docker-compose.yml定义它将运行的服务。

服务主要由其使用的图像、要执行的命令、要公开端口要挂载的卷和要设置的环境变量docker-compose.yml来定义

再次,这是一个用我自己的话进行的非常高级的解释,并尝试用尽可能简单的语言进行解释。

让我们讨论一下我们在中定义的内容docker-compose.yml

我们有一个 Postgres 服务。首次初始化时,它会有一个用户、密码和一个数据库“tutorial”。

它将可供db端口 5432 上的 DNS 名称上的其他服务使用。并且它将可供端口 21003 上的主机使用。

它使用从 Docker Hub 中提取的“postgres”图像。

没有定义数据库的卷,因此如果您杀死数据库,那么您将丢失数据。

下一个服务是针对我们的 Django 开发服务器。

我们指定从中构建图像的文件夹而不是图像Dockerfile,在本例中是当前目录(.)。

如果失败,我们希望它始终能够重新启动。

我们指定使用什么命令来运行我们的开发服务器。

.env环境变量将从我们稍后创建的文件中获取。

我们在 127.0.0.1 上公开了 8600 端口,因此只能从本地计算机访问。请记住,如果您要更改端口,也应该在 中更新端口command

volumes部分说明了哪些目录将从主机挂载到容器内。我们希望将当前目录挂载到/code应用程序运行的位置。请参阅WORKDIR我们的Dockerfile
由于这是开发环境,我们希望代码的更改能够反映在容器中,以便开发服务器能够自动重新加载。

links部分将使容器内部的 DNS 名称解析db成为可能。换句话说,Django 开发服务器将能够连接到db。 也同样如此redis

depends_on部分列出了在启动服务之前必须启动的服务web。在这种情况下,将首先启动redis和,然后启动 。dbweb

最后一步,让我们.env在项目的根目录中创建文件。

DATABASE_URL=postgresql://tutorial:tutorial@db/tutorial
REDIS_URL=redis://redis/0
DJANGO_SECRET_KEY=supersecret123!
DJANGO_DEBUG=True
Enter fullscreen mode Exit fullscreen mode

这里我们以特殊 URL 的形式将数据库和 redis 实例的凭证传递给我们的 django 项目,secret_key Django 应该使用并启用 Django 的调试模式。

目前这些还不会影响我们的应用。但过一段时间就会变得非常重要。

我们需要测试我们的Docker镜像是否可以构建并且docker-compose没有错误。

现在,我们只需告诉它构建我们的图像。

运行此命令:

docker-compose build
Enter fullscreen mode Exit fullscreen mode

如果成功,输出的最后几行应该大致如下:

Removing intermediate container 757d0bd934ca
 ---> b4bba357f84c
Step 11/11 : COPY . /code/
 ---> fa5d799d8fc1

Successfully built fa5d799d8fc1
Successfully tagged tutorial_web:latest
Enter fullscreen mode Exit fullscreen mode

做得好!

是时候进行设置了。

Django 设置

我们希望我们的应用程序具有可扩展性,能够在大量流量下运行并处理增长。

为了做到这一点,我们需要构建我们的应用程序,以便它能够扩展。

在这种情况下,我们的应用程序遵循 12 因素应用程序的规则非常重要:https://12factor.net

让我们在这里列出要点:

  1. 代码库——在修订控制中跟踪一个代码库,进行多次部署
  2. 依赖关系——明确声明并隔离依赖关系
  3. 配置——将配置存储在环境中
  4. 支持服务——将支持服务视为附加资源
  5. 构建、发布、运行——严格分离构建和运行阶段
  6. 进程 – 将应用程序作为一个或多个无状态进程执行
  7. 端口绑定 – 通过端口绑定导出服务
  8. 并发——通过进程模型进行扩展
  9. 可处置性——通过快速启动和正常关闭最大限度地提高稳健性
  10. 开发/生产一致性——尽可能保持开发、预发布和生产环境的相似性
  11. 日志——将日志视为事件流
  12. 管理流程 – 将管理/管理任务作为一次性流程运行

我强烈建议阅读该网站的所有页面。

考虑到这一点,让我们打开我们的设置文件:tutorial/settings.py

我们从那里移除一切并从头开始构建我们自己的。

我将解释每个代码块,然后展示整个文件,以便您可以将其复制到您的项目中。

首先,我们导入几个库,设置项目的根路径并创建一个环境变量。

from pathlib import Path
import environ
import os

BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
env = environ.Env()

Enter fullscreen mode Exit fullscreen mode

BASE_DIR我们需要为项目中的各个位置设置适当的路径。

env将帮助我们正确地从环境变量中获取配置。

DEBUG = env.bool("DJANGO_DEBUG", False)
Enter fullscreen mode Exit fullscreen mode

DEBUG除本地开发环境外,应始终关闭。

# Allowed Hosts Definition
if DEBUG:
    # If Debug is True, allow all.
    ALLOWED_HOSTS = ['*']
else:
    ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['example.com'])

Enter fullscreen mode Exit fullscreen mode

如果DEBUG为 True,那么我们在打开应用程序时应该允许所有 HOSTS。

SECRET_KEY = env('DJANGO_SECRET_KEY')
Enter fullscreen mode Exit fullscreen mode

密钥用于加密/签名 cookie、密码等。您必须妥善保管它,并且不要让其受到版本控制。

"""
Project Apps Definitions
Django Apps - Django Internal Apps
Third Party Apps - Apps installed via requirements.txt
Project Apps - Project owned / created apps

Installed Apps = Django Apps + Third Part apps + Projects Apps
"""
DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.sites',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.redirects',
]

THIRD_PARTY_APPS = [
    'django_extensions',
    'rest_framework',
    'storages',
    'corsheaders',
    'djangoql',
    'post_office',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'crispy_forms',
]

PROJECT_APPS = [
    'usermodel',
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS
Enter fullscreen mode Exit fullscreen mode

区分某个应用程序来自哪里非常方便,这就是我们将内置 Django 应用程序、第三方应用程序和项目应用程序分开的原因。

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
Enter fullscreen mode Exit fullscreen mode
# Databases
DATABASES = {
    "default": env.db("DATABASE_URL")
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
Enter fullscreen mode Exit fullscreen mode

我们的应用程序将从环境变量中接收类似 URL 形式的 DATABASE_URL postgres://username:password@database-host.com:1234/databasename


ROOT_URLCONF = 'tutorial.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'tutorial.wsgi.application'

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]
Enter fullscreen mode Exit fullscreen mode
# User Model Definition
AUTH_USER_MODEL = 'usermodel.User'

Enter fullscreen mode Exit fullscreen mode

对于每个新的 Django 项目,不要忘记创建一个自定义用户模型,否则你以后将无法更改它。我们稍后会讨论这个问题。

TIME_ZONE = 'UTC'
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
USE_I18N = True
USE_L10N = True
USE_TZ = True
Enter fullscreen mode Exit fullscreen mode

这些设置非常静态,但如果您想了解更多信息,我建议您阅读官方文档:https://docs.djangoproject.com/en/3.1/ref/settings/

# Admin URL Definition
ADMIN_URL = env('DJANGO_ADMIN_URL', default='admin/')
Enter fullscreen mode Exit fullscreen mode

管理员,切勿在默认 URL 上设置管理员。使用DJANGO_ADMIN_URL环境变量,您可以针对每个环境设置不同的环境:生产环境、暂存环境,但本地开发环境则保留默认设置。


# Redis Settings
REDIS_URL = env('REDIS_URL', default=None)

if REDIS_URL:
    CACHES = {
        "default": env.cache('REDIS_URL')
    }
Enter fullscreen mode Exit fullscreen mode

REDIS主要用于缓存端临时数据存储。

在这种情况下,我们将其设为可选,如果定义了 REDIS_URL,则我们启用默认缓存。

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Enter fullscreen mode Exit fullscreen mode

这就是我们告诉 Django 检测到应用程序在 SSL 代理后面运行的方式。

在我们的 nginx 服务器定义中我们必须设置X-Forwarded-Proto标头。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler'
        },
    },
    'loggers': {
        '': {  # 'catch all' loggers by referencing it with the empty string
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}
Enter fullscreen mode Exit fullscreen mode

这是一个非常简单的日志记录,它应该将所有级别的模块的所有内容输出到控制台DEBUG,这意味着输出所有可以输出的内容。

# Static And Media Settings
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default=None)
if AWS_STORAGE_BUCKET_NAME:
    AWS_DEFAULT_ACL = None
    AWS_QUERYSTRING_AUTH = False
    AWS_S3_CUSTOM_DOMAIN = env('AWS_S3_CUSTOM_DOMAIN', default=None) or f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
    AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=600'}

    # s3 static settings
    STATIC_LOCATION = 'static'
    STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/'
    STATICFILES_STORAGE = 'tutorial.storages.StaticStorage'

    # s3 public media settings
    PUBLIC_MEDIA_LOCATION = 'media'
    MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_MEDIA_LOCATION}/'
    DEFAULT_FILE_STORAGE = 'tutorial.storages.PublicMediaStorage'
else:
    MIDDLEWARE.insert(2, 'whitenoise.middleware.WhiteNoiseMiddleware')
    STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
    WHITENOISE_USE_FINDERS = True
    STATIC_HOST = env('DJANGO_STATIC_HOST', default='')
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    STATIC_URL = STATIC_HOST + '/static/'
    if DEBUG:
        WHITENOISE_AUTOREFRESH = True
Enter fullscreen mode Exit fullscreen mode

这是用于处理静态文件和媒体文件的配置。它会将静态文件上传python manage.py collectstatic --noinput到 S3,而所有媒体文件(由用户或应用程序自行上传)都将存储在 S3 中。

如果环境变量AWS_STORAGE_BUCKET_NAME不存在,则这部分配置将不会启用,这对于本地开发来说应该是这种情况。

如果AWS_STORAGE_BUCKET_NAME未设置,Django 将使用whitenoise服务器静态文件。

DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='test@example.com')
Enter fullscreen mode Exit fullscreen mode

我们的应用可能在某些时候会发送电子邮件,至少是密码重置邮件。我们应该指定默认发件人。同样,作为环境变量。

让我们来了解第三方应用程序配置。

首先,我们需要了解生产中可能发生的任何错误。

我们将设置我们的应用程序向错误跟踪服务https://HoneyBadger.io报告

再次强调,只有设置了环境变量后,此功能才会启用HONEYBARDGER_API_KEY。您可以从 HoneyBadger 的项目设置中获取此环境变量。

现在让我们配置 Celery 来运行我们的后台任务。

它是可选的。如果celery未安装,或者CELERY_BROKER_URL未定义环境变量,则不会启用。

# Celery Settings
try:
    from kombu import Queue
    from celery import Celery
    CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='amqp://localhost')
    if CELERY_BROKER_URL:
        CELERYD_TASK_SOFT_TIME_LIMIT = 60
        CELERY_ACCEPT_CONTENT = ['application/json']
        CELERY_TASK_SERIALIZER = 'json'
        CELERY_RESULT_SERIALIZER = 'json'
        CELERY_RESULT_BACKEND = env('REDIS_URL', default='redis://localhost:6379/0')
        CELERY_DEFAULT_QUEUE = 'default'
        CELERY_QUEUES = (
            Queue('default'),
        )
        CELERY_CREATE_MISSING_QUEUES = True
except ModuleNotFoundError:
    print("Celery/kombu not installed. Skipping...")
Enter fullscreen mode Exit fullscreen mode

现在让我们配置 Django-AllAuth,以便能够通过社交账户注册/登录。本教程中,我们将 Google 作为唯一的服务提供商。


# AllAuth Settings
AUTHENTICATION_BACKENDS += [
    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
]

ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_FORMS = {'signup': 'usermodel.forms.MyCustomSignupForm'}
ACCOUNT_MAX_EMAIL_ADDRESSES = 2
SOCIALACCOUNT_PROVIDERS = {

}
SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID = env('SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID', default=None)
if SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID:
    SOCIALACCOUNT_PROVIDERS['google'] = {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        },
        # For each OAuth based provider, either add a ``SocialApp``
        # (``socialaccount`` app) containing the required client
        # credentials, or list them here:
        'APP': {
            'client_id': SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID,
            'secret': env('SOCIALACCOUNT_PROVIDERS_GOOGLE_SECRET'),
        }
    }
Enter fullscreen mode Exit fullscreen mode

为了使其正常工作,我们需要从 Google Cloud 服务获取 CLIENT_ID 和 CLIENT_SECRET。

和以前一样,我更喜欢将其设置为可选。因此,如果SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID存在环境变量,我们就将 Google 添加到提供商列表中。

# Crispy Forms Settings
CRISPY_TEMPLATE_PACK = 'bootstrap4'
Enter fullscreen mode Exit fullscreen mode

crispy-forms包帮助我们的表单看起来更美观。在本例中,通过bootstrap4设置,它将根据 Bootstrap 4 HTML 结构呈现我们的表单。

让我们结束电子邮件发送部分。

我喜欢两个有助于发送和管理外发电子邮件的库。

其中之一是django-post-office向用户提供电子邮件模板、日程安排、优先级并存储发出的电子邮件,其中包含日志和状态,以便您可以方便地在应用程序端调试电子邮件是否发出、具体发送了什么以及如果没有发出 - 每封电子邮件都会附有日志。

第二个库是django-ses,用于通过 AWS 简单电子邮件服务 (SES) 发送电子邮件。

# Django Post Office Settings
EMAIL_BACKEND = 'post_office.EmailBackend'

POST_OFFICE = {
    'BACKENDS': {
        'default': 'django_ses.SESBackend',
    },
    'DEFAULT_PRIORITY': 'now',
}

# AWS SES Settings
AWS_SES_REGION_NAME = env('AWS_SES_REGION_NAME', default='us-east-1')
AWS_SES_REGION_ENDPOINT = env('AWS_SES_REGION_ENDPOINT', default='email.us-east-1.amazonaws.com')
AWS_SES_CONFIGURATION_SET = env('AWS_SES_CONFIGURATION_SET', default=None)
Enter fullscreen mode Exit fullscreen mode

这值得解释一下。

当您想通过 SES 发送电子邮件时,您需要向特定区域的 AWS 请求发送容量。在此之前,您在该区域的帐户处于电子邮件沙盒中,只能向您自己(即已验证的电子邮件地址)发送电子邮件。

当 AWS 允许您通过该地区发送电子邮件时,您必须通过上述设置让应用程序知道这一点。

AWS_SES_CONFIGURATION_SET如果您已配置 AWS CloudWatch 来跟踪打开、点击等操作,则需要此设置。如果尚未配置,请留空。

我们的工作到此结束tutorial/settings.py,以下是供您复制的完整文件:

from pathlib import Path
import environ
import os

env = environ.Env()

"""
Project Settings
"""

BASE_DIR = Path(__file__).resolve(strict=True).parent.parent

DEBUG = env.bool('DJANGO_DEBUG', default=False)

# Allowed Hosts Definition
if DEBUG:
    # If Debug is True, allow all.
    ALLOWED_HOSTS = ['*']
else:
    ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['example.com'])

SECRET_KEY = env('DJANGO_SECRET_KEY')

"""
Project Apps Definitions
Django Apps - Django Internal Apps
Third Party Apps - Apps installed via requirements.txt
Project Apps - Project owned / created apps

Installed Apps = Django Apps + Third Part apps + Projects Apps
"""
DJANGO_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.sites',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.redirects',
]

THIRD_PARTY_APPS = [
    'import_export',
    'django_extensions',
    'rest_framework',
    'storages',
    'corsheaders',
    'djangoql',
    'post_office',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'crispy_forms',
]

PROJECT_APPS = [
    'usermodel',
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + PROJECT_APPS

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.redirects.middleware.RedirectFallbackMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# Databases
DATABASES = {
    "default": env.db("DATABASE_URL")
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)

ROOT_URLCONF = 'tutorial.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'tutorial.wsgi.application'

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]
# User Model Definition
AUTH_USER_MODEL = 'usermodel.User'

TIME_ZONE = 'UTC'
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
USE_I18N = True
USE_L10N = True
USE_TZ = True

# Admin URL Definition
ADMIN_URL = env('DJANGO_ADMIN_URL', default='admin/')

# Redis Settings
REDIS_URL = env('REDIS_URL', default=None)

if REDIS_URL:
    CACHES = {
        "default": env.cache('REDIS_URL')
    }
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler'
        },
    },
    'loggers': {
        '': {  # 'catch all' loggers by referencing it with the empty string
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}


# Static And Media Settings
AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME', default=None)
if AWS_STORAGE_BUCKET_NAME:
    AWS_DEFAULT_ACL = None
    AWS_QUERYSTRING_AUTH = False
    AWS_S3_CUSTOM_DOMAIN = env('AWS_S3_CUSTOM_DOMAIN', default=None) or f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
    AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=600'}

    # s3 static settings
    STATIC_LOCATION = 'static'
    STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/'
    STATICFILES_STORAGE = 'tutorial.storages.StaticStorage'

    # s3 public media settings
    PUBLIC_MEDIA_LOCATION = 'media'
    MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_MEDIA_LOCATION}/'
    DEFAULT_FILE_STORAGE = 'tutorial.storages.PublicMediaStorage'

    STATICFILES_DIRS = (
        # os.path.join(BASE_DIR, "static"),
    )
else:
    MIDDLEWARE.insert(2, 'whitenoise.middleware.WhiteNoiseMiddleware')
    STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
    WHITENOISE_USE_FINDERS = True
    STATIC_HOST = env('DJANGO_STATIC_HOST', default='')
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    STATIC_URL = STATIC_HOST + '/static/'
    if DEBUG:
        WHITENOISE_AUTOREFRESH = True
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='test@example.com')

"""
Third Party Settings
"""

# Honeybadger Settings
HONEYBADGER_API_KEY = env('HONEYBADGER_API_KEY', default=None)
if HONEYBADGER_API_KEY:
    MIDDLEWARE = ['honeybadger.contrib.DjangoHoneybadgerMiddleware'] + MIDDLEWARE
    HONEYBADGER = {
        'API_KEY': HONEYBADGER_API_KEY
    }

# Celery Settings
try:
    from kombu import Queue
    from celery import Celery
    CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='amqp://localhost')
    if CELERY_BROKER_URL:
        CELERYD_TASK_SOFT_TIME_LIMIT = 60
        CELERY_ACCEPT_CONTENT = ['application/json']
        CELERY_TASK_SERIALIZER = 'json'
        CELERY_RESULT_SERIALIZER = 'json'
        CELERY_RESULT_BACKEND = env('REDIS_URL', default='redis://localhost:6379/0')
        CELERY_DEFAULT_QUEUE = 'default'
        CELERY_QUEUES = (
            Queue('default'),
        )
        CELERY_CREATE_MISSING_QUEUES = True
except ModuleNotFoundError:
    print("Celery/kombu not installed. Skipping...")

# AllAuth Settings
AUTHENTICATION_BACKENDS += [
    # `allauth` specific authentication methods, such as login by e-mail
    'allauth.account.auth_backends.AuthenticationBackend',
]

ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_FORMS = {'signup': 'usermodel.forms.MyCustomSignupForm'}
ACCOUNT_MAX_EMAIL_ADDRESSES = 2
SOCIALACCOUNT_PROVIDERS = {

}
SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID = env('SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID', default=None)
if SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID:
    SOCIALACCOUNT_PROVIDERS['google'] = {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        },
        # For each OAuth based provider, either add a ``SocialApp``
        # (``socialaccount`` app) containing the required client
        # credentials, or list them here:
        'APP': {
            'client_id': SOCIALACCOUNT_PROVIDERS_GOOGLE_CLIENT_ID,
            'secret': env('SOCIALACCOUNT_PROVIDERS_GOOGLE_SECRET'),
        }
    }
# Crispy Forms Settings
CRISPY_TEMPLATE_PACK = 'bootstrap4'

# Django Post Office Settings
EMAIL_BACKEND = 'post_office.EmailBackend'

POST_OFFICE = {
    'BACKENDS': {
        'default': 'django_ses.SESBackend',
    },
    'DEFAULT_PRIORITY': 'now',
}

# AWS SES Settings

AWS_SES_REGION_NAME = env('AWS_SES_REGION_NAME', default='us-east-1')
AWS_SES_REGION_ENDPOINT = env('AWS_SES_REGION_ENDPOINT', default='email.us-east-1.amazonaws.com')
AWS_SES_CONFIGURATION_SET = env('AWS_SES_CONFIGURATION_SET', default=None)

Enter fullscreen mode Exit fullscreen mode

在下一步中,创建一个文件来settings.py调用它storages.py

from storages.backends.s3boto3 import S3Boto3Storage


class PublicMediaStorage(S3Boto3Storage):
    location = 'media'
    default_acl = 'public-read'
    file_overwrite = False


class StaticStorage(S3Boto3Storage):
    location = 'static'
    default_acl = 'public-read'

Enter fullscreen mode Exit fullscreen mode

这是我们的媒体和静态存储工作所必需的,因为我们从中引用了这些类settings.py

Django 自定义用户模型

每个使用用户和身份验证的 Django 项目都应该定义一个自定义用户模型。即使你保留它与原有的相同,它也能让你以后能够修改它,以更好地满足项目需求。

在项目中期更改用户模型是一项非常棘手的任务,因为您将有其他模型实例引用现有用户,并且您必须迁移所有这些。

话虽如此,在项目的根目录中创建一个文件夹usermodel和一个空文件__init__.py

您可以通过运行来实现类似的目的python manage.py startapp usermodel,但我只是想有机会讨论一下是什么让目录成为 Python 模块。

为了能够从中导入模块或任何内容,目录__init__.py中需要有一个内容。

现在创建usermodel/models.py

我们应该将其放入文件中:

import uuid

from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.contrib.postgres.fields import CIEmailField
from django.core.mail import send_mail
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import timezone

from usermodel.managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    """
    An abstract base class implementing a fully featured User model with
    admin-compliant permissions.
    Username and password are required. Other fields are optional.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = CIEmailField(
        _('Email Address'),
        unique=True,
        error_messages={
            'unique': _("A user with that username already exists."),
        },
    )

    first_name = models.CharField(_('First Name'), max_length=255, blank=True)
    last_name = models.CharField(_('Last Name'), max_length=255, blank=True)

    is_staff = models.BooleanField(
        _('Staff Status'),
        default=False,
        help_text=_('Designates whether the user can log into this admin site.'),
    )

    is_active = models.BooleanField(
        _('Active'),
        default=True,
        help_text=_(
            'Designates whether this user should be treated as active. '
            'Unselect this instead of deleting accounts.'
        ),
    )

    # Audit Values
    is_email_confirmed = models.BooleanField(
        _('Email Confirmed'),
        default=False
    )
    date_joined = models.DateTimeField(
        _('Date Joined'),
        default=timezone.now
    )

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = [
        'first_name',
        'last_name'
    ]

    class Meta:
        verbose_name = _('User')
        verbose_name_plural = _('Users')

    def clean(self):
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self):
        """
        Return the first_name plus the last_name, with a space in between.
        """
        return f"{self.first_name} {self.last_name}"

    def get_short_name(self):
        """Return the short name for the user."""
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """Send an email to this user."""
        send_mail(subject, message, from_email, [self.email], **kwargs)
Enter fullscreen mode Exit fullscreen mode

我们使用电子邮件作为登录名,使用 UUID 作为主键。

UUID 与整数主键的一般优点:

  • 没有人可以通过查看下达另一个订单或创建任何其他类型的对象时获得的最新 ID 来估计用户/订单/付款的数量。
  • 你无法通过暴力破解的方式访问其他对象,所以这增加了一层“安全性”。即使某些对象的链接是公开的,但仅供拥有链接的人访问——如果没有确切的ID,访问起来也会很困难。
  • 现在又出现了一个问题,这倒也挺好:最大的整数 2,147,483,647 似乎太大了。但是一旦达到这个数字,就无法再添加任何其他对象了。整数是主键的默认类型。当数字用完时,数据库将拒绝任何新记录。你可能会想,好吧,我只需将字段类型更改为 a 即可bigint。但是想象一下,将迁移应用于如此庞大的 20 亿条记录的表需要多少时间?你甚至可以趁着这段时间去度假,毕竟在构建如此庞大的项目时,你可能需要这段时间。😂 UUID 不会出现这样的问题。

现在,在我们的模型中我们引用了一个自定义管理器。

objects = UserManager()
Enter fullscreen mode Exit fullscreen mode

让我们创建一个文件 usermodel/managers.py 并将以下代码放入其中:

from django.contrib.auth.base_user import BaseUserManager


class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not email:
            raise ValueError('The given username must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create_user(email, password, **extra_fields)
Enter fullscreen mode Exit fullscreen mode

usermodel/admin.py

from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from usermodel.models import User
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin


@admin.register(User)
class UserAdmin(DefaultUserAdmin):
    fieldsets = (
        (
            None,
            {
                'fields': (
                    'email', 'password'
                )
            }
        ),
        (
            _('Permissions'),
            {
                'fields': (
                    'is_active',
                    'is_staff',
                    'is_superuser',
                    'groups',
                    'user_permissions',
                ),
            }
        ),
        (
            _('Important dates'),
            {
                'fields': (
                    'last_login',
                    'date_joined',
                )
            }
        ),
        (
            _('User data'),
            {
                'fields': (
                    ('is_email_confirmed',),
                )
            }
        ),
    )
    add_fieldsets = (
        (
            None,
            {
                'classes': ('wide',),
                'fields': ('email', 'password1', 'password2'),
            }
        ),
    )
    list_display = ('email', 'first_name', 'last_name', 'is_staff')
    search_fields = ('first_name', 'last_name', 'email')
    ordering = ('email',)
Enter fullscreen mode Exit fullscreen mode

最棒的是,可以使用非交互式管理命令来创建超级用户。

虽然我们有办法创建超级用户,但对于发布阶段等非交互式情况来说这并不方便。

我在所有项目中都选择使用此脚本,它会检查数据库中是否有超级用户,如果没有,则使用随机密码创建一个超级用户。

在目录中创建以下目录和文件usermodel

├── management
│   ├── __init__.py
│   └── commands
│       ├── __init__.py
│       └── makesuperuser.py
Enter fullscreen mode Exit fullscreen mode

__init__.py应该是空的,这里是代码usermodel/management/makesuperuser.py

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.utils.crypto import get_random_string

User = get_user_model()


class Command(BaseCommand):
    def handle(self, *args, **options):
        try:
            u = None
            if not User.objects.filter(email='admin@example.com').exists() and not User.objects.filter(
                    is_superuser=True).exists():
                print("admin user not found, creating one")
                email = 'admin@example.com'
                new_password = get_random_string()

                u = User.objects.create_superuser(email, new_password)
                print(f"===================================")
                print(f"A superuser was created with email {email} and password {new_password}")
                print(f"===================================")
            else:
                print("admin user found. Skipping super user creation")
            print(u)
        except Exception as e:
            print(f"There was an error: {e}")
Enter fullscreen mode Exit fullscreen mode

现在让我们为自定义用户模型创建迁移。

运行这个:

docker-compose run web python manage.py makemigrations usermodel
Enter fullscreen mode Exit fullscreen mode

命令的输出应该是这样的:

Migrations for 'usermodel':
  usermodel/migrations/0001_initial.py
Enter fullscreen mode Exit fullscreen mode

使用初始迁移打开此文件。

在文件顶部添加导入命令:

from django.contrib.postgres.operations import CITextExtension
Enter fullscreen mode Exit fullscreen mode

并将其添加CITextExtension(),为列表的第一个元素operations

该文件应如下所示:

# Generated by Django 3.1.7 on 2021-03-08 13:10

import django.contrib.postgres.fields.citext
from django.contrib.postgres.operations import CITextExtension
from django.db import migrations, models
import django.utils.timezone
import usermodel.managers
import uuid


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('auth', '0012_alter_user_first_name_max_length'),
    ]

    operations = [
        CITextExtension(),
        migrations.CreateModel(
            name='User',
            fields=[
                ('password', models.CharField(max_length=128, verbose_name='password')),
                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
                ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
                ('email', django.contrib.postgres.fields.citext.CIEmailField(error_messages={'unique': 'A user with that username already exists.'}, max_length=254, unique=True, verbose_name='Email Address')),
                ('first_name', models.CharField(blank=True, max_length=255, verbose_name='First Name')),
                ('last_name', models.CharField(blank=True, max_length=255, verbose_name='Last Name')),
                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='Staff Status')),
                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='Active')),
                ('is_email_confirmed', models.BooleanField(default=False, verbose_name='Email Confirmed')),
                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date Joined')),
                ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
                ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
            ],
            options={
                'verbose_name': 'User',
                'verbose_name_plural': 'Users',
            },
            managers=[
                ('objects', usermodel.managers.UserManager()),
            ],
        ),
    ]
Enter fullscreen mode Exit fullscreen mode

现在您可以应用迁移:

docker-compose run web python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

输出应如下所示:

Using selector: EpollSelector
Operations to perform:
  Apply all migrations: account, admin, auth, contenttypes, post_office, redirects, sessions, sites, socialaccount, usermodel
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying usermodel.0001_initial... OK
  Applying account.0001_initial... OK
  Applying account.0002_email_max_length... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying post_office.0001_initial... OK
  Applying post_office.0002_add_i18n_and_backend_alias... OK
  Applying post_office.0003_longer_subject... OK
  Applying post_office.0004_auto_20160607_0901... OK
  Applying post_office.0005_auto_20170515_0013... OK
  Applying post_office.0006_attachment_mimetype... OK
  Applying post_office.0007_auto_20170731_1342... OK
  Applying post_office.0008_attachment_headers... OK
  Applying post_office.0009_requeued_mode... OK
  Applying post_office.0010_message_id... OK
  Applying post_office.0011_models_help_text... OK
  Applying sites.0001_initial... OK
  Applying redirects.0001_initial... OK
  Applying sessions.0001_initial... OK
  Applying sites.0002_alter_domain_unique... OK
  Applying socialaccount.0001_initial... OK
  Applying socialaccount.0002_token_max_lengths... OK
  Applying socialaccount.0003_extra_data_default_dict... OK
src/tutorial % 
Enter fullscreen mode Exit fullscreen mode

现在运行makesuperuser管理命令。

docker-compose run web python manage.py makesuperuser
Enter fullscreen mode Exit fullscreen mode

输出将包含用户密码,将其复制到某处,您将需要它来登录管理面板。

admin user not found, creating one
===================================
A superuser was created with email admin@example.com and password rWKwHw5FK6tw
===================================
admin@example.com
src/tutorial % 
Enter fullscreen mode Exit fullscreen mode

尝试再次运行此命令。

admin user found. Skipping super user creation
None
src/tutorial % 
Enter fullscreen mode Exit fullscreen mode

看,第二次没有创建用户。

恭喜,我们完成了自定义用户模型!

Django 项目的 Procfile

Procfile 是告诉 Appliku Deploy 如何运行您的应用程序的文件。

该文件必须位于项目的根目录中。

记录类型有 3 种:

  • web
  • release
  • 其他

web该进程将是处理 HTTP 请求的进程。

release将保存每次发布时执行的命令,如应用迁移和其他活动。

所有其他进程没有特殊含义。例如,你可以放置 Celery Worker、Scheduler 或其他任何特定于你应用的进程。

Procfile 只能包含一个web进程和一个release进程。您可以根据需要创建任意数量的其他类型的进程。

对于我们的教程来说,这将是Procfile

web: gunicorn tutorial.wsgi --log-file -
release: bash release.sh
Enter fullscreen mode Exit fullscreen mode

在项目根目录中创建一个文件release.sh

#!/bin/bash
python manage.py migrate --noinput
python manage.py makesuperuser
Enter fullscreen mode Exit fullscreen mode

release.sh每次发布新版本时都会执行,它会应用迁移并尝试在我们的应用中创建超级用户。你还记得,只有在第一次发布时才会创建超级用户。

Django项目结构

在开始部署之前,让我们先看一下我们的项目。

src/tutorial % tree
.
├── Dockerfile
├── Procfile
├── docker-compose.yml
├── manage.py
├── release.sh
├── requirements.txt
├── tutorial
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── storages.py
│   ├── urls.py
│   └── wsgi.py
└── usermodel
    ├── __init__.py
    ├── admin.py
    ├── management
    │   ├── __init__.py
    │   └── commands
    │       ├── __init__.py
    │       └── makesuperuser.py
    ├── managers.py
    ├── migrations
    │   ├── 0001_initial.py
    │   ├── __init__.py
    └── models.py

Enter fullscreen mode Exit fullscreen mode

将您的 Django 应用程序推送到 GitHub

为了部署您的应用程序,您需要将其推送到 GitHub 存储库。

本教程假设您已拥有 GitHub 帐户。如果没有,请访问https://github.com并注册。

然后创建一个存储库,我们称之为django_appliku_tutorial

出于隐私目的,您可以将存储库设为私有,也可以将其设为公开,以便您的同事或未来的雇主可以看到您在做什么:)

请记住:切勿在代码中存储任何凭证。另请记住:从代码中删除的任何内容都会保留在存储库历史记录中。

点击创建存储库按钮。

现在你看到的是空的仓库页面。让我们参考“…或在命令行上创建新仓库”一节中的说明。

这是他们给我的例子。

您必须更改存储库的路径。

此外,由于我们有文件,所以我们想添加所有文件git add .

转到终端到项目的根目录并写入以下命令:

echo "# django_appliku_tutorial" > README.md
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:appliku/django_appliku_tutorial.git
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

推送代码后,存储库的 GitHub 页面应如下所示:

部署 Django 项目

我们将使用 Appliku Deploy,而不是进行老式的手动部署、编写大量配置并希望它能起作用。

Appliku Deploy 的功能:

  • 在 Digital Ocean(或 AWS)中配置服务器
  • 从 GitHub 仓库获取代码,
  • 在您的服务器上构建您的应用程序
  • 在您的服务器上部署您的应用程序
  • 设置 Web 服务器(nginx)并从 Let's Encrypt 颁发 SSL 证书。

为了部署您的应用程序,您应该在 GitHub、Digital Ocean 和 Appliku Deploy 中拥有帐户。

如果您没有,请按照以下链接创建它们:

如果您刚刚在 Appliku Deploy 上注册并登录,请确保完成入职培训,将帐户连接到 GitHub 和 Digital Ocean。

第一步是创建服务器。

此操作通过 Appliku Deploy 界面完成。请注意,您无法重复使用通过 Digital Ocean 界面手动创建的现有服务器。必须通过 Appliku Deploy 进行配置。

转到服务器选项卡:https://app.appliku.com/servers

点击“创建新服务器”按钮,您将进入提供商选择页面(https://app.appliku.com/providers

选择 DigitalOcean。

您将被带到一个页面,您可以在其中选择要配置的 Droplet 类型(https://app.appliku.com/new-server-digital-ocean)。

为了本教程的目的,我们将选择最便宜的可用服务器类型 1gb RAM、1 CPU、25GB SSD,价格为 5 美元/月,地区为:🇳🇱AMS3。

点击“创建服务器”。

之后,您将进入服务器列表。您将看到您的服务器,但没有任何详细信息。这是因为 Digital Ocean 尚未完全配置它。服务器配置完成后,将显示 IP 地址和服务器大小。配置进度显示在最右侧的列中。

您可以点击服务器名称查看服务器详情。此页面会定期更新,以反映服务器的当前状态。

当“状态”变为“活动”时,表示 Digital Ocean 已完成服务器配置。

此时“设置”字段应显示“已开始”。

这意味着 Appliku Deploy 已连接到服务器并正在运行安装脚本。它将安装运行应用程序所需的软件:Docker、Nginx、certbot,并对其进行配置。

完成设置大约需要 2-3 分钟。

您可以前往“设置日志”选项卡查看进度。请注意,此页面不会自动更新,您需要刷新页面才能查看最新记录。

返回服务器的“概览”选项卡:当“设置”字段显示“完成”时,表示您可以创建一个应用程序并将其部署在此服务器上。

如果“设置”字段显示“失败”,那么您可以检查“设置日志”选项卡以尝试找出发生了什么。

最常见的失败原因是云提供商提供的服务器质量不佳,由于网络或磁盘问题无法连接互联网。这种情况很少见,但确实会发生。

在这种情况下,您应该单击“在 Digital Ocean 面板中管理服务器”,销毁该服务器并返回 Appliku Deploy 界面创建另一个服务器。

如果您仍然在列表中看到旧服务器,请打开服务器详情,页面将刷新并显示服务器的当前状态。如果服务器被删除,服务器状态将变为“已删除”,服务器将从服务器列表中消失。现在您可以创建另一个服务器。

创建应用程序。转到“应用程序”选项卡,然后点击“从 GitHub 创建新应用程序”(https://app.appliku.com/start)。

您将看到“创建新应用程序”表单。

填写应用程序名称,选择存储库(我们之前创建的)、分支(main)并选择要部署到的服务器。

之后您可以点击“创建应用程序”。

您将发现自己位于新创建的应用程序页面上。

让我们快速浏览一下这个页面。

第一部分是构建设置。

它说基础镜像是 Python 3.8,这意味着 Appliku Deploy 在底层将使用 python:3.8 Docker 镜像来构建应用程序的镜像。

您可以指定“构建命令”,它将作为 Dockerfile 的最后一条语句执行。请记住,我们将所有环境变量传递给构建命令,因此您的构建命令将能够使用它。

如果您需要使用自己的 Dockerfile 说明,您可以在下拉菜单中选择“自定义 Dockerfile”并在文本字段中输入说明。

申请流程部分

Procfile在此列表中,您将看到存储库中除 之外的所有记录release

选择要启用的流程。目前我们只有web,因此请将开关切换到On

配置变量部分

在本节中,您应该指定在运行和构建阶段传递给应用程序的环境变量。

让我们创建几个变量。

DJANGO_SECRET_KEY给这个变量一些长值,比如“foisr45r4ufuihsuihiuh3rluhihuihrui4wh4uihu4huiwhui44343423”,这样就没有人能猜到了。

DJANGO_ALLOWED_HOSTS应该包含你的应用名称 + applikuapp.com。或者你稍后要附加到网站的任何其他域名。在我们的例子中是djangoapplikututorial.applikuapp.com

Heroku 配置变量同步

如果您要从 Heroku 迁移此应用程序,此功能将非常有用。您只需输入您的 Heroku API 密钥和应用程序名称,它将持续从 Heroku 拉取配置变量中的任何更改,并将其更新到您的应用程序中。请记住,在启用同步的情况下,在 appliku 中编辑配置变量是没有意义的——它们将在下次同步时被覆盖。

现在我们需要创建一个数据库和一个 redis 实例。

因此,我们现在不应该部署应用程序,而应该单击“继续应用程序概览”。

新应用程序的应用程序仪表板如下所示:

转到数据库选项卡。

数据库

单击“新建数据库”,选择 Postgres 并选择您的服务器。

点击“创建数据库”

您的新 postgres 数据库应该出现在列表中。

您可以看到数据库的类型、状态和凭证 URL。

当“状态”列为“已部署”时,表示您的数据库已准备好接受连接。

我们还需要一个 redis 实例。

我们以同样的方式添加它。

点击“新建数据库”按钮,选择redis和同一个服务器。点击“创建数据库”。

Redis 实例应该出现在列表中,就像 postgres 实例一样。

现在我们可以回去编辑我们的配置变量来使用我们的新数据库。

转到应用程序的“设置”选项卡。

点击“显示配置变量”

这就是我们现在所拥有的:

Postgres 和 redis 凭证被添加到它们自己的特定于实例的变量中。

我们现在需要的是使用来自 DATABASE_72_URL 的值创建 DATABASE_URL,并使用来自 REDIS_73_URL 的值创建 REDIS_URL。

我们必须手动执行此操作的原因是,您可以拥有多个相同类型的数据库,并且您负责设置要使用的应用程序的环境变量。

这就是配置变量在此阶段应该看起来的样子。

DATABASE_72_URL请记住,和中的数字REDIS_73_URL对您来说会有所不同。

现在我们已经设置好了配置变量,您应该转到“概览”选项卡。

我们已准备好开始第一次部署。

单击“立即部署”按钮。

将显示有关当前部署的信息:

当它完成时,状态将反映出来,表明它已经完成:

让我们单击“管理部署”并查看部署日志中生成的管理员密码。

您位于“部署”选项卡。

第一张卡片包含部署设置。您可以更改仓库分支、要部署到的服务器,并切换“推送部署”功能,该功能会在推送到 GitHub 仓库时启动部署。

第二张卡片包含部署历史记录。

在这个例子中,我有两个部署。第一个部署失败了,因为我在编写本教程时在设置文件中输入了一个拼写错误。你可以看到它显示“构建失败”。这显然意味着它失败了,但还有另一条信息——我们知道它在哪个阶段失败了:构建阶段。

您可以点击“查看日志”来了解失败的原因。

我们现在的重点应该是成功部署。

它显示“清理完毕”。

Appliku Deploy 中的部署有几个阶段:

  • 新建 - 部署刚刚创建,但尚未执行任何操作。
  • 构建 - 您的服务器正在从存储库中提取代码并构建图像
  • 部署——部署我们的镜像
  • 正在释放 - 正在执行释放命令
  • 清理 - 您的服务器正在清理过时的 docker 镜像层以释放一些磁盘空间。

当您单击“查看日志”时,您将在日志窗口末尾的某处看到密码。

让我们打开我们的应用。在“应用”导航栏中,点击“打开应用”。您的网站将在新窗口中打开。

您应该会看到“未找到”。

这是预料之中的,因为我们没有定义任何页面。

如果出现 502 网关错误,则说明您忘记启用“Web”进程。返回“进程”选项卡并启用 Web Worker。

然后在概览页面上点击“应用流程和环境变量”按钮。这应该比整个构建更快地应用更改。

如果您看到 400 Bad Request 错误,则说明您在 env var 中没有正确拼写域名DJANGO_ALLOWED_HOSTS

要修复此问题,请转到应用程序“设置”选项卡并编辑值以DJANGO_ALLOWED_HOSTS匹配域。

如果出现任何其他错误,请尝试重新部署应用并查看日志,查找任何与错误相关的内容。应用的“日志”选项卡也可以帮助您找出问题所在。

现在让我们进入 Django 项目的管理界面。

添加/admin到您的网站末尾。

您将看到 Django 登录表单。

输入admin@example.com登录名。密码是生成的,您可以在部署日志中看到。

点击“登录”按钮。

您应该会看到 Django 管理面板。

恭喜!

您刚刚创建了您的第一个 Django 应用,并将其部署到 Appliku Deploy 上。所有这一切都无需学习任何 DevOps 相关知识:nginx、证书等等。一切都已为您完成。

新文章即将发布,您将能够通过发送电子邮件、接受付款和构建适当的 SaaS 产品来扩展 Django 项目的功能。

部署愉快!

文章来源:https://dev.to/kostjapalovic/django-project-tutorial-for-beginners-settings-docker-compose-postgres-and-redis-1gdj
PREV
厌倦了部署,我建立了自己的 Heroku
NEXT
在 AWS Lightsail 上部署 Django 应用程序:Docker、Docker Compose、PostgreSQL、Nginx 和 Github 操作