Why a reactive notebook on your own server
In Jupyter, execution order depends on your clicks: a variable redefined further down, a forgotten cell, and the displayed results no longer match the code. The .ipynb file, JSON loaded with outputs, is hard to review and creates Git conflicts on every run.
marimo analyses the dependencies between cells. When a cell changes, every cell that depends on it re-runs, and only those. The notebook is an ordinary Python file: git diff reads it, python notebook.py runs it.
Hosting it on your VPS keeps the data on a server you control, gives the whole team the same environment from a browser, and lets you pick the memory and CPU your computations need.
What marimo brings
- Reactive execution — an edited cell re-runs the cells that depend on it, with no hidden state.
- .py files — readable notebooks, versioned in Git and runnable as scripts.
- UI elements — sliders, dropdowns, tables and charts wired to Python variables.
- Notebook to app —
marimo runserves a notebook read-only with the code hidden. - Migration from Jupyter —
marimo converttransforms an .ipynb file. - Official "data" image — pandas, polars, numpy and altair preinstalled.
marimo, Jupyter and code-server
Scroll the table
| marimo | JupyterLab | code-server | |
|---|---|---|---|
| Notebook format | .py file | .ipynb file (JSON) | Project files |
| Execution order | Reactive, by dependencies | Manual, on click | Whatever script you run |
| Review in Git | Readable Python diff | JSON diff with outputs | Code diff |
| Publish as an app | Yes, `marimo run` | Third-party extensions | No |
| Best for | Analysis and internal tools in Python | Wide range of kernels | Full software development |
Prerequisites
An Ubuntu 24.04 VPS with 2 GB of RAM and 20 GB of disk (plan more memory if you load large datasets), Docker and the Compose plugin installed, and root SSH access. A subdomain (for example notebooks.example.com) pointing to the VPS is useful for HTTPS access, but not required: an SSH tunnel is enough when you work alone.
Install marimo with Docker Compose
Prepare the directory and the password
Create the application directory and a
.envfile holding the editor password.mkdir -p /opt/marimo && cd /opt/marimo echo "ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')" > .env chmod 600 .envWrite the compose file
Create
compose.yaml. The official "data" image already contains the analysis libraries. The port is published on the loopback interface only, and the password is passed through a file rather than on the command line.services: marimo: image: ghcr.io/marimo-team/marimo:0.25.2-data restart: unless-stopped ports: - "127.0.0.1:2718:8080" environment: ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD requis} command: - sh - -c - umask 077 && printf %s "$$ADMIN_PASSWORD" > /tmp/marimo-password && exec marimo edit --headless --skip-update-check --host 0.0.0.0 -p 8080 --token-password-file /tmp/marimo-password /app volumes: - marimo-data:/app volumes: marimo-data: {}Start and check
Start the stack, then query the health probe: it should answer 200.
docker compose up -d docker compose ps curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:2718/healthPublish the editor over HTTPS
marimo talks to the browser over WebSocket: the nginx vhost must forward the
UpgradeandConnectionheaders, otherwise the editor loads but cells never run.server { server_name notebooks.example.com; location / { proxy_pass http://127.0.0.1:2718; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; } }certbot --nginx -d notebooks.example.com
No domain? Use an SSH tunnel
The editor runs Python code with the container's privileges: never expose it without a password. When you work alone there is no need to publish it — an SSH tunnel opens the editor on http://localhost:2718 from your machine.
ssh -L 2718:127.0.0.1:2718 root@<vps-ip>Secure the editor
An open marimo editor lets anyone who gets in run Python on your server: that is its purpose, and it is why a few simple rules apply.
A long, unique password. The one in the .env file is random; do not replace it with a password you use elsewhere, and change it when a collaborator who knows it leaves the project (edit .env, then docker compose up -d).
A port that stays local. The compose file publishes the port on 127.0.0.1 only: from the Internet, only nginx over HTTPS reaches it. Never replace that address with 0.0.0.0.
No plain-text secrets in notebooks. A notebook is a versioned Python file: an API key typed into a cell ends up in the Git history. Read it from an environment variable added to the compose file, or from a file outside the repository.
Logs to watch. On startup, marimo prints its access address in docker compose logs, password included: the log stays on your VPS, but do not paste it as is into a ticket or a chat.
Your first reactive notebook
Log in with the password from the .env file. The editor opens the /app folder, which already holds the official tutorials. Create a notebook with two cells: the first defines a slider, the second filters a CSV file with its value. Move the slider: the second cell re-runs by itself.
import marimo as mo
import polars as pl
threshold = mo.ui.slider(0, 1000, value=100, label="Minimum amount")
thresholdsales = pl.read_csv("data/sales.csv")
sales.filter(pl.col("amount") >= threshold.value)Read your data: CSV, Parquet and APIs
The "data" image covers most analysis work without installing anything. pandas reads CSV, Excel and JSON files; polars reads CSV and Parquet very fast and can process files larger than memory in lazy mode; altair draws interactive charts displayed right under the cell.
Drop your files into the volume's /app/data folder, with scp to the VPS then docker compose cp, or upload them from the editor's file panel. For an API, Python's standard library is enough to download JSON, which pandas or polars then turn into a table.
Every cell result is a live object: a DataFrame shows as a sortable, filterable table, and a selection made in a table or chart can feed the next cell. This mechanism is what turns an exploration into a reusable tool.
Turn a notebook into an app
A notebook with UI elements is already an app. marimo run serves it read-only: visitors play with the sliders and dropdowns without seeing or editing the code. Publish this second port behind its own vhost, protected like the editor.
docker compose exec marimo marimo run /app/rapport.py --host 0.0.0.0 -p 8081Your existing Jupyter notebooks convert in one command; then review the cells that redefined the same variable, which marimo rejects by design.
docker compose exec marimo marimo convert /app/old.ipynb -o /app/new.pyWorking together on the same notebooks
A marimo server is a shared editor: everyone with the password sees the same files. For a small team, the simplest rule is one notebook per person and per topic, plus a Git repository in /app to keep the history.
Because every notebook is a .py file, the usual developer tools apply: git diff shows exactly which cell changed, a pull request reads like code, and a Python linter or formatter runs on it unchanged. A notebook can even be imported as a module or run as a script from a scheduled job, to produce the same report every night.
Sizing the VPS
marimo itself is light; your data sets the server size. Plan 2 GB of RAM to explore files of a few hundred megabytes with pandas, and move to 4 or 8 GB if you load larger datasets or if several people work at the same time. CPU matters mostly for heavy computations: polars uses every available core. Finally, keep enough disk for the image (about 400 MB) and for your data files, which grow faster than the notebooks.
Backup and updates
Notebooks live in the marimo-data volume. The safest option is to version /app in a Git repository; otherwise, archive the volume regularly.
docker run --rm -v marimo_marimo-data:/data -v "$PWD":/backup alpine \
tar czf /backup/marimo-$(date +%F).tar.gz -C /data .To update, change the image tag in compose.yaml after reading the project's release notes, then run docker compose pull && docker compose up -d. The notebooks, plain .py files, are left untouched.
One-click install from the ServOrbit Marketplace
On a ServOrbit VPS, marimo installs from the Marketplace in your client area: the script deploys the official container, generates the editor password and creates the notebooks volume. The password is shown in your client area. Attach a subdomain of one of your domains, or the VPS's free subdomain, to open the editor over HTTPS; without a domain, the SSH tunnel described above works as is, with the port shown in your client area.