Deploy a Jupyter Server for Team Data Work
A Jupyter server is a long-running web application that hosts notebooks, kernels, and files; your team connects from a browser, and all code executes on the server, not on anyone's laptop. This guide deploys one on Railway that your whole team shares: one Python environment, one set of data files, one pool of compute, instead of "works on my machine" differences across laptops.
The setup includes:
- Token authentication so only your team can open it.
- A volume so notebooks survive redeploys.
- A health check so dependency upgrades deploy with zero downtime.
One constraint up front: Railway services run on CPU. There are no GPUs, so this setup fits data wrangling, analysis, reporting, and classical machine learning, not GPU model training.
Will long-running cells time out?
No. Jupyter's browser UI talks to kernels over WebSockets, which Railway supports on every service. Those connections are exempt from the idle timeout that applies to plain HTTP requests, so a cell that runs for an hour keeps streaming output to your browser without being cut.
Plain HTTP requests to a service are closed after 5 minutes of no data transfer, and run at most 15 minutes even while transferring. Jupyter's kernel protocol does not hit those limits because the long-lived channel is a WebSocket.
Create the project files
You need two files. Railway's builder, Railpack, detects requirements.txt and builds a Python environment automatically, so no Dockerfile is required.
requirements.txt:
jupyterlab==4.4.5
pandas==2.3.1
matplotlib==3.10.3Add whatever your team analyzes with: polars, scikit-learn, duckdb, database drivers. Pin versions. The server is shared, so an unpinned upgrade breaks everyone at once.
railway.json:
{
"$schema": "https://railway.com/railway.schema.json",
"deploy": {
"startCommand": "jupyter lab --ip=0.0.0.0 --port=$PORT --no-browser --ServerApp.root_dir=/data --ServerApp.allow_remote_access=True",
"healthcheckPath": "/api",
"restartPolicyType": "ON_FAILURE"
}
}What each flag does:
--ip=0.0.0.0binds all interfaces so Railway's proxy can reach the server.--port=$PORTlistens on the port Railway injects.--ServerApp.root_dir=/dataserves notebooks from the volume mount path you will create below.--ServerApp.allow_remote_access=Trueaccepts requests whoseHostheader is your public domain. Without it, Jupyter blocks non-local hosts.healthcheckPath: /apipoints the health check at Jupyter's version endpoint, which returns 200 without authentication. With a health check configured, Railway keeps the old deployment serving until the new one is live.
Deploy the service
From the directory containing both files, using the Railway CLI:
railway init
railway upOr push the two files to a GitHub repository and create a service from it in the dashboard. See deploying services for both flows.
The first deploy will crash-loop until the volume and token exist. Set those up next.
Add a volume for notebooks
Container filesystems on Railway are ephemeral. Every redeploy starts from a fresh image, so notebooks written to the container disk disappear. A volume persists them.
- Right-click the service in your project canvas and choose Attach Volume (or use the Command Palette with
⌘K). - Set the mount path to
/data.
The start command above already points Jupyter's root directory at /data, so everything your team creates in the JupyterLab file browser lands on the volume.
Two volume behaviors worth knowing:
- Volumes are mounted when the container starts, not during build or pre-deploy. Do not write seed data to
/datain a build step; it will not persist. - Volumes cost $0.15 per GB per month, metered per minute.
That persistence has a limit, though: a volume is still a single disk. For datasets you cannot lose, enable volume backups or keep raw data in a storage bucket and treat the volume as a working directory.
Secure the server with a token
A Jupyter server executes arbitrary code. Never expose one without authentication.
Jupyter reads the JUPYTER_TOKEN environment variable at startup and requires it for every session. Set it as a service variable:
- Open the service's Variables tab.
- Add
JUPYTER_TOKENwith a long random value:
openssl rand -hex 32Once you redeploy, team members open:
https://<your-domain>/?token=<value>or paste the token into the login page. Everyone who has the token has full execution access, so treat it like a shared password: store it in your team's secret manager and rotate it when someone leaves. For per-user logins and an admin-managed user list, run JupyterHub instead of a single shared server; the volume and networking setup in this guide applies the same way.
Expose the server on a domain
In the service's Settings tab, under Networking, generate a Railway-provided domain: a *.up.railway.app URL with TLS included. Railway detects the port your server listens on and sets it as that domain's target port. You can replace it with a custom domain later.
If other services in the same environment need to call the Jupyter REST API (for example, a scheduler that executes notebooks), they can use private networking at http://<service-name>.railway.internal:<port> instead of going over the public internet. Private networking is scoped per environment.
Size the server for your team
Railway bills for usage: $10 per GB of RAM per month and $20 per vCPU per month, metered by the minute. There is no per-seat charge, so one shared server for five analysts costs the same as for one.
Memory is the number that matters. Every open kernel holds its DataFrames in RAM, and pandas typically needs several times the on-disk size of a dataset. Watch the service's memory graph during a normal working day, then set replica limits with headroom above the observed peak. When a kernel exhausts memory the whole server is at risk, so encourage the team to shut down idle kernels from the JupyterLab kernel sidebar.
If the server sits idle outside working hours, consider serverless. Railway stops the container after 10 minutes with no outbound traffic and wakes it on the next request. Note that open JupyterLab tabs keep WebSocket traffic flowing, so the server only sleeps once everyone closes their tabs. Notebooks are on the volume, so nothing is lost across sleep and wake cycles.
Optional: real-time collaboration
JupyterLab supports Google-Docs-style collaborative editing through the jupyter-collaboration extension. Add it to requirements.txt:
jupyter-collaboration==4.1.0Redeploy, and multiple people editing the same notebook see each other's cursors and edits live. Without the extension, two people editing one notebook silently overwrite each other on save, so either install it or agree on one-notebook-per-person conventions.
Verify the deployment
After the deploy goes live:
curl https://<your-domain>/apishould return a JSON version payload like {"version": "2.16.0"}. Then open the domain in a browser, authenticate with the token, create a notebook, and run:
import pandas as pd
df = pd.DataFrame({"x": range(5)})
print(df.describe())Redeploy the service and confirm the notebook is still there. That proves the volume mount is correct.
Next steps
- Volumes covers mount paths, sizing, and growth.
- Back up and restore volumes protects the notebook data itself.
- Right-size CPU and memory helps you tune limits as the team grows.
- Managing secrets on Railway covers safer handling of tokens like
JUPYTER_TOKEN.