NGINX reverse proxy with SSL for Jupyter and TapChat

Using NGINX as a reverse proxy for webapps

I got a KVM VPS from SSDNodes last week. I wanted to host a transmission seedbox, tapchat IRC bouncer and Jupyter notebook on it. I've done all of these before, but it takes too long to look up guides for each of these every time I need to do this. I'm documenting the process of setting up jupyter and NGINX here for future reference. The VPS is running Ubuntu 16.04, but the procedure shouldn't bee too different for other distros

Install NGINX and docker

First, install docker by following the official documentation here:

Installing Docker using the repository

# update apt list
sudo apt-get update

# install essential packages
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

# add gpg keys for the docker repository
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# add the AMD64 docker repository
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

# update apt list
sudo apt-get update

# install docker
sudo apt-get install docker-ce

# verify that docker works
sudo docker run hello-world

Next, install NGINX

apt install nginx

Verify that NGINX works by opening your server domain name in your browser

Enable SSL

Install certbot

Use the LetsEncrypt certbot tool to generate an SSL certificate and configure NGINX to use it

Securing NGINX with LE on Ubuntu 16.04 - DigitalOcean tutorial

First, make sure the domains you want to use are configured to point to the IP address of your server. With the default NGINX configuration, all of these domains should show the default NGINX page when opened on your prowser.

Next, install certbot

# Add certbot PPA
sudo add-apt-repository ppa:certbot/certbot

# Update package list
sudo apt-get update

# Install certbot
sudo apt-get install python-certbot-nginx

Configure NGINX to use SSL

Next, edit the default NGINX config and add your domains

sudo nano /etc/nginx/sites-available/default

Find the existing server_name line and replace the underscore, _, with your domain name:

server_name domain.tld subdomain1.domain.tld subdomain2.domain.tld;

now reload the NGINX configuration.

sudo systemctl reload nginx

Obtain an SSL certificate

Certbot makes it easy to set up SSL for existing NGINX configurations

sudo certbot --nginx -d domain.tld -d subdomain1.domain.tld -d subdomain2.domain.tld

I chose to redirect HTTP requests to HTTPS when certbot prompted, but this isn't necessary. Once the certificates are obtained, open each domain in your browser to verify that the certificate was successfully installed.

Set up auto-renewal

There is no need to add a crontab entry to renew certificates. The certbot package automatically adds a systemd timer to renew the certificates. Verify that the renewal process works by doing a dry run.

sudo certbot renew --dry-run

Check if the auto renew script is running by listing systemd timers.

root@kvm:~# systemctl list-timers
NEXT                         LEFT          LAST                         PASSED       UNIT                         ACTIVATES
Sun 2017-12-03 09:08:15 PST  1h 40min left Sun 2017-12-03 03:01:58 PST  4h 25min ago snapd.refresh.timer          snapd.refresh.service
Sun 2017-12-03 09:12:01 PST  1h 44min left Sun 2017-12-03 03:01:58 PST  4h 25min ago apt-daily.timer              apt-daily.service
Sun 2017-12-03 12:41:34 PST  5h 13min left n/a                          n/a          certbot.timer                certbot.service
Sun 2017-12-03 19:48:58 PST  12h left      Sat 2017-12-02 19:48:58 PST  11h ago      systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service

4 timers listed.
Pass --all to see loaded but inactive timers, too.

Running jupyter

Original instructions

Set up websocket and HTTP forwarding in NGINX

Edit the NGINX config to forward ipython.domain.tld (or any other subdomain) to 127.0.0.1:8888. To do this, first I removed ipython.domain.tld from the default server config, and created a separate server block for it. This is what the original server block after setting up SSL looked like:

# Default server configuration
#
server {
	listen 80 default_server;
	listen [::]:80 default_server;

	# SSL configuration
	#
	# listen 443 ssl default_server;
	# listen [::]:443 ssl default_server;
	#
	# Note: You should disable gzip for SSL traffic.
	# See: https://bugs.debian.org/773332
	#
	# Read up on ssl_ciphers to ensure a secure configuration.
	# See: https://bugs.debian.org/765782
	#
	# Self signed certs generated by the ssl-cert package
	# Don't use them in a production server!
	#
	# include snippets/snakeoil.conf;

	root /var/www/html;

	# Add index.php to the list if you are using PHP
	index index.html index.htm index.nginx-debian.html;

	server_name ipython.domain.tld subdomain1.domain.tld subdomain2.domain.tld;

	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to displaying a 404.
		try_files $uri $uri/ =404;
	}

	# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
	#
	#location ~ \.php$ {
	#	include snippets/fastcgi-php.conf;
	#
	#	# With php7.0-cgi alone:
	#	fastcgi_pass 127.0.0.1:9000;
	#	# With php7.0-fpm:
	#	fastcgi_pass unix:/run/php/php7.0-fpm.sock;
	#}

	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	#
	#location ~ /\.ht {
	#	deny all;
	#}

	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/ipython.domain.tld/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/ipython.domain.tld/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot




	if ($scheme != "https") {
		return 301 https://$host$request_uri;
	} # managed by Certbot


	# Redirect non-https traffic to https
	# if ($scheme != "https") {
	# 	return 301 https://$host$request_uri;
	# } # managed by Certbot

}

This is the block I added below after removing ipython.domain.tld from the original server config:

server {
	listen 80;
	listen [::]:80;
	server_name ipython.domain.tld;

	error_log /var/log/nginx/jupyter_error.log;
	access_log /var/log/nginx/jupyter_access.log;

	location / {
		proxy_pass  http://127.0.0.1:8888;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}

	location ~* /(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? {
		proxy_pass http://127.0.0.1:8888;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		# WebSocket support
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
	}

	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/ipython.domain.tld/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/ipython.domain.tld/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

	if ($scheme != "https") {
		return 301 https://$host$request_uri;
	} # managed by Certbot

}

The location / block forwards all incoming connections on the ipython.domain.tld domain to 127.0.0.1:8888 and adjusts the headers so that jupyter sees the packets as originating from the client instead of 127.0.0.1. The location ~* /(api/kernels/[^/]+/(channels|iopub|shell|stdin)|terminals/websocket)/? block ensures that the websockets created by jupyter to communicate with the client browser get forwarded to the container.

Once these changes are made, restart NGINX

sudo systemctl reload nginx

Now start a CPU only tensorflow docker instance. I'm using this tensorflow container because it comes preinstalled with many of the packages I need.

docker run --name jupyter -it -p 8888:8888 gcr.io/tensorflow/tensorflow

Note down the token and enter it at https://ipython.domain.tld to access the Jupyter workspace

Enable password for Jupyter

open a terminal from jupyter and set a password.

jupyter notebook password

Kill the docker container by pressing CTRL-C. Launch it again in the background.

docker start jupyter

Open https://ipython.domain.tld and enter the password to access the workspace.

Running TapChat

TapChat is the only free IRC bouncer with an android app and working push notifications I could find. I currently have a paid IRCcloud subscription, but I would like to have a free self-hosted alternative for when my subscription expires. The trouble with tapchat is that it generates its own self-signed SSL certs. It took me a while to figure out how I can get it to work with NGINX.

Set up and launch the container

Original instructions

Create a named volume to persist data.

docker volume create --name tapchat-data

Pull the latest tapchat docker image.

docker pull csmith/tapchat:latest

Start tapchat.

docker run -d --name tapchat --restart always -p 8067:8067 -v tapchat-data:/.tapchat csmith/tapchat:latest

Copy tapchat CA outside the container

The CA used by tapchat is needed by NGINX. To do this, simply by attach to the container, cat the .pem file and paste that to a file outside the container.

docker exec -i -t tapchat /bin/bash
cd .tapchat
cat tapchat.pem
exit

Copy the contents of this file to /etc/ssl/certs/tapchat.pem

Finally, get the hash of the pem and add a symlink to it in /etc/ssl/certs/

openssl x509 -noout -hash -in /etc/ssl/certs/tapchat.pem
# use the hash from the previous command in place of "deadbeef" below
ln -s /etc/ssl/certs/tapchat.pem /etc/ssl/certs/deadbeef.0

Use NGINX as a reverse proxy

Add the following server block to the existing config:

server {
	listen 80;
	listen [::]:80;

	server_name irc.domain.tld;

	error_log /var/log/nginx/tapchat_error.log;
	access_log /var/log/nginx/tapchat_access.log;

	location / {
		proxy_pass  https://127.0.0.1:8067;
		proxy_set_header Host $host;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		
		# SSL upstream
		proxy_ssl_trusted_certificate /etc/ssl/certs/tapchat.pem;
		proxy_ssl_verify off;
		
		# WebSocket support
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
	}

	listen 443 ssl; # managed by Certbot
	ssl_certificate /etc/letsencrypt/live/irc.domain.tld/fullchain.pem; # managed by Certbot
	ssl_certificate_key /etc/letsencrypt/live/irc.domain.tld/privkey.pem; # managed by Certbot
	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

	if ($scheme != "https") {
		return 301 https://$host$request_uri;
	} # managed by Certbot

}

Now restart NGINX and open https://irc.domain.tld to access your tapchat server.

2021

Back to Top ↑

2020

My worst one-liner yet

5 minute read

I have a habit of writing excessively long bash one-liners well beyond the threshold of it making more sense to write a script. Chaining commands and transfo...

Back to Top ↑

2019

Back to Top ↑

2018

Home setup part 3: IWS

5 minute read

I have a strange list of requirements, and a limited amount of hardware to satisfy them with. I needed: a Windows desktop for windows only software and ga...

Home setup part 2: The Matrix

3 minute read

A couple of weeks after moving into my apartment, I got a 100Mbps connection from Dsouza cable network, some local ISP that I had never heard of before. At f...

Home setup part 1: The Oasis

3 minute read

For the lat couple of months, I’ve been spending my weekends setting up my home PC, network and other infrastructure. Over this series of blog posts, I will ...

Back to Top ↑

2017

Back to Top ↑

2016

Headless access on Pine64

1 minute read

Quickly setting up headless access on linux SBCs like the pine64 This is a quick guide to enabling headless VNC access on the pine64 using USB serial.

My github blag

less than 1 minute read

My Github Blag I’ll mostly be posting how-tos on things that took me a long time to figure out, in case I need to do them again

Back to Top ↑