run-site
uvx-wannabe for full-fledged* Django sites.Run several Django sites side-by-side — one per worktree, branch, or checkout — each with its own PostgreSQL and Redis on automatically-picked, non-conflicting ports. The current stack's ports + connection URLs are written to
.run-site-config(a TOML dotfile at the project root) so both you and your coding agent can talk to the right services without fighting over5432and6379or parsing logs to find them.Primary use cases: test automation across branches, multi-agent coding workflows, comparing two versions of a site at once. As a bonus, the same engine pulls projects straight from a Git URL with no manual
git clone/ venv /uv sync— see Run from a Git URL below.* Full-fledged = a Django project that expects PostgreSQL, Redis, and some kind of seed dump for the database — not a one-file
manage.py runservertoy. This is the goal we're aiming at; we're not fully there yet — current focus is--from-git/--from-pathergonomics, dump-restore strategies, the runtime banner, and the.run-site-configsidecar. See CHANGELOG.md for what's shipping and the Status section for current rough edges.
Pure CLI orchestrator for local Django development. PostgreSQL & Redis
testcontainers + dump load + local runserver/Celery + log multiplexer +
hooks — all in one command. Zero Django dependency in the CLI itself.
What it does
One command — run-site run — to spin up a complete local Django dev
stack:
- PostgreSQL + Redis testcontainers on random (or stable, with
--reuse) ports. - Optional dump load (
.sql,.sql.gz,.dump/.pgdump) using the right strategy (init-script for fresh PG,psql/pg_restorepost-start otherwise). - Local
migrate, dev superuser creation/refresh,runserver, Celery worker/beat, and any extra processes you declare — multiplexed into one terminal with colored log prefixes. - HTTP readiness probe and browser auto-open.
- Lifecycle hooks (
pre_containers,post_migrate,pre_serve, …) that can shell out (type = "command") or run inside Django (type = "django"viamanage.py shell -c). --from-git URL/--from-path PATH— run any Django project from any source without manualgit clone+uv sync.- A runtime banner that hands you everything you'd otherwise look up:
the
psqlconnection command, libpq env-var line, sidecar dotfile path, whether containers are ephemeral or kept on exit, and the dev superuser credentials. - A
.run-site-configsidecar dotfile at the project root that records the live ports + connection URLs for any tool that wants to read them (e.g.django-dev-helpersat Django bootstrap).
The CLI does not import Django, does not modify urls.py, does not know
your settings.py. It only spawns <your-python> <your-manage.py> <command>
as subprocesses and multiplexes their logs.
Install
pipx install run-site
# or
uv tool install run-site
Requirements: Python 3.11+, Docker daemon running, git (only if you use
--from-git).
Quickstart
From your Django project root, generate a config — it auto-detects
manage.py, your Django project module, Celery, and uv:
run-site init
That writes runsite.toml with sensible defaults; for typical projects
no edits are needed. When uv is on PATH it pins the Python
invocation to uv run --no-sync python so deps come from
pyproject.toml / uv.lock automatically.
Then run:
run-site run
You get migrate, an admin / admin superuser, runserver listening on
a random free port, browser opened on the homepage, container logs
streaming in your terminal — and the banner below.
What run-site run shows you
════════════════════════════════════════
run-site is running
════════════════════════════════════════
Project: myproj
Root: /Users/me/code/myproj
App: http://localhost:54812/
Admin: http://localhost:54812/admin/
Superuser: admin / admin (created)
email=admin@example.com
Postgres: 127.0.0.1:54321
db=myproj user=myproj password=password
psql: PGPASSWORD=password psql -h 127.0.0.1 -p 54321 -U myproj -d myproj
env: PGHOST=127.0.0.1 PGPORT=54321 PGDATABASE=myproj PGUSER=myproj PGPASSWORD=password
Redis: 127.0.0.1:16379
Lifecycle: Postgres + Redis will be removed on exit.
Pass --reuse to keep them between runs (faster restart, dump preserved).
Celery: disabled
[tip] enable Celery in runsite.toml:
[celery]
enabled = true
app = "<your_django_module>.celery"
then re-run (use --no-celery to skip per-run).
Sidecar: /Users/me/code/myproj/.run-site-config (removed on shutdown)
════════════════════════════════════════
Notable touches:
Superuser:tells you(created)on a fresh DB,(existing — password reset to dev default)when the user pre-existed and[superuser].overwrite = true(the default), or(existing — password unchanged)when overwrite is off — and only displays the password in states where it's actually what's in the DB right now.psql:is a copy-paste-ready command line (passwords with shell meta-chars getshlex.quoted).env:is the libpq variable line — paste once into your shell and every laterpsql/pg_dumpagainst the dev DB just works.Lifecycle:is the explicit--reuse/ no---reuseindicator; with--reuseit tells you the exactdocker rm -f <slug>-runsite-{pg,redis}to clean up later.- The
Celeryenable hint only shows when celery is disabled in the config — running with--no-celeryagainst an enabled config doesn't trigger it (that's a deliberate per-run override). - All secrets get hidden behind a single
[banner].show_db_credentials = falseswitch.
The .run-site-config sidecar
Every run writes a TOML file to <project_root>/.run-site-config
before the runserver starts and removes it on shutdown:
project_slug = "myproj"
generated_at = "2026-05-07T13:42:11+00:00"
[web]
host = "localhost"
port = 54812
url = "http://localhost:54812/"
[postgres]
host = "127.0.0.1"
port = 54321
db = "myproj"
user = "myproj"
password = "password"
url = "postgres://myproj:password@127.0.0.1:54321/myproj"
[redis]
host = "127.0.0.1"
port = 16379
db = 0
url = "redis://127.0.0.1:16379/0"
[celery]
enabled = true
app = "myproj.celery"
Use cases: tooling that wants the live ports without parsing logs,
django-dev-helpers reading them at app load, scripts you write that
run alongside the dev server. Add .run-site-config to your
.gitignore — it's regenerated per-run.
Run from a Git URL
No clone, no venv, no uv sync to do by hand. The "uvx mode" — try a
project without even installing run-site first:
uv tool run run-site run --from-git git@github.com:mpasternak/django-multiseek.git --yes
--yes skips the cloning-confirmation prompt, making the command
copy-paste-safe in tutorials and CI. The same pattern works against any
public or SSH-accessible repo:
run-site run --from-git https://github.com/iplweb/bpp.git --branch main
The CLI clones the repo to ~/.cache/run-site/checkouts/<slug>/,
creates a venv, installs deps (auto-detecting uv.lock /
pyproject.toml / requirements.txt), then runs as usual. Reuse with
--no-pull --no-install. See docs/from-git.md.
Run from any local checkout
run-site run --from-path ~/Programowanie/some-django-app
No need to cd first.
Reuse containers between runs
run-site run --reuse
Stable container names — <project_slug>-runsite-pg and -redis — survive
between runs so you don't reload the dump each time. The banner's
Lifecycle: line tells you which mode you're in and how to clean up.
Common recipes
Restore a PostgreSQL dump before the site starts
Dump restoration is a first-class feature — not a hook. Point
[dump] at any .sql, .sql.gz, .dump, or .pgdump file and
run-site picks the right loader strategy automatically:
[dump]
default_path = "fixtures/baseline.sql" # relative to project root
strategy = "auto" # init-script for fresh PG, post-start otherwise
restore_jobs = "auto" # parallelism for pg_restore (auto = min(8, cpu_count))
fail_fast = true
| Strategy | When to use it |
|---|---|
auto (default) |
Plain .sql + fresh container → init-script. Otherwise → post-start. Reused container → skipped (existing data preserved). |
init-script |
Force PG to load the dump from /docker-entrypoint-initdb.d/. Only works for .sql on freshly-created containers. |
post-start |
Always restore via psql / pg_restore after PG is up. Handles every supported format. |
Override per-run from the CLI:
run-site run --from-dump fixtures/2026-05-07.sql.gz
run-site run --no-dump # skip the restore for this run
run-site run --dump-strategy=post-start # force post-start, even on a reused container (nukes data)
The full reference lives in docs/configuration.md#dump.
Lifecycle hooks — pre/post each stage
Hooks let you wedge custom logic into the orchestrator's flow. Two
flavors: type = "command" (regular subprocess) and type = "django"
(through manage.py shell -c, with a ctx dict containing the live
ports and credentials).
The available stages, in order:
pre_containers → post_containers → pre_dump → post_dump → post_migrate
↓
post_superuser → pre_serve
↓
(runserver runs)
↓
post_stop
Note: there is no pre_migrate stage — use post_dump (it runs
right before migrate). And there is no post_serve stage —
runserver blocks until shutdown, so the closest is post_stop
(best-effort cleanup; errors get logged, not fatal).
pre_containers — build assets before anything starts
[[hooks.pre_containers]]
type = "command"
command = ["make", "assets"]
timeout = 300
cli_disable_flag = "--skip-assets" # `run-site run --skip-assets` skips this run
post_dump — patch the freshly-loaded baseline
Right after the dump loads, before migrate:
[[hooks.post_dump]]
type = "django"
callable = "myproject.runsite_hooks:rotate_dev_secrets"
timeout = 30
# myproject/runsite_hooks.py
def rotate_dev_secrets(ctx: dict) -> None:
"""Replace any production-looking secrets the dump may have shipped
with safe dev placeholders. Runs once per restore."""
from django.contrib.auth import get_user_model
User = get_user_model()
User.objects.filter(is_superuser=True).update(
password="!unusable", # force re-login through the dev superuser flow
)
post_migrate — load fixtures or run management commands
[[hooks.post_migrate]]
type = "django"
callable = "myproject.runsite_hooks:load_dev_fixtures"
def load_dev_fixtures(ctx: dict) -> None:
from django.core.management import call_command
call_command("loaddata", "fixtures/dev_seed.json", verbosity=0)
post_superuser — clean up auth quirks
[[hooks.post_superuser]]
type = "django"
callable = "myproject.runsite_hooks:clear_password_policy"
def clear_password_policy(ctx: dict) -> None:
from password_policies.models import PasswordChangeRequired
PasswordChangeRequired.objects.filter(
user__username=ctx["superuser"]["username"]
).delete()
pre_serve — last call before runserver
The .run-site-config sidecar is already on disk by this stage, so a
hook can read it.
[[hooks.pre_serve]]
type = "django"
callable = "myproject.runsite_hooks:warm_caches"
timeout = 60
def warm_caches(ctx: dict) -> None:
"""Pre-fill the homepage cache so the first request is fast."""
from django.test import Client
Client().get("/")
Custom CLI flag for a hook
Add [[hooks.<stage>.cli_args]] to register a flag dynamically — the
parser is rebuilt after config load so --help shows it:
[[hooks.post_migrate]]
type = "django"
callable = "myproject.runsite_hooks:fetch_token"
timeout = 60
[[hooks.post_migrate.cli_args]]
flag = "--get-token-from"
dest = "ssh_source"
metavar = "USER@HOST"
help = "Pull a deploy token from this SSH host after migrations"
run-site run --get-token-from admin@bpp-prod
def fetch_token(ctx: dict) -> None:
source = ctx["opts"].get("ssh_source")
if not source:
return # flag not passed — no-op
# … scp / ssh whatever you need …
post_stop — best-effort cleanup
[[hooks.post_stop]]
type = "command"
command = ["bash", "-lc", "rm -rf .runtime-cache/"]
Errors here are logged, not fatal — post_stop shouldn't be able
to break a clean shutdown.
Full reference + the ctx dict schema: docs/hooks.md.
A real-world hook setup: examples/runsite.bpp.toml.
What's in the box
| File / module | Role |
|---|---|
cli.py |
Argparse entrypoint, two-pass parsing, run / doctor / init dispatch. |
init_cmd.py |
run-site init — generates runsite.toml from project layout. |
config.py |
runsite.toml / pyproject.toml loader + validator. |
discovery.py |
Project root, manage.py, local Python resolution chain. |
containers.py |
testcontainers PG + Redis, named/reuse, Ryuk policy. |
dumps.py |
Format detection, init-script vs. post-start strategy. |
env.py |
Build env for subprocesses + the DEV_HELPERS_* contract. |
sidecar.py |
Write/remove the .run-site-config runtime file. |
processes.py |
Spawn, terminate, HTTP probe. |
log_multiplexer.py |
Colored prefixes per stream. |
hooks.py |
Command / Django hook execution. |
superuser.py |
manage.py shell -c with get_user_model(). |
banner.py |
Orchestrator banner with URLs, credentials, helpers. |
source/from_git.py |
Clone/pull, slug extraction, ownership policy. |
source/venv_setup.py |
uv venv / python -m venv. |
source/deps_installer.py |
uv sync / pip install -r. |
Companion package — django-dev-helpers
The features that live inside Django (autologin endpoint, dotfile
generation, agent help banner) are intentionally split into a separate
package, django-dev-helpers.
You install it in your Django project and the two communicate via a
documented DEV_HELPERS_* env-var contract — neither imports the other.
The .run-site-config sidecar gives django-dev-helpers a second,
file-based path to the same data.
uv add django-dev-helpers --group dev
INSTALLED_APPS = [..., "django_dev_helpers"]
See docs/with-django-dev-helpers.md for the full integration story.
Documentation
- Quickstart
- Configuration reference
- Run from Git or path
- Local processes (Celery, extras)
- Hooks
- Integration with
django-dev-helpers - Troubleshooting
Examples
examples/runsite.minimal.toml— bare minimum config.examples/runsite.celery.toml— adds Celery worker + beat.examples/runsite.bpp.toml— full BPP-style config with custom PG image, dump, hooks, dynamic CLI args.examples/test_site/— a small Django project used by integration tests; runs end-to-end withrun-site run.
Status
v0.3 is the current release. CLI flags and config schema may still evolve before 1.0 — see CHANGELOG.md for what's changed.
License
MIT — see LICENSE.
Release files for run-site 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| run_site-0.4.0.tar.gz | 93.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| run_site-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 159.7 kB
Release files / run_site-0.4.0.tar.gz
| Download URL | run_site-0.4.0.tar.gz |
|---|---|
| Size | 93.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f0782f84d5ed1f09cc38e1911cf6e7294e20d4e702abacc678a4ce64ad22f154
|
|
BLAKE2b-256 checksum How to use checksums |
4639a069391a92be849d90f8c3a009f51ce10d5c30f5a48a70d77f54b57d612d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / run_site-0.4.0-py3-none-any.whl
| Download URL | run_site-0.4.0-py3-none-any.whl |
|---|---|
| Size | 65.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
35d9d2501fe987d5fb07359c9cd0f8b9ba0625b922040ac0461b290886fa5479
|
|
BLAKE2b-256 checksum How to use checksums |
1fbc6db721f5b260ff1291e32eb7f88afd06abe55c3cad33d5d8b49fa1eca121
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|