Date
Nov. 21st, 2024
 
2024年 10月 21日

Post: Deploy Django with Gunicorn and Ngnix

Deploy Django with Gunicorn and Ngnix

Published 18:01 Jan 28, 2020.

Created by @ezra. Categorized in #Programming, and tagged as #Back-end, #Django, #Gunicorn, #Nginx, #UNIX/Linux, #Ubuntu Linux.

Source format: Markdown

Table of Content

准备

首先要更新软件源,以 Ubuntu 为例:

$ sudo apt-get update -y

安装 python3virtualenvnginxsqlite3openssl

$ sudo apt-get install -y python3 nginx sqlite3 openssl
$ python -m pip install virtualenv

之后,创建并进入虚拟环境:

$ cd /path/to/project/
$ virtualenv venv
$ source venv/bin/activate

需要退出时:

(venv) $ deactivate

在虚拟环境中安装 djangogunicorn

(venv) $ pip install django gunicorn

创建项目

如果还没有创建项目:

$ mkdir django-project/
$ django-admin startproject myproject django-project/
$ cd django-project/
$ django-admin startapp myapp
$ python manage.py migrate
$ mkdir -pv myapp/templates/myapp/

现在,你的目录结构大概是这样的:

/home/ubuntu/
│
├── django-project/
│    │
│    ├── myapp/
│    │   ├── admin.py
│    │   ├── apps.py
│    │   ├── __init__.py
│    │   ├── migrations/
│    │   │   └── __init__.py
│    │   ├── models.py
│    │   ├── templates/
│    │   │   └── myapp/
│    │   ├── tests.py
│    │   └── views.py
│    │
│    ├── myproject/
│    │   ├── asgi.py
│    │   ├── __init__.py
│    │   ├── settings.py
│    │   ├── urls.py
│    │   └── wsgi.py
│    │   
│    ├── db.sqlite3
│    └── manage.py
│
└── venv/  ← Virtual environment

接着,编辑 myproject/settings.py 文件,在 INSTALLED_APPS 部分添加刚才我们创建的 myapp 应用:

$ nano myproject/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "myapp",
]

创建主页:

$ nano myapp/templates/myapp/home.html
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

准备渲染工作:

$ nano myapp/views.py
from django.shortcuts import render

def index(request):
    return render(request, "myapp/home.html")

添加链接:

$ nano myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]
$ nano myproject/urls.py
from django.urls import include, path

urlpatterns = [
    path("myapp/", include("myapp.urls")),
    path("", include("myapp.urls")),
]

SECRET_KEY

准备好一个简单的页面后,我们开始进入正题,编辑 myproject/settings.py 并找到类似下面的内容:

SECRET_KEY = "django-insecure-o6w@a46mx..."  # 删除或注释这行
# SECRET_KEY = "django-insecure-o6w@a46mx..."  # 删除或注释这行

相应的,使用新的内容替换:

import os

# ...

try:
    SECRET_KEY = os.environ["SECRET_KEY"]
except KeyError as e:
    raise RuntimeError("Could not find a SECRET_KEY in environment") from e

此时,Django 便知道要去环境变量中寻找 SECRET_KEY 而不是在项目源文件中。

现在我们去创建这个环境变量:

$ echo "export SECRET_KEY='$(openssl rand -hex 40)'" > .DJANGO_SECRET_KEY
$ source .DJANGO_SECRET_KEY

你可以查看这个文件进行校验:

$ cat .DJANGO_SECRET_KEY
export SECRET_KEY='26a2d2ccaf9ef850...'

WSGIServer

$ pwd
/home/ubuntu
$ source env/bin/activate
$ python -m pip install httpie
$ # 发送 GET 请求并且追踪 30 次重定向
$ alias GET='http --follow --timeout 6'

有必要检查一下目前的进展:

$ cd django-project/
$ python manage.py check
System check identified no issues (0 silenced).
$ # 在后台 127.0.0.1:8000
$ nohup python manage.py runserver &
$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &

现在这个网站还只能在本地访问,我们来成为第一个访客吧!

$ GET :8000/myapp/
HTTP/1.1 200 OK
Content-Length: 182
Content-Type: text/html; charset=utf-8
Date: Sat, 28 Jan 2020 00:11:38 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.10
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

准备上线

当然,首先你要有一台服务器以及其公网 IP,并完成配置 DNS、域名解析等操作。

例如:

TypeHostValue TTL
A Record@ 50.19.125.152Automatic
A Recordwww50.19.125.152Automatic

现在,我们先结束前面运行的服务:

$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &
$ kill 43689
[1]+  Done                    nohup python manage.py runserver

可以进一步确认:

$ pgrep runserver  # 空
$ jobs -l  # Empty or 'Done'
$ sudo lsof -n -P -i TCP:8000 -s TCP:LISTEN  # 空
$ rm nohup.out
$ nano project/settings.py
# 把下面的域名换成你的域名:
ALLOWED_HOSTS = [".caveops.com"]
# 这样可以同时允许 `www.caveops.com` 与 `caveops.com`。

再次尝试运行:

$ nohup python manage.py runserver '0.0.0.0:8000' &

打开日志 nohup.out 输出:

$ tail -f nohup.out

现在,到浏览器里输入下面的地址试试吧:

http://www.caveops.codes:8000/myapp/

而在刚刚打开的日志中,应该会出现类似下面的内容:

[<date>] "GET /myapp/ HTTP/1.1" 200 182

Gunicorn

现在我们将 WSGIServer 替换为 Gunicorn吧。

$ pwd
/home/ubuntu/django-project
$ mkdir -pv config/gunicorn/
mkdir: created directory 'config'
mkdir: created directory 'config/gunicorn/'
$ sudo mkdir -pv /var/{log,run}/gunicorn/
mkdir: created directory '/var/log/gunicorn/'
mkdir: created directory '/var/run/gunicorn/'
$ sudo chown -cR ubuntu:ubuntu /var/{log,run}/gunicorn/
changed ownership of '/var/log/gunicorn/' from root:root to ubuntu:ubuntu
changed ownership of '/var/run/gunicorn/' from root:root to ubuntu:ubuntu

创建配置文件:

$ nano config/gunicorn/dev.py
"""Gunicorn *development* config file"""

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "caveops.wsgi:application"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 2
# The socket to bind
bind = "0.0.0.0:8000"
# Restart workers when code changes (development only!)
reload = True
# Write access and error info to /var/log
accesslog = "/var/log/gunicorn/dev.log"
errorlog = "/var/log/gunicorn/error.log"
enable_stdio_inheritance = True
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
pidfile = "/var/run/gunicorn/dev.pid"
# Daemonize the Gunicorn process (detach & enter background)
daemon = True

关闭之前运行的服务:

$ jobs -l
[1]+ 26374 Running                 nohup python manage.py runserver &
$ kill 26374
[1]+  Done                    nohup python manage.py runserver

在使用 Gunicorn 运行前,别忘了 .DJANGO_SECRET_KEY 哦:

$ pwd
/home/ubuntu/django-project
$ source .DJANGO_SECRET_KEY
$ gunicorn -c config/gunicorn/dev.py

现在可以看看日志了:

$ tail -f /var/log/gunicorn/dev.log
[2020-01-28 01:29:50 +0000] [49457] [INFO] Starting gunicorn 20.1.0
[2020-01-28 01:29:50 +0000] [49457] [DEBUG] Arbiter booted
[2020-01-28 01:29:50 +0000] [49457] [INFO] Listening at: http://0.0.0.0:8000 (49457)
[2020-01-28 01:29:50 +0000] [49457] [INFO] Using worker: sync
[2020-01-28 01:29:50 +0000] [49459] [INFO] Booting worker with pid: 49459
[2020-01-28 01:29:50 +0000] [49460] [INFO] Booting worker with pid: 49460
[2020-01-28 01:29:50 +0000] [49457] [DEBUG] 2 workers

同样的,在浏览器里访问试试:

http://www.caveops.com:8000/myapp/

你会在日志中看到一些新的信息:

113.xx.xx.xx - - [27/Sep/2020:01:28:46 +0000] "GET /myapp/ HTTP/1.1" 200 182

Nginx

首先,在你的服务器管理平台开放 80 端口 HTTP (TCP) 访问。

启动 Nginx

$ sudo systemctl start nginx
$ sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; ...
   Active: active (running) since Mon 2020-01-28 01:37:04 UTC; 2min 49s ago
...

到浏览器访问试试:

http://caveops.com/

这是你应该会看到一个 Welcome to nginx 的页面。

而如果你访问前面那个地址,会显示 404 Not Found

http://caveops.com/myapp/

创建一个配置文件:

$ nano /etc/nginx/sites-available/caveops
server_tokens               off;
access_log                  /var/log/nginx/supersecure.access.log;
error_log                   /var/log/nginx/supersecure.error.log;

server {
        listen 80;
    server_name caveops.com www.caveops.com;

    location /favicon.ico {
        alias /home/ubuntu/django-project/collectstatic/favicon.ico;
    }
    location /robots.txt {
        alias /home/ubuntu/django-project/collectstatic/robots.txt;
    }
    location /humans.txt {
        alias /home/ubuntu/django-project/collectstatic/humans.txt;
    }
    location /static {
        autoindex on;
        alias /home/ubuntu/django-project/collectstatic;

        # kill cache
        add_header Last-Modified $date_gmt;
        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
        if_modified_since off;
        expires off;
        etag off;
        # don't cache it
        proxy_no_cache 1;
        # even if cached, don't try to use it
        proxy_cache_bypass 1; 
    }
    location /media {
        autoindex on;
        alias   /home/ubuntu/django-project/media/;

        # kill cache
        add_header Last-Modified $date_gmt;
        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
        if_modified_since off;
        expires off;
        etag off;
        # don't cache it
        proxy_no_cache 1;
        # even if cached, don't try to use it
        proxy_cache_bypass 1; 
    }
    location / {
        proxy_pass              http://localhost:8000;
        proxy_set_header        Host $host;
        proxy_pass_header       Server;
        proxy_redirect          off;
        proxy_set_header        X-Forwarded-For $remote_addr;
        proxy_set_header        X-Scheme $scheme;
        proxy_connect_timeout 60;
        proxy_read_timeout    60;

        # kill cache
        add_header Last-Modified $date_gmt;
        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
        if_modified_since off;
        expires off;
        etag off;
        # don't cache it
        proxy_no_cache 1;
        # even if cached, don't try to use it
        proxy_cache_bypass 1; 
    }
}

同时修改默认配置:

$ nano /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##
        client_max_body_size 200M;
        sendfile off;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
# 
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
# 
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
# 
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }
#}

上面的配置中关闭了 Nginx 缓存功能,如果你不需要,请根据情况自行修改:

#  /etc/nginx/sites-available/caveops

        # kill cache
        add_header Last-Modified $date_gmt;
        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
        if_modified_since off;
        expires off;
        etag off;
        # don't cache it
        proxy_no_cache 1;
        # even if cached, don't try to use it
        proxy_cache_bypass 1; 
# /etc/nginx/nginx.conf

        sendfile off;

别急,此时这个配置文件还不能使用,我们还需要处理一些 Django 的设置:

$ pwd
/home/ubuntu/django-project
$ mkdir -p static/js

创建一个 JavaScript 文件用来测试:

$ nano static/js/greenlight.js
// Enlarge the #changeme element in green when hovered over
(function () {
    "use strict";
    function enlarge() {
        document.getElementById("changeme").style.color = "green";
        document.getElementById("changeme").style.fontSize = "xx-large";
        return false;
    }
    document.getElementById("changeme").addEventListener("mouseover", enlarge);
}());

修改 Django 设置:

$ nano myproject/settings.py
STATIC_URL = "/static/"
# Note: 把下面的域名替换为你的域名
STATIC_ROOT = BASE_DIR / 'collectstatic'
STATICFILES_DIRS = [BASE_DIR / "static"]

MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'

同时关闭 DEBUG 设置:

DEBUG = False

创建这个目录:

$ sudo mkdir -pv /var/www/caveops/static/
mkdir: created directory '/var/www/caveops'
mkdir: created directory '/var/www/caveops/static/'
$ sudo chown -cR ubuntu:ubuntu /var/www/caveops/
changed ownership of '/var/www/caveops/static' ... to ubuntu:ubuntu
changed ownership of '/var/www/caveops/' ... to ubuntu:ubuntu

整理静态文件:

$ pwd
/home/ubuntu/django-project
$ source venv/bin/activate
(venv )$ python3 manage.py collectstatic
129 static files copied to '/var/www/caveops/static'.

现在测试一下 Nginx 的配置文件吧:

$ sudo service nginx configtest /etc/nginx/sites-available/caveops
 * Testing nginx configuration                                  [ OK ]

重启 Nginx

$ sudo systemctl restart nginx

在浏览器访问试试:

http://caveops.com/myapp/

你应当会看到我们编写的网站页面,并且文字被替换成了绿色。也就是说,页面访问与资源文件访问的配置都完成了。

HTTPS

简单的方法

最简单的办法是通过 Cloudfrale,使用 Cloudfrale 的 DNS 并进行域名解析。

请注意,你可能需要在 Cloudfrale 控制台对缓存机制进行设置已达到理想效果。

复杂一些的方法

首先修改 Nginx 配置文件:

$ nano /etc/nginx/nginx.conf

替换下面内容

# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1.2 TLSv1.3;

确认是否支持 1.3

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

现在可以安装 Certbot 了:

$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

此时仍要注意开放端口:

ReferenceTypeProtocolPort RangeSource
1HTTPS TCP443 0.0.0.0/0
2HTTPTCP800.0.0.0/0
3CustomAllAllsecurity-group-id
4SSHTCP22my-laptop-ip-address/32

执行下面命令:

$ sudo certbot --nginx --rsa-key-size 4096 --no-redirect
Saving debug log to /var/log/letsencrypt/letsencrypt.log
...

你可能会被要求根据提示进行一些配置,比如输入邮箱等。

当被要求输入域名时,仿照下列格式(逗号分隔)输入:

www.caveops.com,caveops.com

完成后,你会看到类似下面的信息:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/www.caveops.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/www.caveops.com/privkey.pem
This certificate expires on 2020-01-28.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this
  certificate in the background.

Deploying certificate
Successfully deployed certificate for caveops.com
  to /etc/nginx/sites-enabled/caveops
Successfully deployed certificate for www.caveops.com
  to /etc/nginx/sites-enabled/caveops
Congratulations! You have successfully enabled HTTPS
  on https://caveops.com and https://www.caveops.com

接下来在 /etc/nginx/site-available/caveopsserver 中增加一段内容:

server {

  # ....

  # 增加下面的内容:

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.caveops.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.caveops.com/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

重新加载 Nginx

$ sudo systemctl reload nginx

在浏览器访问试试:

https://www.caveops.com/myapp/

此时大部分浏览器都会在地址栏出现一个锁图标,大功告成!

HTTP 重定向到 HTTPS

同样还是修改 Nginx 配置文件:

$ nano /etc/nginx/sites-available/caveops

单独添加下面的内容:

server {
  server_name       .caveops.com;
  listen                    80;
  return                    307 https://$host$request_uri;
}
# 添加上面的内容

server {
  # ...
} 

再次进行测试:

$ sudo service nginx configtest /etc/nginx/sites-available/supersecure
 * Testing nginx configuration                                  [ OK ]

重新载入:

$ sudo systemctl reload nginx

使用 HTTPie 访问:

$ GET --all http://caveops.com/myapp/
HTTP/1.1 307 Temporary Redirect
Connection: keep-alive
Content-Length: 164
Content-Type: text/html
Date: Tue, 28 Jan 2020 02:16:30 GMT
Location: https://caveops.com/myapp/
Server: nginx

<html>
<head><title>307 Temporary Redirect</title></head>
<body bgcolor="white">
<center><h1>307 Temporary Redirect</h1></center>
<hr><center>nginx</center>
</body>
</html>

HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Jan 2020 02:16:30 GMT
Referrer-Policy: same-origin
Server: nginx
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>
Pinned Message
HOTODOGO
The Founder and CEO of Infeca Technology.
Developer, Designer, Blogger.
Big fan of Apple, Love of colour.
Feel free to contact me.
反曲点科技创始人和首席执行官。
开发、设计与写作皆为所长。
热爱苹果、钟情色彩。
随时恭候 垂询