Using remote Jupyter Notebooks on iPad

I do the majority of my coding on a remote server which houses the data and virtual environments I use. When I’m working on my laptop, I’ll open a remote session through the VSCodium extension Open Remote - SSH by GitHub user jeanp413 which has all the niceties and tools I’m used to, like GitHub Copilot. However, when traveling for conferences, workshops, etc., I can find it cumbersome to lug my laptop around. So, I found a way to remotely edit and run my code using my iPad through Jupyter Notebooks. It isn’t the most comfortable setup and I don’t have access to all the tools I’m used to, but it only requires installing one free app, Termius, a “modern SSH client designed for productivity and collaboration.”.
Here’s an overview of the setup:
- Open a connection to the remote server
- Launch a remote Jupyter session
- Port forwarding
- Open the Jupyter session locally

1. Open a connection to the remote server
Open the iPad Termius app. If this is the first time connecting to the remote server with Termius, create a new host connection. (see Connect via SSH with an SSH Key). In the “Hosts” tab, tap the “+” and “New Host.” Most of the options can be left as default, but here are the settings I change:
- Label: Give it a short, descriptive label to distinguish this host from others you may make
- Ex:
my-server
- Ex:
- IP or Hostname: The address of the remote server (i.e., what comes after the
@
symbol when logging in via a regular terminal)- Ex:
my-server.host.org
- Ex:
- Username: Your username on the remote server (i.e., what comes before the
@
symbol when logging in via a regular terminal)- Ex:
my-user
- Ex:
- Password: Optional
- Key: Generate a new ssh key, as you would to connect to the remote server on any other client. See Generating a new SSH key and adding it to the ssh-agent for an example of how to do this with GitHub’s servers
- Ex:
my-server-key
- Ex:
- Theme: I use Termius Dark
Open a connection to the remote host. Assuming the connection is made, you should be greeted with an ssh terminal in your home directory. Next, navigate to the project directory (for example, ~/my-project
) and activate the virtual environment which, here, I’ll refer to as my-env
.
2. Launch a remote Jupyter session
With a conda environment activated (conda activate my-env
), make sure the required packages for Jupyter are installed. I use miniconda on my remote server, but with any conda environment, the installed packages can be displayed by running conda list
. If you are using a virtualenv, you can use pip list
. Make sure you see the Jupyter-related packages listed in the output. If you don’t, you’ll need to install Jupyter.
my-user@my-server:~/my-project$ conda activate my-env
(my-env) my-user@my-server:~/my-project$ conda list
# packages in environment at /home/my-user/miniconda3/envs/my-env:
#
# Name Version Build Channel
...
jupyter 1.1.1 pypi_0 pypi
jupyter-cache 1.0.1 pypi_0 pypi
jupyter-client 8.6.3 pypi_0 pypi
jupyter-console 6.6.3 pypi_0 pypi
jupyter-core 5.8.1 pypi_0 pypi
jupyter-events 0.12.0 pypi_0 pypi
jupyter-lsp 2.2.5 pypi_0 pypi
jupyter-server 2.16.0 pypi_0 pypi
jupyter-server-terminals 0.5.3 pypi_0 pypi
jupyterlab 4.4.3 pypi_0 pypi
jupyterlab-pygments 0.3.0 pypi_0 pypi
jupyterlab-server 2.27.3 pypi_0 pypi
jupyterlab-widgets 3.0.15 pypi_0 pypi
...
(my-env) my-user@my-server:~$
Next, launch a Jupyter notebook session using the --no-browser
argument. This will avoid Jupyter notebook trying to launch the default browser application. On the remote server I use, there is no default browser, so this command will fail if that argument is not included.
Here’s an example of the output:
(my-env) my-user@my-server:~/my-project$ jupyter-notebook --no-browser
[I 2025-08-15 17:30:23.977 ServerApp] jupyter_lsp | extension was successfully linked.
[I 2025-08-15 17:30:23.984 ServerApp] jupyter_server_terminals | extension was successfully linked.
[I 2025-08-15 17:30:23.991 ServerApp] jupyterlab | extension was successfully linked.
[I 2025-08-15 17:30:23.998 ServerApp] notebook | extension was successfully linked.
[I 2025-08-15 17:30:25.010 ServerApp] notebook_shim | extension was successfully linked.
[I 2025-08-15 17:30:25.226 ServerApp] notebook_shim | extension was successfully loaded.
[I 2025-08-15 17:30:25.230 ServerApp] jupyter_lsp | extension was successfully loaded.
[I 2025-08-15 17:30:25.232 ServerApp] jupyter_server_terminals | extension was successfully loaded.
[I 2025-08-15 17:30:25.243 LabApp] JupyterLab extension loaded from /home/my-user/miniconda3/envs/uplt/lib/python3.9/site-packages/jupyterlab
[I 2025-08-15 17:30:25.243 LabApp] JupyterLab application directory is /home/my-user/miniconda3/envs/uplt/share/jupyter/lab
[I 2025-08-15 17:30:25.244 LabApp] Extension Manager is 'pypi'.
[I 2025-08-15 17:30:25.411 ServerApp] jupyterlab | extension was successfully loaded.
[I 2025-08-15 17:30:25.427 ServerApp] notebook | extension was successfully loaded.
[I 2025-08-15 17:30:25.428 ServerApp] The port 8888 is already in use, trying another port.
[I 2025-08-15 17:30:25.428 ServerApp] The port 8889 is already in use, trying another port.
[I 2025-08-15 17:30:25.429 ServerApp] The port 8890 is already in use, trying another port.
[I 2025-08-15 17:30:25.429 ServerApp] Serving notebooks from local directory: /home/my-user/unox
[I 2025-08-15 17:30:25.429 ServerApp] Jupyter Server 2.16.0 is running at:
[I 2025-08-15 17:30:25.429 ServerApp] http://localhost:8891/tree?token=59de7262c7e27bbf20ee1f04ae2598dc691cac294fc9bda5
[I 2025-08-15 17:30:25.429 ServerApp] http://127.0.0.1:8891/tree?token=59de7262c7e27bbf20ee1f04ae2598dc691cac294fc9bda5
[I 2025-08-15 17:30:25.429 ServerApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 2025-08-15 17:30:25.511 ServerApp]
To access the server, open this file in a browser:
file:///home/my-user/.local/share/jupyter/runtime/jpserver-3702276-open.html
Or copy and paste one of these URLs:
http://localhost:8891/tree?token=<token>
http://127.0.0.1:8891/tree?token=<token>
[I 2025-08-15 17:30:25.828 ServerApp] Skipped non-installed server(s): bash-language-server, dockerfile-language-server-nodejs, javascript-typescript-langserver, jedi-language-server, julia-language-server, pyright, python-language-server, python-lsp-server, r-languageserver, sql-language-server, texlab, typescript-language-server, unified-language-server, vscode-css-languageserver-bin, vscode-html-languageserver-bin, vscode-json-languageserver-bin, yaml-language-server
Where <token>
would be a several-dozen character string of letters and numbers.
Once the Jupyter server has been launched, this terminal instance will be occupied. Do not close it until you are done with your session. I’ll show how to open a new terminal window within JupyterLab later in case you need to run more commands on the remote server.
3. Port forwarding
At this point, the Jupyter session is running on the remote host, on a particular port number. In the output of launching the Jupyter server, this will be the four-digit number after http://localhost:
, in this case, 8891
. Port forwarding is what will allow the local device, the iPad, to access this Jupyter session.
Note, the default port for this is 8888
, however, if that port is occupied, Jupyter will increment the number until it finds an open one. I share this server with several other active users, so I will often see the port number increment several times before it finds one unoccupied. While I only needed to set up the connection to the host server in Termius once, I find I usually need to set up a new port forwarding rule every time I start a session.
In the Termius app, open the “Port Forwarding” tab, tap the “+”, and skip the setup wizard. This will bring up the “Create Rule” window. Fill in the following information:
- Label: Give it a short, descriptive label to distinguish this rule. I usually write the host label followed by the port number
- Ex:
my-server 8891
- Ex:
- Local port: Unless I know that I’m using particular ports on my iPad for other things, I always set this to the same as the “Destination port number”
- Ex:
8891
- Ex:
- Bind address: Leave blank
- Intermediate host: Select the host on which you launched the Jupyter session
- Ex:
my-user@my-server.host.org:22
- Ex:
- Destination address:
localhost
- Always
localhost
- Always
- Destination port: The four-digit port number noted earlier
- Ex:
8891
- Ex:

4. Open the Jupyter session locally
Once the port forwarding has been set up, the next step is to open that forwarded port in the browser. In the output of launching the Jupyter session, find the URL which matches this pattern:
http://localhost:8891/tree?token=<token>
Where, again, <token>
will be a several-dozen character string of letters and numbers.
Copy it, and paste it into a browser tab (I use Firefox on my iPad) where the token will disappear once the page loads. This opens a Jupyter Notebook window which displays a list of all the files and directories that are within the project directory in which the Jupyter session was launched.
From here, you can open and run code in any Jupyter Notebook within this directory. However, I prefer to use JupyterLab, where you can easily open multiple tabs of different notebooks and tile the workspace to see the file explorer and even a terminal window at the same time. Under the “View” drop-down, click “Open JupyterLab” which will open a new browser tab.
Once JupyterLab is open, you can close the Jupyter Notebook tab.
In JupyterLab, you can open a new tab by either double-tapping a file in the explorer sidebar, or by pressing the “+” icon in the top of the main window. By doing the latter, a “Launcher” tab appears with options to open different types of blank tabs. I usually open a terminal tab to run scripts and move files around.
A new terminal tab will default to the directory from which you launched the Jupyter Notebook in Termius. With multiple tabs open, you can tile the workspace by dragging the tabs around.
Screen real estate is limited on the iPad, however I often find it helpful to have multiple tabs open side-by-side when I am coding.
With this set up, I can get into my usual workflow of editing code, running it, and viewing the output, all from my iPad.
Conclusion
While the JupyterLab interface on iPad is more cramped than what I am used to in VSCodium, this setup works well enough for certain situations where it is worth the trade-off of being able to leave my computer at home.
Some extensions I am considering adding if I start using this setup more heavily in the future:
- JupyterLab - Git extension, for more easily monitoring my
git
status and branches - Jupyter - Copilot extension, for utilizing the GitHub Copilot autocomplete in Jupyter Notebooks