Inital Setup

I presume the use of `vim` in a few of these commands. If you are more comfortable using `nano` for editing text, please be sure to make the appropriate substitutions.

On a brand new server… (Change the top line to be your name)

export USER_NAME=mathematicalmichael
sudo apt update -y && sudo apt upgrade -y
sudo apt install vim htop make -y
useradd $USER_NAME -m -s /bin/bash
passwd $USER_NAME
usermod -aG sudo $USER_NAME

Now set your password. We then set privileges and switch to this new user (and new shell, which should appear in color!).


Here are some aliases I like to have in my ~/.bash_aliases file:

vim ~/.bash_aliases

# show users logged into the hub who have open sessions.
alias dpsu='docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}" -f "name=r*user*"'
# both of these show/filter docker processes with prettier formatting. 
alias dps='docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}"'

alias ..='cd ../'                         # Move up 1 directory
alias ...='cd ../../'                     # Move up 2 directories
alias ....='cd ../../../'                 # Move up 3 directories
alias clc='clear'
alias ll='ls -ltr' ## see list of files in reverse chronological order.
alias la='ls -A' ## show hidden files (can add a/A to the above too... preference)

# completion from history. VERY USEFUL
alias cic='set completion-ignore-case On'
alias cico='set completion-ignore-case Off'
alias rr='source ~/.bashrc' # refresh environment
alias erc='vim ~/.bash_aliases' # shortcut to edit my shortcuts.

# don't put duplicate lines or lines starting with space in the history.
# See bash(1) for more options

# append to the history file, don't overwrite it
shopt -s histappend
# the following bind up/down to history search
bind '"\e[A": history-search-backward'
bind '"\e[B": history-search-forward'
if ! shopt -oq posix; then
  if [ -f /usr/share/bash-completion/bash_completion ]; then
    . /usr/share/bash-completion/bash_completion
  elif [ -f /etc/bash_completion ]; then
    . /etc/bash_completion

Then run source ~/.bashrc and you’ll have a nice environment.

Docker Install

If you already had an account with sudo privileges, then you can start from here assuming you are logged in as such. The code that follows installs Docker, and runs a little test by checking the version to make sure everything worked.

sudo apt install apt-transport-https ca-certificates curl software-properties-common

(You will be prompted for your password)

curl -fsSL | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] bionic stable"
sudo apt update
apt-cache policy docker-ce
sudo apt install docker-ce -y
docker --version 


sudo usermod -aG docker root
sudo usermod -aG docker ${USER}
sudo curl -L$DOCKER_COMPOSE_VERSION/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version


SSH Keys

Now is a good time to reboot the machine since we just updated and installed a bunch of things. Before we do that, let us make sure we can ssh in with either of the usernames now. Hit enter at the prompt. YOUR COMPUTER WILL REBOOT AFTER

sudo cat /root/.ssh/authorized_keys > ~/.ssh/authorized_keys
Your computer/server might take a minute or two to reboot.

After restarting ssh in as We will take care of the proxy and security set up first (in order to complete the instructions that only need to be performed initially).


Here is how you do it without a container (ideally we could containerize this whole aspect as well into our docker-compose.yml file, but that is not yet the case). Run all this first.


sudo apt-get install software-properties-common
sudo add-apt-repository universe
sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install certbot 
sudo certbot certonly

Follow prompts, enter info for your site. This creates a public/private key pair with a certificate authority so that browsers can trust the connection is valid.

If you are behind a private network (`.pvt` or any other non-public domain), then you must acquire this certificate and key from another (presumably the school's) certificate authority. Make sure the paths are set properly in the configuration to reflect where you placed these keys. Furthermore, you may have to comment out the `ssl-ciphers` line (we had to at CU Denver to make it work, but this isn't required on public domains).

The keys generated will by default be in /etc/ssl/live/, which you will see reflected in the example configuration below (set for

Then run the following,

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096

This creates a long encryption key that we will pass to nginx (our proxy) in the next step for additional security.


sudo apt-get update
sudo apt install nginx

Nginx Config

To secure our connection, we will create the following file in /etc/nginx/sites-enabled/:

You may need to remove default.conf, or edit it directly to match the following instead.

If you choose to edit `default.conf`, make sure to add the following line to override the default nginx settings, which will prevent you from saving notebooks greater than 1MB in size: `client_max_body_size 20m;` (see example below).

Run sudo vim /etc/nginx/sites-enabled/hub.conf and edit the file:

# top-level http config for websocket headers
# If Upgrade is defined, Connection = upgrade
# If Upgrade is empty, Connection = close
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;

# HTTP server to redirect all 80 traffic to SSL/HTTPS
server {
    listen 80;

    # Tell all requests to port 80 to be 302 redirected to HTTPS
    return 302 https://$host$request_uri;

# HTTPS server to handle JupyterHub
server {
    listen 443;
    ssl on;

     ssl_certificate /etc/letsencrypt/live/;
     ssl_certificate_key /etc/letsencrypt/live/;

    # memory limits need to be upped for notebooks. default of 1mb is insufficient. 
    client_max_body_size 20m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security max-age=15768000;

    # Managing literal requests to the JupyterHub front end
    location /math {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # websocket headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

    location /math8660 {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # websocket headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

    # Managing requests to verify letsencrypt host
    location ~ /.well-known {
        allow all;

That last section starting with location is what we’ll be tweaking if we want to host this at or or whatever we want. (Although in the case of a directory /suffix/ format, we will have to make sure the line from the that we commented out above matches this configuration file.

The assumption (by default) is that the `$HUB_NAME` is used as the suffix. `` is the default `location` we will have to set. The idea here is that we can set up multiple hubs simply by changing the `.env` file to choose a new name and port number.

As long as we point to the correct port, nginx (our proxy) will handle the rest. The / refers to the fact that we want the hub to be hosted at (with nothing appended/prepended).

If you want some kind of homepage instead of a Hub login, you can configure that to be the case with the following:

location / {
    root /var/www/public_html;

Restart nginx for changes to take effect:

service nginx restart

You can now proceed to adding a hub in the Hub Setup documentation.