Date
Jul. 19th, 2024
 
2024年 6月 14日

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
I'm looking for a SOFTWARE PROJECT DIRECTOR / SOFTWARE R&D DIRECTOR position in a fresh and dynamic company. I would like to gain the right experience and extend my skills while working in great teams and big projects.
Feel free to contact me.
For more information, please view online résumé or download PDF
本人正在寻求任职 软件项目经理 / 软件技术经理 岗位的机会, 希望加⼊某个新鲜⽽充满活⼒的公司。
如有意向请随时 与我联系
更多信息请 查阅在线简历下载 PDF