Run Saturn Lab
Turn an existing folder of Jupyter notebooks into a clear visual board. Pull two public Docker images, point at your folder, and start mapping the project.
Install
Requirements: Docker 20+ and Docker Compose v2. No source checkout needed — the images are public on GitHub Container Registry.
# 1 · grab the compose file + env template $ curl -L -O https://saturnlab.pizner.com/downloads/docker-compose.prod.yml $ curl -L -o .env https://saturnlab.pizner.com/downloads/saturn.env.example # 2 · set at least SECRET_KEY in .env $ nano .env # 3 · launch and print the actual clickable address $ docker compose -f docker-compose.prod.yml up -d && \ SATURN_PORT="$(docker compose -f docker-compose.prod.yml port frontend 80 | tail -n 1 | sed 's/.*://')" && \ echo "Saturn Lab → http://localhost:${SATURN_PORT}"
The command asks Docker for the resolved port, including values loaded from .env, and prints the correct clickable URL.
Open Saturn Lab at http://localhost → (default port)
That pulls ghcr.io/marekpizner/saturn-backend and …/saturn-frontend and starts everything: the web UI, the API, PostgreSQL, and a built-in JupyterLab.
Bring your own notebooks
Set NOTEBOOKS_PATH to an existing notebook directory. Saturn discovers .ipynb files recursively and opens the host files directly — no import directory and no notebook copies inside Docker. After login, the repository opens automatically without asking for a project name. If Saturn finds a root requirements.txt, it asks before installing anything into the persistent project environment used by Jupyter and pipeline runs.
requirements.txt only when you trust the repository.# .env NOTEBOOKS_PATH=/Users/me/projects/my_analysis # open an existing project → notebooks appear within a few seconds
Not reachable after up?
Almost always a leftover from a previous failed start (e.g. a port was busy). Reset cleanly:
$ docker compose -f docker-compose.prod.yml down $ docker compose -f docker-compose.prod.yml up -d
If port 80 (or 8888) is taken, set PORT / JUPYTER_PORT in .env and try again.
Product tour
The maintained interface previews on the home page mirror the current product controls. The workflow is deliberately small:
Configuration
Everything is set in .env — no image rebuild. Change ports freely:
PORT=8080 # web UI → http://localhost:8080 JUPYTER_PORT=9999 # JupyterLab → http://localhost:9999 BIND_IP=127.0.0.1: # localhost-only (trailing colon); empty = all interfaces
| Variable | Default | Description |
|---|---|---|
SECRET_KEY | — | Django secret key — required, set a long random string |
DB_PASSWORD | saturn_db_pass | PostgreSQL password |
ADMIN_PASSWORD | empty | If set, auto-creates an admin. Empty → first-run Setup page |
JUPYTER_TOKEN | derived | Optional override; a strong token is derived from SECRET_KEY by default |
PORT | 80 | Web UI host port |
JUPYTER_PORT | 8888 | JupyterLab host + container port |
BIND_IP | empty | Bind address. Empty = all interfaces; 127.0.0.1: = localhost only |
NOTEBOOKS_PATH | . | Host notebook workspace read and written directly by Saturn |
TAG | 0.1.13 | Image version to run (latest for newest) |
ALLOWED_HOSTS | * | Comma-separated allowed hostnames |
First login
On a fresh install the first screen is a Setup page — choose the admin username and password (any length). After login, an existing mounted repository opens automatically. Only an empty workspace asks you to create and name a new project.
For unattended deploys, set ADMIN_PASSWORD in .env beforehand and an admin account is created automatically, skipping Setup.
Notebooks
Each node on the board is a Jupyter notebook. A tiny SDK wires data between steps — it's injected automatically, just from saturn import saturn:
from saturn import saturn df = saturn.read() # primary upstream input dfs = saturn.read_all() # all inputs → {name: DataFrame} tok = saturn.secret("API_KEY") # project secret (set in the sidebar) # ... your work ... saturn.save(df) # output → shown on the canvas edge saturn.log(accuracy=0.94) # tracked metric saturn.note("baseline model") # lineage note
Loose notebooks first appear in the grey Notebooks lane. Files already inside a known lane folder are categorized automatically. Move a card into another category and Saturn moves the real file into that lane's host folder. Select a card to resize it from its edge and corner handles; the size persists.
The folder layout stays simple: lane directories such as processing/, filtration/, aggregation/, experiments/, and notebooks/ live directly below NOTEBOOKS_PATH. Existing folders such as Experiments/ are adopted instead of duplicated. New lane names produce readable folders, and deleting a lane moves every notebook safely into notebooks/.
Select one or more cards with Shift/Ctrl/Cmd, then choose Fork to copy their real notebook content and the connections between them. Existing host files are never overwritten or deleted from the board.
Upgrade & backup
Upgrade
$ docker compose -f docker-compose.prod.yml pull $ docker compose -f docker-compose.prod.yml up -d
Migrations run automatically; your data volumes are preserved.
Back up your data
$ docker run --rm -v saturn_storage:/data -v $(pwd):/backup \ alpine tar czf /backup/saturn_storage.tar.gz /data
Existing notebooks remain under NOTEBOOKS_PATH. Notebooks created or forked in Saturn are written into its direct lane folders. Runtime data and models live in the saturn_storage volume; the database in postgres_data. All survive down/up.
Licensing
Read the PolyForm Noncommercial 1.0.0 terms. Have thoughts, found a bug, or want to evaluate Saturn commercially? Email marek@pizner.com.
Troubleshooting
Port already in use
Set PORT (and/or JUPYTER_PORT) in .env to a free port and run up -d again.
A node is stuck on "running"
Each notebook has a 10-minute timeout; after that it's marked error. Click the node to read the traceback.
View logs
$ docker compose -f docker-compose.prod.yml logs backend -f