Skip to content

Wiring together

Let's build something real — a FastAPI application on Python 3.14, backed by PostgreSQL. Three jails, each doing one thing, all wired together.

The config

The complete project files are available in the repo examples.

Create a file called stack.ucl:

jail "python-314" {
  setup {
    python { type = "ansible"; file = "playbooks/python-314.yml"; }
  }
}

jail "postgres-16" {
  setup {
    postgres { type = "ansible"; url = "hub://postgres/16"; }
  }
  forward {
    pg { host = 6432; jail = 5432; }
  }
}

jail "fastapi-314" {
  base { type = "jail"; name = "python-314"; }

  depends ["postgres-16"]

  setup {
    fastapi { type = "ansible"; file = "playbooks/fastapi-314.yml"; }
  }
  forward {
    http { host = 8080; jail = 8000; }
  }
  mount {
    src { host = "."; jail = "/srv/app"; }
  }
  exec {
    httpserver {
      cmd = "python3.14 -m uvicorn app:app --reload";
      dir = "/srv/app";
      healthcheck {
        test = "fetch -qo /dev/null http://127.0.0.1:8000";
        interval = "30s";
        timeout = "10s";
        retries = 5;
      }
    }
  }
}

Bring it up

jrun up stack.ucl

Or run jrun with no arguments and interactive wizard will guide you through picking the config file.

What's happening

Jailrun uses Ansible for provisioning — every setup block points to a playbook that runs when the jail is first created.

The hub:// scheme tells jrun to pull the playbook from Jailrun Hub — a curated collection of playbooks for common services like PostgreSQL, Redis, Nginx, and more. You can mix remote playbooks with your own local ones, composing layer by layer. In this config, postgres-16 uses a Jailrun Hub playbook while python-314 and fastapi-314 use local ones.

Compiling from source can be slow. You do it once in python-314, then fastapi-314 is created as its clone via the base block — a fully independent copy ready in milliseconds, using no extra disk space until it diverges.

Deploy order is controlled by depends. Jailrun resolves the dependency graph automatically. In this case: python-314 first (it's the base), then postgres-16 (it's a dependency), then fastapi-314 last.

Jails discover each other by name. From inside fastapi-314, try ping postgres-16.local.jrun — it just works. Use jail names directly in your app's database config.

Port forwarding works from your host. PostgreSQL is reachable at localhost:6432. FastAPI at localhost:8080. Healthchecks are built in — the process supervisor monitors each service and restarts it if the check fails.

Live reload works out of the box. Your project directory is shared into the jail. Uvicorn's --reload sees file changes instantly.

Check status

jrun status
jrun status

Inspect and debug

Drop into any jail:

jrun ssh postgres-16

Run a command without opening a shell:

jrun cmd postgres-16 psql -U postgres -c 'SELECT version()'

Update and tear down

See CLI reference for the full list of commands to redeploy, pause, and tear down jails.