CLI orchestrator for local Django development — testcontainers PostgreSQL/Redis, dump loading, log multiplexing, hooks, and run-from-git.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file run_site-0.4.0.tar.gz.
File metadata
- Download URL: run_site-0.4.0.tar.gz
- Upload date:
- Size: 93.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0782f84d5ed1f09cc38e1911cf6e7294e20d4e702abacc678a4ce64ad22f154
|
|
| MD5 |
fca3c35b956fd770295593651931f9b5
|
|
| BLAKE2b-256 |
4639a069391a92be849d90f8c3a009f51ce10d5c30f5a48a70d77f54b57d612d
|
File details
Details for the file run_site-0.4.0-py3-none-any.whl.
File metadata
- Download URL: run_site-0.4.0-py3-none-any.whl
- Upload date:
- Size: 65.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35d9d2501fe987d5fb07359c9cd0f8b9ba0625b922040ac0461b290886fa5479
|
|
| MD5 |
d61036e0799cd88cb02c152235521e6c
|
|
| BLAKE2b-256 |
1fbc6db721f5b260ff1291e32eb7f88afd06abe55c3cad33d5d8b49fa1eca121
|