I have just returned from tutoring at NCSS 2017 (which was absolutely amazing - totally recommend!), where our group created a website built on python and the tornado framework.

To make it easier for everyone in our group to test out the finished website together, I set up a copy on a vps. What follows is a quick walkthrough of what I did and how you can set up something similar in only a few minutes.

what you’ll need

Make sure you have access to the following:

  • domain or subdomain of which you have ownership
  • I used a Digitalocean vps, but any server running linux that you have root access to would be fine.
  • nginx installed and running
  • you’ll also need to have python installed

create a new user

For security the python server should be running on a separate user with limited privileges.

# useradd -r -m webapp-user

Note: where I use names like webapp-user, you can use whatever you like as long as it’s consistent!

change to that user

You should change to the new user temporarily for downloading the source code and setting up the server for running.

# su - webapp-user

download the code base (as webapp-user)

If your code is hosted on git (as ours was), you can simply git clone <repo>. If it’s a private repository, it’s a simple process to set up deploy keys for read-only access from the server. Otherwise download the code from wherever it is and extract into a subdirectory of /home/webapp-user.

Now you can perform any steps required to be able to actually run the development server. For example, creating a virtual environment and installing dependencies with pip, which you should already know how to do if you’ve been testing the server during development.

create a run script (as webapp-user)

I find a quick way to get a development server running is to simply run it within a tmux session. Install tmuxinator and create a new project to run the server.

$ gem install tmuxinator
$ # tmuxinator needs EDITOR set
$ echo "export EDITOR=vim" >> ~/.bashrc
$ source ~/.bashrc
$ tmuxinator new webapp

My tmuxinator yml config ended up looking like the following. Note that your config may need to be more complex if you use virtual environments, etc. Tmuxinator has good documentation so it shouldn’t be difficult to set up.

# ~/.tmuxinator/webapp.yml

name: webapp
root: ~/webapp-root-dir/

windows:
  - server: python3 server.py

Starting the server is now just a matter of running:

$ tmuxinator start webapp

You can then check up on the server by attaching to the tmux session it started.

add to crontab for autostart on reboot (as webapp-user)

This is optional, but nice for if your server reboots and you don’t want to have to manually restart the server. Edit the crontab for webapp-user to add:

@reboot /home/webapp-user/bin/tmuxinator start webapp

Note that sometimes PATH isn’t set to what you want when things are run with cron, so might be a good idea to use the full path of tmuxinator here.

create a Let’s Encrypt certificate

We definitely need SSL, so an easy way is to use the certbot tool to generate a certificate signed by Let’s Encrypt:

# certbot certonly -d webapp.example.com

You can save a few minutes by not bothering to set up ssl here if you must. In that case you will need to remove the ssl parts of the nginx config file in the next step.

configure nginx as reverse proxy

The development server should be listening on localhost at a port such as 8888 or 5000 (the firewall should be set to block incoming connections to the port too)), so is inaccessible from the outside world. Now we need to configure nginx to act as a reverse proxy to handle the ssl encryption and listen on ports 80 and 443. Security!

Enter something like the following in your nginx config. If you already use nginx for other sites, you’ll only have to add the two server blocks.

# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" $request_time';

  access_log  /var/log/nginx/access.log  main;

  sendfile            on;
  tcp_nopush          on;
  tcp_nodelay         on;
  keepalive_timeout   65;
  types_hash_max_size 2048;

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

  # ssl proxy configuration
  # change 'webapp.example.com' to your domain
  # also set proxy_pass port number to whatever your server is listening on
  server {
    listen       443 ssl;
    listen       [::]:443 ssl;

    access_log  /var/log/nginx/streetline_access.log  main;

    # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
    ssl_certificate /etc/letsencrypt/live/webapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/webapp.example.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
    ssl_dhparam /etc/nginx/dhparams.pem;

    # intermediate configuration. tweak to your needs.
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_prefer_server_ciphers on;

    # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
    add_header Strict-Transport-Security max-age=15768000;

    # OCSP Stapling ---
    # fetch OCSP records from URL in ssl_certificate and cache them
    ssl_stapling on;
    ssl_stapling_verify on;

    ## verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /etc/letsencrypt/live/webapp.example.com/chain.pem;

    resolver 8.8.8.8 8.8.4.4 valid=86400;
    resolver_timeout 10;


    server_name  webapp.example.com;
    root         /usr/share/nginx/html/;

    # Load configuration files for the default server block.
    # include /etc/nginx/default.d/*.conf;

    location / {
      proxy_set_header        Host $host;
      proxy_set_header        X-Real-IP $remote_addr;
      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header        X-Forwarded-Proto $scheme;
      proxy_set_header        Proxy "";

      proxy_pass          http://localhost:5000;
      ## sometimes necessary:
      # proxy_read_timeout  90;

      proxy_redirect      http://localhost:5000 https://webapp.example.com;
    }

  }

  # redirect all http traffic to https
  server {
    listen       80;
    listen       [::]:80;
    server_name  webapp.example.com;
    return       301 https://$server_name$request_uri;
  }
}

Done!

And that should be it. The final setup should consist of:

  • a Let’s Encrypt ssl certificate
  • python/tornado (or other framework) server listening on localhost:5000
  • nginx reverse proxy to the python server and using the certificate
  • server running in tmux and set to start automatically on boot