Skip to content

Deployment

Plombery is designed to run as a single process on a single machine: one uvicorn process serves the REST API, the web UI and runs the scheduler and your pipelines. There is no broker, no worker pool and no separate scheduler service to operate.

Running in production

Your app is an ASGI application, so any ASGI server will do. Assuming your app lives in app.py:

app.py
from plombery import get_app

# import the modules that register your pipelines
import my_pipelines  # noqa

app = get_app()

Run it with:

uvicorn app:app --host 0.0.0.0 --port 8000

Do not run multiple workers

Don't use --workers N, and don't run several instances against the same database. The scheduler lives inside the process and its state is in memory, so every worker keeps its own copy of the schedule and every scheduled run would be fired once per worker.

If you need to survive a restart of the machine, use a process supervisor that runs a single instance, such as systemd or your container platform's restart policy.

What has to be persisted

Two things outlive a run and must be on durable storage:

What Where Setting
Run history The database, ./plombery.db by default database_url
Logs and task output The .data/ directory, under the working directory by default data_path

If either is on an ephemeral filesystem, you lose the run history and the logs at every restart. See Database for using something other than SQLite, and System for data_path.

Behind a reverse proxy

When Plombery is served on a domain rather than on localhost:8000, set the public URL and restrict CORS:

plombery.config.yaml
frontend_url: https://plombery.example.com
allowed_origins:
  - https://plombery.example.com

A minimal nginx site, remembering that the UI uses WebSockets for live logs:

server {
    server_name plombery.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # required for the live logs
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Docker

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# keep the database and the run data on a volume
ENV DATABASE_URL=sqlite:////data/plombery.db
ENV DATA_PATH=/data
VOLUME /data

# the version recorded on every run
ARG PIPELINE_VERSION
ENV PIPELINE_VERSION=$PIPELINE_VERSION

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Versioning the deployment

Every run records the version of the pipeline that produced it. Unless a pipeline sets its own, that version comes from the pipeline_version setting, and a deployment is exactly where it has to be set: the git repository the version would otherwise be read from is not part of the image.

Pass it from the build, where the repository is still available:

docker build --build-arg PIPELINE_VERSION=$(git describe --tags --always) .

In a CI pipeline the revision is already in the environment — $GITHUB_SHA on GitHub Actions, $CI_COMMIT_SHA on GitLab CI — and any string identifying the deployed code works just as well, a release number included. Without it, runs record no version.

systemd

/etc/systemd/system/plombery.service
[Unit]
Description=Plombery
After=network.target

[Service]
Type=simple
User=plombery
WorkingDirectory=/opt/plombery
ExecStart=/opt/plombery/.venv/bin/uvicorn app:app --host 0.0.0.0 --port 8000
Restart=always

[Install]
WantedBy=multi-user.target

Before you expose it

Plombery runs the Python code you gave it, and the UI can trigger any pipeline. Anyone who can reach it can run your pipelines and read their output, so on a public address you should always enable authentication, and set auth.secret_key to a real secret rather than leaving the default.