Deployment guide

marimo on a VPS: self-hosted reactive Python notebooks

Deploy on a VPS Cloud →

Tutorial

marimo on a VPS: self-hosted reactive Python notebooks

Self-hosting8 min read4 steps

A Jupyter notebook that has been running all afternoon often ends up lying: one cell edited, another never re-run, and the chart on screen no longer matches the code. marimo fixes this at the root. This open-source Python notebook (Apache-2.0 licensed) saves every notebook as a plain .py file and automatically re-runs the cells that depend on the one you change. This guide installs it with Docker Compose on a VPS, protects the editor with a password, publishes it over HTTPS behind nginx, then shows how to turn a notebook into a small app.

Contents· Why a reactive notebook on your own server1/14
  1. 01Why a reactive notebook on your own server
  2. 02What marimo brings
  3. 03marimo, Jupyter and code-server
  4. 04Prerequisites
  5. 05Install marimo with Docker Compose
  6. 06No domain? Use an SSH tunnel
  7. 07Secure the editor
  8. 08Your first reactive notebook
  9. 09Read your data: CSV, Parquet and APIs
  10. 10Turn a notebook into an app
  11. 11Working together on the same notebooks
  12. 12Sizing the VPS
  13. 13Backup and updates
  14. 14One-click install from the ServOrbit Marketplace

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 run serves a notebook read-only with the code hidden.
  • Migration from Jupyter — marimo convert transforms an .ipynb file.
  • Official "data" image — pandas, polars, numpy and altair preinstalled.

marimo, Jupyter and code-server

Scroll the table

marimoJupyterLabcode-server
Notebook format.py file.ipynb file (JSON)Project files
Execution orderReactive, by dependenciesManual, on clickWhatever script you run
Review in GitReadable Python diffJSON diff with outputsCode diff
Publish as an appYes, `marimo run`Third-party extensionsNo
Best forAnalysis and internal tools in PythonWide range of kernelsFull 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

  1. Prepare the directory and the password

    Create the application directory and a .env file holding the editor password.

    mkdir -p /opt/marimo && cd /opt/marimo
    echo "ADMIN_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')" > .env
    chmod 600 .env
  2. Write 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: {}
  3. 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/health
  4. Publish the editor over HTTPS

    marimo talks to the browser over WebSocket: the nginx vhost must forward the Upgrade and Connection headers, 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")
threshold
sales = 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 8081

Your 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.py

Working 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.

Your Python notebooks, on your VPS

A ServOrbit Ubuntu 24.04 VPS with 2 GB of RAM is enough for marimo. One-click install from the Marketplace, password generated for you, HTTPS on your domain or on the VPS's free subdomain.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab