Skip to main content

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 over 5432 and 6379 or 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 runserver toy. This is the goal we're aiming at; we're not fully there yet — current focus is --from-git / --from-path ergonomics, dump-restore strategies, the runtime banner, and the .run-site-config sidecar. 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.

PyPI version Python CI License

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_restore post-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" via manage.py shell -c).
  • --from-git URL / --from-path PATH — run any Django project from any source without manual git clone + uv sync.
  • A runtime banner that hands you everything you'd otherwise look up: the psql connection command, libpq env-var line, sidecar dotfile path, whether containers are ephemeral or kept on exit, and the dev superuser credentials.
  • A .run-site-config sidecar dotfile at the project root that records the live ports + connection URLs for any tool that wants to read them (e.g. django-dev-helpers at 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 get shlex.quoted).
  • env: is the libpq variable line — paste once into your shell and every later psql / pg_dump against the dev DB just works.
  • Lifecycle: is the explicit --reuse / no---reuse indicator; with --reuse it tells you the exact docker rm -f <slug>-runsite-{pg,redis} to clean up later.
  • The Celery enable hint only shows when celery is disabled in the config — running with --no-celery against 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 = false switch.

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 fatalpost_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

Examples

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

run_site-0.4.0.tar.gz (93.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

run_site-0.4.0-py3-none-any.whl (65.8 kB view details)

Uploaded Python 3

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

Hashes for run_site-0.4.0.tar.gz
Algorithm Hash digest
SHA256 f0782f84d5ed1f09cc38e1911cf6e7294e20d4e702abacc678a4ce64ad22f154
MD5 fca3c35b956fd770295593651931f9b5
BLAKE2b-256 4639a069391a92be849d90f8c3a009f51ce10d5c30f5a48a70d77f54b57d612d

See more details on using hashes here.

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

Hashes for run_site-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 35d9d2501fe987d5fb07359c9cd0f8b9ba0625b922040ac0461b290886fa5479
MD5 d61036e0799cd88cb02c152235521e6c
BLAKE2b-256 1fbc6db721f5b260ff1291e32eb7f88afd06abe55c3cad33d5d8b49fa1eca121

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page