Skip to content

Instantly share code, notes, and snippets.

@miranda-zhang
Last active March 25, 2018 02:52
Show Gist options
  • Save miranda-zhang/2a3efc4757f121a0f0c836c90fb8e780 to your computer and use it in GitHub Desktop.
Save miranda-zhang/2a3efc4757f121a0f0c836c90fb8e780 to your computer and use it in GitHub Desktop.

Deploy Django

Gunicorn

The Gunicorn "Green Unicorn" is a Python Web Server Gateway Interface (WSGI) HTTP server. Recommended to use with nginx.

Check version

gunicorn -v

http://docs.gunicorn.org/en/stable/install.html

Configuration

Read the doc.

An example configuration file: gunicorn_config.py

import multiprocessing

chdir = '/home/miranda/workspace/CodeBench/'
bind = "0.0.0.0:8000"
# bind = 'unix:/run/gunicorn/socket'
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'eventlet'
accesslog = 'gunicorn.access'
errorlog = 'gunicorn.error'
reload = True # Restart workers when code changes.

Use cpu_count() * 2 + 1 gunicorn workers.

You can check number of cores with the following linux command:

nproc

This can be changed up and down at runtime using signals. See gunicorn docs for details.

Testing Gunicorn's Ability to Serve the Project

gunicorn -c gunicorn_config.py myproject.wsgi:application

e.g. :

gunicorn -c gunicorn_config.py codebench.wsgi:application

We passed Gunicorn a module by specifying the relative directory path to Django's wsgi.py file, which is the entry point to our application, using Python's module syntax. Inside of this file, a function called application is defined, which is used to communicate with the application. To learn more about the WSGI specification, click here.

Other Settings

TODO: reload, Security(limit_request_field_size) etc.

See documentation for more details.

reload

This setting is intended for development.

Systemd

A tool that is starting to be common on linux systems is Systemd. Below are configurations files and instructions for using systemd to create a unix socket for incoming Gunicorn requests. Systemd will listen on this socket and start gunicorn automatically in response to traffic. Later in this section are instructions for configuring Nginx to forward web traffic to the newly created unix socket.

Configuration

gunicorn.service

/etc/systemd/system/gunicorn.service

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
PIDFile=/home/miranda/run/gunicorn/pid
User=miranda
Group=www-data
RuntimeDirectory=gunicorn
WorkingDirectory=/home/miranda/workspace/CodeBench/deploy
ExecStart=/root/.virtualenvs/codebench/bin/gunicorn --pid /home/miranda/run/gunicorn/pid   \
          -c gunicorn_config.py \
          codebench.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
Restart=always
RestartSec=3
PrivateTmp=true

[Install]
WantedBy=multi-user.target

We'll let the www-data group which Nginx belongs to be the group owners.

RuntimeDirectory is the a subdirectory relative to WorkingDirectory. https://www.freedesktop.org/software/systemd/man/systemd.exec.html

We need to give the path to the Gunicorn executable, which is stored within our virtual environment. It can be find out by:

type gunicorn

We will tell it to use a Unix socket instead of a network port to communicate with Nginx, since both services will be running on this server. This is more secure and faster.

You can add any other configuration for Gunicorn here as well. For instance, we can specify a config file via -c. WorkingDirectory can specify a directory for your script to execute, we have put the gunicorn_config.py in this directory. Read more here

gunicorn.socket

/etc/systemd/system/gunicorn.socket

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/home/miranda/run/gunicorn/socket

[Install]
WantedBy=sockets.target

tmpfiles.d

todo: not working at the moment, using /home/miranda/run/gunicorn instead.

/etc/tmpfiles.d/gunicorn.conf

d /run/gunicorn 0755 miranda www-data -

systemd-tmpfiles uses the configuration files to describe the creation, cleaning and removal of volatile and temporary files and directories which usually reside in directories such as /run or /tmp.

Type Path           Mode UID     GID      Age Argument
   d /run/gunicorn  0755 miranda www-data -

See documentation here.

Type d

Create a directory. The mode and ownership will be adjusted if specified and the directory already exists. Contents of this directory are subject to time based cleanup if the time argument is specified.

systemctl

Next enable the socket so it autostarts at boot:

systemctl enable gunicorn.socket

Either reboot, or start the services manually:

systemctl start gunicorn.socket

After running curl --unix-socket /run/gunicorn/socket http, Gunicorn should start and you should see some HTML from your server in the terminal.

  1. If not, try check:
systemctl status gunicorn
  1. Check error log

  2. To be sure that gunicorn is refreshed, run these commands:

pkill gunicorn
systemctl daemon-reload
systemctl start gunicorn
  1. Version related problem:
pip install "eventlet==0.20.0" --force-reinstall
  1. Ensure systemd services restart on failure

/etc/systemd/system/gunicorn.service

Restart=always
RestartSec=3

In one terminal

watch "ps -ef|grep gunicorn"

Keep the above open, open another terminal, try to kill it:

pkill gunicorn

You should see the process restart.

Configure Nginx to send traffic to the new Gunicorn socket

You must now configure your web proxy to send traffic to the new Gunicorn socket. Edit your nginx.conf to include the following:

proxy_pass http://unix:/run/gunicorn/socket;

Read more here or read the section below about Nginx.

Now make sure you enable the nginx service so it automatically starts at boot:

systemctl enable nginx.service

Either reboot, or start Nginx with the following command:

systemctl start nginx

Supervisor

It does a small subset tasks which can be done with systemd (or upstart in earlier version of system).

One advantage of supervisor is that it is largely platform agnostic,so you don't have to worry about whether your distro has upstart/systemd/whatever. Plus, supervisor is a bit easier to configure and get running correctly when all you want to do is keep an app running forever.

Read more here

Nginx

A Comprehensive Guide

Installation

Before installation make sure port 80 is not being used.

netstat -tulpn | grep :80

apt install nginx-core

Nginx Configuration

An example configuration file with Nginx:

http://docs.gunicorn.org/en/stable/deploy.html

All nginx configuration files are located in the /etc/nginx/ directory. The primary configuration file is /etc/nginx/nginx.conf. Note, This is where the files will be located if you install nginx from the package manager.

static directory

python manage.py collectstatic

include statement

Try to avoid too many chained inclusions (i.e., including a file that itself includes a file, etc.) Keep it to one or two levels of inclusion if possible, for readability purposes. You can include all files in a certain directory. Or to be more specific, you can include all .conf files in a directory:

include /etc/nginx/conf.d/*.conf;

sites-enabled directory

include /etc/nginx/sites-enabled/*;

This allows for server block configurations to be loaded in from separate files found in the sites-enabled sub-directory. Usually these are symlinks to files stored in /etc/nginx/sites-available/. By using symlinks you can quickly enable or disable a virtual server while preserving its configuration file.

Linux command, create a link to TARGET with the name LINK_NAME:

ln -sf TARGET LINK_NAME

-s, --symbolic: make symbolic links instead of hard links -f, --force: remove existing destination files

How Nginx Processes Headers

  • Nginx gets rid of any empty headers. There is no point of passing along empty values to another server; it would only serve to bloat the request.

  • Nginx, by default, will consider any header that contains underscores as invalid. It will remove these from the proxied request. If you wish to have Nginx interpret these as valid, you can set the underscores_in_headers directive to "on", otherwise your headers will never make it to the backend server.

  • The "Host" header is re-written to the value defined by the $proxy_host variable. This will be the IP address or name and port number of the upstream, directly as defined by the proxy_pass directive.

  • The "Connection" header is changed to "close". This header is used to signal information about the particular connection established between two parties. In this instance, Nginx sets this to "close" to indicate to the upstream server that this connection will be closed once the original request is responded to. The upstream should not expect this connection to be persistent.

  • $host: This variable is set, in order of preference to: the host name from the request line itself, the "Host" header from the client request, or the server name matching the request. There are other common values for the "Host" header: like $proxy_host and $http_host. In most cases, you will want to set the "Host" header to the $host variable. It is the most flexible and will usually provide the proxied servers with a "Host" header filled in as accurately as possible.

Setting (or Resetting) Headers

The X-Forwarded-Proto header gives the proxied server information about the schema of the original client request (whether it was an http or an https request).

The X-Real-IP is set to the IP address of the client so that the proxy can correctly make decisions or log based on this information.

We could move the proxy_set_header directives out to the server or http context, allowing it to be referenced in more than one location.

Using Buffers to Free Up Backend Servers

Without buffers, data is sent from the proxied server and immediately begins to be transmitted to the client. If the clients are assumed to be fast, buffering can be turned off in order to get the data to the client as soon as possible. With buffers, the Nginx proxy will temporarily store the backend's response and then feed this data to the client. If the client is slow, this allows the Nginx server to close the connection to the backend sooner. It can then handle distributing the data to the client at whatever pace is possible.

More details in this article: Understanding nginx http proxying load balancing buffering and caching

create a self-signed certificate with openssl

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365

https://stackoverflow.com/questions/10175812/how-to-create-a-self-signed-certificate-with-openssl

Test Nginx Configuration

nginx -t

Restart Nginx

service nginx restart

Nginx as load balancer

Read the doc.

Load Balancing Method

Round-robin by default.

Separate out Database server from App server

SQLite isn't a network database, so it doesn't have any network connection ability built into it. Recommended to use something else.

MySQL

Ubuntu Installation
apt install mysql-server mysql-client libmysqlclient-dev python-dev
pip install mysql-python
Win 10 desktop (Enterprise) Installation
easy_install mysql-python
MySQL setup

Create a database named codebench and a database named codebench_test on the newly installed MySQL instance. This can be achieved using

echo "CREATE DATABASE codebench character set UTF8 collate utf8_bin;CREATE DATABASE codebench_test set UTF8 collate utf8_bin;" | mysql -uroot -p<root MySQL password>

Grant remote access permission:

  1. Change mysql config

Start with mysql config file:

/etc/mysql/my.cnf

For me, it contains:

!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/

And the relevant line is in

/etc/mysql/mysql.conf.d/mysqld.cnf

Comment out this line by adding # in front:

bind-address      = 127.0.0.1
  1. Restart mysql server.
service mysql reload

Different operating systems would need different commands, read more here.

  1. Change GRANT privilege

By default, mysql username and password you are using is allowed to access mysql-server locally. So need to update privilege.

mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '<root MySQL password>' WITH GRANT OPTION;
mysql> FLUSH PRIVILEGES;

You can also specify a separate USERNAME & PASSWORD for remote access.

You can check final outcome by:

SELECT * from information_schema.user_privileges where grantee like "'root'%";
  1. Test Connection, from the remote server terminal/command-line:

    mysql -h HOST -u USERNAME -p

Read more here, also has examples on "Revoke Access".

May need to strict up those settings to implement a better security practice. Read more on Django security.

Check charset
SELECT SCHEMA_NAME 'database', default_character_set_name 'charset', DEFAULT_COLLATION_NAME 'collation' FROM information_schema.SCHEMATA;

https://stackoverflow.com/questions/1049728/how-do-i-see-what-character-set-a-mysql-database-table-column-is

Django settings.py

Django docs for database setup.

If you are moving from SQLite to MySQL, it's recommended to backup the data, then delete all migration files (except the __init__.py), so you can makemigrations from a clean state before try to migrate data to the new MySQL database.

python manage.py makemigrations
python manage.py migrate

You should only do it once on the database server, assuming you only have one database server.

Redis

Similar to above, comment out the bind 127.0.0.1 line in:

/etc/redis/redis.conf

TODO: Again security issues, need careful tuning on production server. Such as, adding an AUTH password in Redis and configuring your firewall (e.g. iptables) to block unauthorized clients. Read more on Stackoverflow, and this blog.

Restart the service

service redis-server restart

Test on remote server:

redis-cli -h [db server ip] ping

Enabling Session Persistence

Read the documentation here.

The sticky cookie method. With this method, NGINX adds a session cookie to the first response from the upstream group and identifies the server which has sent the response. When a client issues next request, it will contain the cookie value and NGINX will route the request to the same upstream server:

sticky cookie srv_id expires=1h;

The srv_id parameter sets the name of the cookie which will be set or inspected. The optional expires parameter sets the time for the browser to keep the cookie. The optional domain parameter defines a domain for which the cookie is set. The optional path parameter defines the path for which the cookie is set.

Compiling the nginx Sticky Session Module in Ubuntu

Sticky upstream is a third party module, check the list of third party Nginx modules here

Check the original tutorial.

Install Dependencies

sudo su -l aptitude update && aptitude dist-upgrade aptitude install build-essential software-properties-common

Remote desktop Edit /etc/xrdp/xrdp.ini

Fabric

  • make executing shell commands over SSH easy and Pythonic
  • automate interactions with remote servers

http://docs.fabfile.org/en/1.14/tutorial.html

Thread-safety

Generic Django issues

Eventlet-module issues

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment