# Setting Up Jupyter Lab on Digital Ocean Droplet

**URL:** <https://discourse.jupyter.org/t/setting-up-jupyter-lab-on-digital-ocean-droplet/37951>\
**Category:** JupyterLab\
**Tags:** jupyterlab\
**Created:** [September 25, 2025, 2:43pm UTC](https://discourse.jupyter.org/t/setting-up-jupyter-lab-on-digital-ocean-droplet/37951 "2025-09-25T14:43:53Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Ethan1](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ethan1/32/20367_2.png) [@Ethan1](https://discourse.jupyter.org/u/Ethan1)\
**Post date:** [September 25, 2025, 2:43pm UTC](https://discourse.jupyter.org/t/setting-up-jupyter-lab-on-digital-ocean-droplet/37951/1 "2025-09-25T14:43:53Z")

</div>

| Timestamp | Notes |
| --- | --- |
| [0:38](https://youtu.be/lxv_alNU7GY?t=38) | Deployed the LTS option Ubuntu 24.04 x64 |
| [2:43](https://youtu.be/lxv_alNU7GY?t=163) | Enabled the Uncomplicated Firewall before creating an A record |
| [4:03](https://youtu.be/lxv_alNU7GY?t=243) | created A record |
| | login as `root` |
| [4:37](https://youtu.be/lxv_alNU7GY?t=277) | ran `sudo adduser --disabled-password --gecos "" jupyter` |
| [4:41](https://youtu.be/lxv_alNU7GY?t=281) | ran `sudo usermod -aG sudo jupyter` |
| [4:46](https://youtu.be/lxv_alNU7GY?t=286) | ran `apt update` |
| [4:59](https://youtu.be/lxv_alNU7GY?t=299) | ran `apt install -y python3-venv python3-pip nginx ufw` |
| [5:38](https://youtu.be/lxv_alNU7GY?t=338) | ran `-iu jupyter` which failed |
| [5:53](https://youtu.be/lxv_alNU7GY?t=353) | ran `sudo -iu jupyter` which worked |
| [5:59](https://youtu.be/lxv_alNU7GY?t=359) | ran `python3 -m venv ~/jlab-venv` |
| [6:08](https://youtu.be/lxv_alNU7GY?t=368) | ran `~/jlab-venv/bin/pip install --upgrade pip` |
| [6:17](https://youtu.be/lxv_alNU7GY?t=377) | ran `~/jlab-venv/bin/pip install jupyterlab jupyterlab-git` |
| [6:49](https://youtu.be/lxv_alNU7GY?t=409) | ran `~/jlab-venv/bin/jupyter server --generate-config` |
| [7:01](https://youtu.be/lxv_alNU7GY?t=421) | ran `python3 -c "from jupyter_server.auth import passwd; print(passwd())"` which failed |
| [7:44](https://youtu.be/lxv_alNU7GY?t=464) | ran `~/jlab-venv/bin/python -c "from jupyter_server.auth import passwd; print(passwd())"` |
| [7:46](https://youtu.be/lxv_alNU7GY?t=466) | entered the password that will be used to login to Jupyter Lab webpage |
| [12:20](https://youtu.be/lxv_alNU7GY?t=740) | used PuttyGen to get OpenSSH key from the `.ppk` |

[17:13](https://youtu.be/lxv_alNU7GY?t=1033)

in `cmd` i ran `scp -i C:\Users\ppyem\Documents\id_rsa root@134.209.30.67:/home/jupyter/.jupyter/jupyter_server_config.py C:\Users\ppyem\Documents\ ` to get the full configuration file locally

| Timestamp | Notes |
| --- | --- |
| [18:57](https://youtu.be/lxv_alNU7GY?t=1137) | i ran `nano ~/.jupyter/jupyter_server_config.py` |

[20:09](https://youtu.be/lxv_alNU7GY?t=1209)

i appended to bootom

```python
c.ServerApp.ip = "127.0.0.1"
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
c.ServerApp.allow_remote_access = True
c.ServerApp.trust_xheaders = True

# require login everywhere
c.ServerApp.allow_unauthenticated_access = False

# authentication
c.ServerApp.token = ""
c.ServerApp.password = "argon2:$argon2id$v=19$m=10240,t=10,p=8$XOEmNWiKRa+XjdjRYROfPg$ob4eQEU3/Cp+uL9dfT6SF9XHMulApkh6y/fELz3C61A"

```

| Timestamp | Notes |
| --- | --- |
| [20:29](https://youtu.be/lxv_alNU7GY?t=1229) | i ran `nano ~/.jupyter/jupyter_server_config.py` |
| [20:30](https://youtu.be/lxv_alNU7GY?t=1230) | i ran `ls -l ~/.jupyter/jupyter_server_config.py` |
| [20:52](https://youtu.be/lxv_alNU7GY?t=1252) | i ran `chmod 600 /home/jupyter/.jupyter/jupyter_server_config.py` |
| [20:59](https://youtu.be/lxv_alNU7GY?t=1259) | i ran `sudo systemctl restart jupyterlab` which failed becuase there’s no such password to run this command |
| [23:56](https://youtu.be/lxv_alNU7GY?t=1436) | ran ` exit` to change user from `jupyter` to `root` |

[24:29](https://youtu.be/lxv_alNU7GY?t=1469)

ran

```python
printf 'Cmnd_Alias JLABCMDS = /bin/systemctl restart jupyterlab, /bin/systemctl status jupyterlab, /bin/systemctl stop jupyterlab, /bin/systemctl start jupyterlab\njupyter ALL=(root) NOPASSWD: JLABCMDS\n' > /etc/sudoers.d/jupyter-jupyterlab
chmod 440 /etc/sudoers.d/jupyter-jupyterlab

```

| Timestamp | Notes |
| --- | --- |
| [24:37](https://youtu.be/lxv_alNU7GY?t=1477) | ran `ls -l /etc/sudoers.d/jupyter-jupyterlab` |
| [24:43](https://youtu.be/lxv_alNU7GY?t=1483) | ran `cat /etc/sudoers.d/jupyter-jupyterlab` |
| [25:10](https://youtu.be/lxv_alNU7GY?t=1510) | ran `exit` |
| | can’t login to user `jupyter` via PuTTy |
| [26:37](https://youtu.be/lxv_alNU7GY?t=1597) | logged in as `root` |
| [26:52](https://youtu.be/lxv_alNU7GY?t=1612) | ran `install -d -m 700 -o jupyter -g jupyter /home/jupyter/.ssh` |
| [26:56](https://youtu.be/lxv_alNU7GY?t=1616) | ran `cp /root/.ssh/authorized_keys /home/jupyter/.ssh/authorized_keys` |
| [27:02](https://youtu.be/lxv_alNU7GY?t=1622) | ran `chown jupyter:jupyter /home/jupyter/.ssh/authorized_keys` |
| [27:07](https://youtu.be/lxv_alNU7GY?t=1627) | ran `chmod 600 /home/jupyter/.ssh/authorized_keys` |
| [27:13](https://youtu.be/lxv_alNU7GY?t=1633) | ran `ls -l /home/jupyter/.ssh/authorized_keys` |
| [27:25](https://youtu.be/lxv_alNU7GY?t=1645) | logged out |
| [27:58](https://youtu.be/lxv_alNU7GY?t=1678) | logged in via SSH key as `jupyter` |
| [28:05](https://youtu.be/lxv_alNU7GY?t=1685) | ran `sudo systemctl restart jupyterlab` which failed due to non-existant service unit |
| [28:35](https://youtu.be/lxv_alNU7GY?t=1715) | ran `sudo tee /etc/systemd/system/jupyterlab.service >/dev/null <<'EOF'` |

```auto
[Unit]
Description=JupyterLab (proxied by Nginx)
After=network.target

[Service]
Type=simple
User=jupyter
Group=jupyter
WorkingDirectory=/home/jupyter
Environment="PATH=/home/jupyter/jlab-venv/bin:/usr/bin"
ExecStart=/home/jupyter/jlab-venv/bin/jupyter lab
Restart=on-failure
RestartSec=5

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF

```

| Timestamp | Notes |
| --- | --- |
| [32:33](https://youtu.be/lxv_alNU7GY?t=1953) | ran `systemctl daemon-reload` |
| [32:37](https://youtu.be/lxv_alNU7GY?t=1957) | ran `systemctl enable --now jupyterlab` |
| [32:43](https://youtu.be/lxv_alNU7GY?t=1963) | ran `systemctl status jupyterlab --no-pager` |
| [33:08](https://youtu.be/lxv_alNU7GY?t=1988) | ran `journalctl -u jupyterlab -e --no-pager` |
| [33:12](https://youtu.be/lxv_alNU7GY?t=1992) | ran `sudo -u jupyter /home/jupyter/jlab-venv/bin/jupyter --version` |
| [33:43](https://youtu.be/lxv_alNU7GY?t=2023) | ran `nano /etc/systemd/system/jupyterlab.service` |
| [33:53](https://youtu.be/lxv_alNU7GY?t=2033) | replace a line to `ExecStart=/home/jupyter/jlab-venv/bin/python3 -m jupyterlab --no-browser --config=/home/jupyter/.jupyter/jupyter_server_config.py` |
| [34:08](https://youtu.be/lxv_alNU7GY?t=2048) | ran `systemctl daemon-reload` |
| [34:13](https://youtu.be/lxv_alNU7GY?t=2053) | `systemctl restart jupyterlab` |
| [34:25](https://youtu.be/lxv_alNU7GY?t=2065) | ran `journalctl -u jupyterlab -e --no-pager` |
| [35:17](https://youtu.be/lxv_alNU7GY?t=2117) | ran `nano /etc/systemd/system/jupyterlab.service` |
| [35:27](https://youtu.be/lxv_alNU7GY?t=2127) | grey-ed out `ProtectHome` |
| [35:45](https://youtu.be/lxv_alNU7GY?t=2145) | ran `systemctl daemon-reload` |
| [35:49](https://youtu.be/lxv_alNU7GY?t=2149) | ran `systemctl restart jupyterlab` |
| [35:54](https://youtu.be/lxv_alNU7GY?t=2154) | ran `systemctl status jupyterlab --no-pager` |
| [36:24](https://youtu.be/lxv_alNU7GY?t=2184) | ran `apt update` |
| [36:34](https://youtu.be/lxv_alNU7GY?t=2194) | ran `apt install -y nginx certbot python3-certbot-nginx` |
| [36:45](https://youtu.be/lxv_alNU7GY?t=2205) | ran `ufw allow OpenSSH` |
| [36:52](https://youtu.be/lxv_alNU7GY?t=2212) | ran `ufw allow 'Nginx Full'` |
| [36:57](https://youtu.be/lxv_alNU7GY?t=2217) | ran `ufw enable` |
| | new putty session on `root` user |
| [37:36](https://youtu.be/lxv_alNU7GY?t=2256) | ran `tee /etc/nginx/sites-available/jupyterlab >/dev/null <<'EOF'` |
| | same as `nano /etc/nginx/sites-available/jupyterlab` |

```auto
# Simple rate limit to protect /login from bots
limit_req_zone $binary_remote_addr zone=jrate:10m rate=10r/s;

server {
    listen 80;
    server_name lab.example.com;

    # Security headers (added on HTTP too; Certbot will keep them on HTTPS)
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header Referrer-Policy strict-origin-when-cross-origin;

    location / {
        limit_req zone=jrate burst=20 nodelay;

        proxy_pass http://127.0.0.1:8888;
        proxy_http_version 1.1;

        # Websocket + proxy headers Jupyter needs
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;

        client_max_body_size 64m;
        proxy_read_timeout 600;
        proxy_send_timeout 600;
    }
}

```

| Timestamp | Notes |
| --- | --- |
| [40:21](https://youtu.be/lxv_alNU7GY?t=2421) | ran `ln -s /etc/nginx/sites-available/jupyterlab /etc/nginx/sites-enabled/jupyterlab` |
| 40:29 | ran `nginx -t && systemctl reload nginx` |
| [40:40](https://youtu.be/lxv_alNU7GY?t=2440) | ran `certbot --nginx -d lab.example.com --redirect -m you@example.com --agree-tos -n` |
| [41:25](https://youtu.be/lxv_alNU7GY?t=2485) | ran `certbot renew --dry-run` |
| [49:25](https://youtu.be/lxv_alNU7GY?t=2965) | i show Jupyter Lab up and running with Let’s Encrypt HTTPS |

---

<div class="post-metadata">

**Author:** ![Ethan1](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.jupyter.org/ethan1/32/20367_2.png) [@Ethan1](https://discourse.jupyter.org/u/Ethan1)\
**Post date:** [September 28, 2025, 11:38am UTC](https://discourse.jupyter.org/t/setting-up-jupyter-lab-on-digital-ocean-droplet/37951/2 "2025-09-28T11:38:55Z")

</div>

Here’s a new (untested) set of commands that might lead to a more secure outcome;

1. Deployed the LTS option Ubuntu 24.04 x64
2. `ssh root@<your_server_ip>`
3. `ufw allow http`
4. `ufw allow https`
5. `ufw allow ssh`
6. `ufw enable`
7. `adduser --disabled-password --gecos "" jupyter`
8. `usermod -aG sudo jupyter`
9. `su - jupyter`
10. `sudo apt update && sudo apt upgrade -y`
11. `exit`
12. `passwd jupyter`
13. `su - jupyter`
14. `sudo apt update && sudo apt upgrade -y`
15. `sudo apt install -y python3-pip python3-venv`
16. `python3 -m venv ~/jupyterlab-env`
17. `source ~/jupyterlab-env/bin/activate`
18. `pip install --upgrade pip jupyterlab`
19. `jupyter lab --generate-config`
20. `jupyter lab password`

> [@](#):
>
> `nano ~/.jupyter/jupyter_lab_config.py`
> 
> then add anywhere  
> 36.
> 
> ```auto
> c.ServerApp.ip = "0.0.0.0"
> c.ServerApp.port = 8888
> c.ServerApp.open_browser = False
> 
> ```

1. `sudo nano /etc/systemd/system/jupyterlab.service`

```auto
[Unit]
Description=JupyterLab
After=network.target

[Service]
Type=simple
User=jupyter
WorkingDirectory=/home/jupyter
Environment="PATH=/home/jupyter/jupyterlab-env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/home/jupyter/jupyterlab-env/bin/jupyter lab --no-browser --ip=127.0.0.1 --port=8888
Restart=on-failure

[Install]
WantedBy=multi-user.target

```

1. `sudo systemctl daemon-reload`
2. `sudo systemctl enable jupyterlab`
3. `sudo systemctl start jupyterlab`
4. `sudo systemctl status jupyterlab`
5. `sudo apt install -y nginx`
6. `sudo nano /etc/nginx/sites-available/jupyterlab`

```auto
server {
    listen 80;
    server_name lab.year2physics.site;

    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;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Increase the buffer size for large notebooks
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

```

1. `sudo ln -s /etc/nginx/sites-available/jupyterlab /etc/nginx/sites-enabled/`
2. `sudo nginx -t`
3. `sudo systemctl reload nginx`
4. `sudo apt install -y certbot python3-certbot-nginx`
5. `sudo certbot --nginx -d lab.year2physics.site`
6. `sudo nano /etc/nginx/sites-available/jupyterlab`
7. you may need to replace your config so Nginx preserves `/lab` when proxying:

```auto
server {
    listen 80;
    server_name lab.year2physics.site;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name lab.year2physics.site;

    ssl_certificate /etc/letsencrypt/live/lab.year2physics.site/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/lab.year2physics.site/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    client_max_body_size 64M;

    # redirect root to /lab
    location = / {
        return 302 /lab;
    }

    # proxy all /lab routes
    location /lab/ {
        proxy_pass http://127.0.0.1:8888/lab/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

```

1. run `sudo nano /home/jupyter/.jupyter/jupyter_server_config.json`
2. then replace to, merging your password hash

```auto
{
  "IdentityProvider": {
    "hashed_password": "argon2:$argon2id$v=19$m=10240,t=10,p=8$Kucdq4m2c8qoTBbjRsP6Ig$rXB/oS24X2nWWlbGPYbxbQzoQAvlQj..."
  },
  "ServerApp": {
    "ip": "127.0.0.1",
    "port": 8888,
    "open_browser": false,
    "allow_remote_access": true,
    "trust_xheaders": true,
    "token": "",
    "base_url": "/lab",
    "allow_origin": "https://lab.year2physics.site",
    "allow_credentials": true
  }
}

```

1. run `sudo systemctl restart jupyterlab`
2. shoul;d be working now
