> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nebius.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying JupyterLab® on a virtual machine with Nebius Tunnels

You can deploy and run JupyterLab® on a non-GPU virtual machine (VM) without a public IP address. Use a [tunnel](/tunnels/overview#how-tunnels-work) in Nebius Tunnels to access JupyterLab from your browser. This way, your VM stays secure and isolated while anyone from the internet can open JupyterLab and connect to the VM via a protective tunnel.

To establish the tunnel, the VM configuration should include a tunnel agent. The agent runs on the VM automatically after the VM starts. When you open JupyterLab in the browser, the tunnel routes the traffic to the agent. Thus, only the IP address of the tunnel is exposed to the internet.

<Note>
  Use one VM per tunnel. Do not add several VMs to a single tunnel.
</Note>

## Costs

Nebius AI Cloud charges you for the following billing items:

* [Compute virtual machine](/compute/resources/pricing#virtual-machines-gpus-vcpus-ram)
* [Boot disk](/compute/resources/pricing#disks) attached to the VM

Nebius Tunnels is [in preview](/overview/services#service-and-application-stages) and provided free of charge.

## Steps

### Create a tunnel

<Tabs group="interfaces">
  <Tab title="Web console">
    1. In the [web console](https://console.nebius.com), select the project where you want to create the infrastructure. All resources must be located in the same project.
    2. Go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/rOlLZ_MFvrheaI-h/_assets/sidebar/networking.svg?fit=max&auto=format&n=rOlLZ_MFvrheaI-h&q=85&s=7a4fe6d935f68131371d7b49a9bd595f" width="16" height="16" data-path="_assets/sidebar/networking.svg" /> **Networking** → **Tunnels**.
    3. Click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/plus.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=7c9efc69d65fc58db0eb73702fd81aa1" width="16" height="16" data-path="_assets/plus.svg" /> **Create tunnel**.
    4. On the page that opens, set the tunnel name to `my-tunnel`.
    5. Click **Create tunnel**.
  </Tab>

  <Tab title="CLI">
    1. [Install and configure](/cli/install) the Nebius AI Cloud CLI.

       When configuring the CLI profile, set the project where you want to create the infrastructure. All resources must be located in the same project.

       All `nebius` commands by default use the [project ID](/iam/manage-projects#how-to-get-a-project-id) from your CLI profile configuration.

    2. Create a tunnel:

       ```bash theme={null}
       nebius tunnel create --title my-tunnel 
       ```
  </Tab>
</Tabs>

### Prepare a service account

To authorize the VM in the tunnel, prepare a service account with the `applicationtunnel.agent` role. Later, you attach this service account to the VM.

1. Create the service account:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. Click **Create resource** → **Service account**.
       3. In the window that opens, set the name to `tunnel-agent-sa` and select the project.
       4. Click **Create and continue**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam service-account create --name tunnel-agent-sa 
       ```
     </Tab>
   </Tabs>

2. Create a [custom group](/iam/authorization/groups/index#custom-groups) in Identity and Access Management to assign the role to the service account:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. Click **Create resource** → **Group**.
       3. In the window that opens, set the group name to `tunnel-agents`. In the **Scope** field, select the project where you create all the resources.
       4. Click **Create**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam group create --name tunnel-agents 
       ```
     </Tab>
   </Tabs>

3. Attach the tunnel and grant the `applicationtunnel.agent` role to this group. You can only grant this role by using the CLI.

   <Tabs group="interfaces">
     <Tab title="CLI">
       1. Get the group ID:

          ```bash theme={null}
          nebius iam group list
          ```

          The ID is specified in the `items.metadata.id` parameter in the output.

       2. Get the tunnel ID:

          ```bash theme={null}
          nebius tunnel list
          ```

       3. Create an access permit in the custom group:

          ```bash theme={null}
          nebius iam access-permit create \
            --parent-id <group_ID> \
            --resource-id <tunnel_ID> \
            --role applicationtunnel.agent
          ```
     </Tab>
   </Tabs>

4. Add the service account to the group:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. Go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/sidebar/administration.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=e6411dc023fd6972922c0a12a59ccf21" width="16" height="16" data-path="_assets/sidebar/administration.svg" /> **Administration** → **IAM**.
       2. On the **Groups** tab, find and open the `tunnel-agents` group.
       3. Click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/plus.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=7c9efc69d65fc58db0eb73702fd81aa1" width="16" height="16" data-path="_assets/plus.svg" /> **Add members**.
       4. In the window that opens, add the `tunnel-agent-sa` service account.
       5. Once the account is added, close the window.

       Check the group page in general. Make sure that:

       * The `tunnel-agent-sa` service account is specified on the **Members** tab.
       * The tunnel and the `applicationtunnel.agent` role are specified on the **Access permits** tab.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius iam group-membership create \
         --parent-id <group_ID> \
         --member-id <service_account_ID>
       ```

       To get the service account ID, run:

       ```bash theme={null}
       nebius iam service-account list
       ```
     </Tab>
   </Tabs>

### Create a virtual machine

Prepare the VM configuration that includes the JupyterLab image and the tunnel agent. Then, create the VM — the image and agent are deployed on the VM automatically.

<Warning>
  If you need to change the VM configuration later, delete the VM and create a new one. A tunnel should be associated with only one VM, and you can't change the cloud-init configuration of an existing VM.
</Warning>

1. [Generate an SSH key pair](/compute/virtual-machines/ssh-keys#generating-a-key-pair).

2. Prepare the cloud-init configuration file. You'll attach it to the VM.

   In this file, specify two parameters:

   * `users.ssh_authorized_keys`: Public SSH key.
   * `write_files.content.tunnel_id`: ID of the tunnel.

   <Accordion title="jupyterlab.yaml">
     ```yaml theme={null}
     users:
       - name: ubuntu
         sudo: "ALL=(ALL) NOPASSWD:ALL"
         shell: /bin/bash
         ssh_authorized_keys:
           - <public_SSH_key>

     # The driverless images don't include Docker, so the configuration does.
     package_update: true
     packages:
       - docker.io

     write_files:
       - path: /etc/nebius-tunnel-agent/config.yaml
         permissions: "0640"
         content: |
           tunnel_id: <tunnel_ID>

           iam:
             metadata:
               enabled: true

           logging:
             level: info
             format: json

           services:
             - name: jupyter
               type: http
               target: 127.0.0.1:8888
             # Preserve the ssh service, so you can get the JupyterLab token
             - name: ssh
               type: tcp
               target: 127.0.0.1:22

       - path: /etc/systemd/system/nebius-tunnel-agent.service
         permissions: "0644"
         content: |
           [Unit]
           Description=Nebius tunnel agent
           Documentation=https://docs.nebius.com/tunnels
           After=network-online.target
           Wants=network-online.target

           [Service]
           Type=simple
           User=tunna
           Group=tunna
           ExecStart=/usr/local/bin/nebius-tunnel-agent --config /etc/nebius-tunnel-agent/config.yaml
           Restart=always
           RestartSec=5
           NoNewPrivileges=true
           ProtectSystem=strict
           ProtectHome=true
           PrivateTmp=true
           ReadOnlyPaths=/etc/nebius-tunnel-agent

           [Install]
           WantedBy=multi-user.target

       - path: /usr/local/sbin/install-nebius-tunnel-agent
         permissions: "0755"
         content: |
           #!/bin/bash
           set -euo pipefail

           VERSION="${AGENT_VERSION:-latest}"
           BASE="https://storage.eu-north1.nebius.cloud/products/releases/nebius-tunnel-agent"
           CONF_DIR=/etc/nebius-tunnel-agent

           case "$(uname -m)" in
             x86_64)        PLATFORM=linux-x86_64 ;;
             aarch64|arm64) PLATFORM=linux-aarch64 ;;
             *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
           esac
           ASSET="nebius-tunnel-agent-${PLATFORM}.tar.gz"

           TMP="$(mktemp -d)"
           trap 'rm -rf "$TMP"' EXIT

           fetch() {
             curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused -o "$2" "$1"
           }
           fetch "$BASE/$VERSION/$ASSET" "$TMP/$ASSET"
           fetch "$BASE/$VERSION/checksums-sha256.txt" "$TMP/checksums-sha256.txt"

           (cd "$TMP" && sha256sum --ignore-missing -c checksums-sha256.txt)

           tar -xzf "$TMP/$ASSET" -C "$TMP"
           install -m0755 "$TMP/nebius-tunnel-agent" /usr/local/bin/nebius-tunnel-agent

           getent group tunna >/dev/null || groupadd --system tunna
           id -u tunna >/dev/null 2>&1 || useradd --system --gid tunna \
             --no-create-home --shell /usr/sbin/nologin tunna
           chown -R tunna:tunna "$CONF_DIR"
           chmod 0750 "$CONF_DIR"
           chmod 0640 "$CONF_DIR/config.yaml"

           systemctl daemon-reload
           systemctl enable nebius-tunnel-agent.service
           systemctl restart nebius-tunnel-agent.service

       - path: /etc/systemd/system/jupyter.service
         permissions: "0644"
         content: |
           [Unit]
           Description=JupyterLab (behind a Nebius tunnel)
           After=network-online.target docker.service
           Wants=network-online.target
           Requires=docker.service

           [Service]
           EnvironmentFile=/etc/jupyter/env
           ExecStartPre=-/usr/bin/docker rm -f jupyter
           ExecStartPre=/usr/bin/docker pull quay.io/jupyter/base-notebook:latest
           ExecStart=/usr/bin/docker run --rm --name jupyter \
             -p 127.0.0.1:8888:8888 \
             -e JUPYTER_TOKEN=${JUPYTER_TOKEN} \
             quay.io/jupyter/base-notebook:latest \
             start-notebook.py \
             --ServerApp.ip=0.0.0.0 \
             --ServerApp.allow_origin='*' \
             --ServerApp.allow_remote_access=True \
             --ServerApp.trust_xheaders=True
           ExecStop=/usr/bin/docker stop jupyter
           Restart=on-failure
           RestartSec=10
           TimeoutStartSec=0

           [Install]
           WantedBy=multi-user.target

     runcmd:
       # Agent first: the tunnel is working while the backend is still pulling images.
       - [/usr/local/sbin/install-nebius-tunnel-agent]
       - mkdir -p /etc/jupyter
       - bash -c 'test -s /etc/jupyter/env || { umask 077; printf "JUPYTER_TOKEN=%s\n" "$(openssl rand -hex 24)" > /etc/jupyter/env; }'
       - systemctl enable --now jupyter.service
     ```

     This configuration enables the following actions:

     * Add a username and a public SSH key to the VM.
     * Install Docker on the VM.
     * Deploy the tunnel agent.
     * Authenticate by using the service account attached to the VM.
     * Create two [Nebius Tunnels services](/tunnels/overview#supported-traffic): `jupyter` and `ssh`. Use them to connect to the tunnel.
     * Deploy JupyterLab on the VM.
     * Generate a JupyterLab token. With this token, you'll be able to authenticate to JupyterLab and open it.
   </Accordion>

3. Create the VM:

   <Tabs group="interfaces">
     <Tab title="Web console">
       1. In the sidebar, go to <Icon icon="https://mintcdn.com/nebius-ai-cloud/rOlLZ_MFvrheaI-h/_assets/sidebar/compute.svg?fit=max&auto=format&n=rOlLZ_MFvrheaI-h&q=85&s=8d3eda9b92f5a626a81d01268852f482" width="16" height="16" data-path="_assets/sidebar/compute.svg" /> **Compute** → **Virtual machines**.

       2. Click **Create resource** → **Virtual machine**.

          The creation flow is a step-by-step wizard. The sidebar shows your progress through the configuration sections. To move between sections, click **Back** and **Next**.

       3. On the **Compute** step, configure computing resources:

          1. In the **Platform** section, select:

             * Without GPUs
             * Regular
             * Non-GPU AMD Epyc Genoa

          2. In the **Settings** section, set:

             * **Preset**: 8 CPUs - 32 GiB RAM
             * **Project**: The project where you create all resources for the tunnel.
             * **VM name**: `tunnel-jupyter`.

       4. On the **Storage** step, preserve the **Ubuntu 24.04 LTS** disk image and the SSD disk type but change the disk size to 50 GiB.

       5. On the **Network** step, check the network and subnet. Keep **None** in the **Public IP address** field.

       6. On the **Configuration** step, configure access, identity and VM startup settings:

          1. In the **Service account** field, select the `tunnel-agent-sa` service account.
          2. Enable a custom cloud-init configuration file.
          3. In the field that appears, paste the contents of the `jupyterlab.yaml` file prepared earlier.

       7. On the **Review** step, check the full VM configuration. To change a section quickly, click <Icon icon="https://mintcdn.com/nebius-ai-cloud/1Ha0sWR6e1mnIaHS/_assets/pencil-to-line.svg?fit=max&auto=format&n=1Ha0sWR6e1mnIaHS&q=85&s=9f557f28487b7fec78ae7508b5b6a145" width="16" height="16" data-path="_assets/pencil-to-line.svg" /> next to the corresponding block. The wizard opens the relevant step with your current settings.

       8. Click **Create**.
     </Tab>

     <Tab title="CLI">
       ```bash theme={null}
       nebius compute instance create \
         --name tunnel-jupyter \
         --resources-platform cpu-d3 \
         --resources-preset 8vcpu-32gb \
         --boot-disk-attach-mode READ_WRITE \
         --boot-disk-managed-disk-name tunnel-jupyter-boot \
         --boot-disk-managed-disk-type network_ssd \
         --boot-disk-managed-disk-size-gibibytes 50 \
         --boot-disk-managed-disk-source-image-family-image-family ubuntu24.04-driverless \
         --network-interfaces "[{\"name\": \"eth0\", \"ip_address\": {}, \"subnet_id\": \"<subnet_ID>\"}]" \
         --service-account-id <service_account_ID> \
         --cloud-init-user-data "$(cat jupyterlab.yaml)"
       ```

       In the command, specify the [subnet ID](/vpc/networking/resources#how-to-get-a-subnet-id) and the ID of the `tunnel-agent-sa` service account.
     </Tab>
   </Tabs>

Wait for the VM to start running.

### Open JupyterLab

1. Get the JupyterLab token. To do so, connect to the VM and extract the token from the VM:

   ```bash theme={null}
   ssh -o ProxyCommand="openssl s_client -quiet -verify_quiet -servername %h -connect %h:443" \
      ubuntu@ssh-<tunnel_masked_ID>.tunnel.applications.<region>.nebius.cloud \
      sudo cat /etc/jupyter/env
   ```

   In the VM address, specify:

   * `tunnel_masked_ID`: Mask of the tunnel ID without the `applicationtunnel-` prefix and regional routing code. Contains the last 15 characters of the tunnel ID. For example, for `applicationtunnel-e00abcdef123456789`, the masked ID of the tunnel is `abcdef123456789`.
   * `region`: [Region](/overview/regions) of the project where you created the resources.

   For more information about the tunnel URL, see [Supported traffic](/tunnels/overview#supported-traffic).

   The command returns the JupyterLab token after the message about the connection:

   ```text theme={null}
   JUPYTER_TOKEN=c912db4***
   ```

2. Open JupyterLab in the browser by using the following URL:

   ```text theme={null}
   https://jupyter-<tunnel_masked_ID>.tunnel.applications.<region>.nebius.cloud/lab?token=<JupyterLab_token>
   ```

   In the URL, specify the masked ID of the tunnel, the region and the token.

   The page can take about a minute to open. If you see the message `failed to connect to local service` instead, this means that JupyterLab hasn't started yet. Wait a few moments and try again.

## How to delete the created resources

The created VM and its boot disk are chargeable. If you don't need them, [delete the VM](/compute/virtual-machines/delete), so Nebius AI Cloud doesn't charge for the resources. The boot disk is deleted automatically when you delete the VM.

You can also delete free resources:

* [Tunnel](/tunnels/delete)
* [Service account](/iam/service-accounts/manage#deleting-a-service-account)
* [Custom group](/iam/authorization/groups/delete-groups)

***

*"Jupyter" and the Jupyter logos are trademarks or registered trademarks of LF Charities, used by Nebius B.V. with permission.*
