Electron Packaging¶
DJDesk includes a full Electron wrapper so the reference implementation can ship as an installable desktop app. Use this page as the in-depth companion to Package and distribute. For a discussion about why you might choose Electron at all, start with Why Electron + Django?.
Note
Looking for decision criteria instead of implementation details? See Why Electron + Django? for the benefits/trade-offs matrix.
System overview¶
The moving pieces can be summarized as:
Electron main process
│
│ launches bundled python or system python
▼
run_django.py ──> manage.py runserver (Django)
│
└─ serves http://127.0.0.1:<port> to the renderer window
electron/main.js chooses the interpreter, waits for Django to boot, and points the
renderer at the local server.
Local workflow¶
Install Node dependencies
just electron-install(runsnpm installinsideelectron/).Bundle Django (optional during initial prototyping)
just electron-bundle(callsnpm run bundle) to createelectron/django-bundle/. Without this directory the app falls back to the system Python.Start the shell
just electron-startwhich executesnpm start.main.jslooks forelectron/django-bundle/python/bin/python3on macOS/Linux orelectron/django-bundle/python/python.exeon Windows. If neither exists it triespython3.14,python3, orpythonfrom your PATH. Override the interpreter viaPYTHON=/custom/python npm startwhen needed.
Bundler internals¶
npm run bundle (or just electron-bundle) executes electron/build-django.js.
The script produces a reproducible Python payload in seven stages:
Delete and recreate
electron/django-bundle/.Download the matching python-build-standalone archive for the current OS/arch, verify its SHA256 checksum, and unpack it into
django-bundle/python/.Install every dependency listed under
[project].dependenciesusingpython -m pip install --no-cache-dir …inside the bundled interpreter. Optional extras/dev dependencies are ignored.Copy
manage.pyplussrc/djdesk(excluding__pycache__) intodjango-bundle/src/.Run
python manage.py collectstatic --no-input --clearwithDJANGO_STATIC_ROOTpointing todjango-bundle/staticfiles/.Write
run_django.py(the in-bundle launcher) and aVERSIONfile containinggit rev-parse --short HEADplus a timestamp.Import Django using the bundled interpreter to prove the environment is healthy.
Interpreter downloads are cached under electron/.python-downloads/ so subsequent
bundles reuse existing archives.
Bundle layout¶
electron/django-bundle/
├── manage.py
├── run_django.py
├── src/djdesk/…
├── staticfiles/
├── python/
└── VERSION
run_django.py augments PYTHONPATH so django-bundle/src takes precedence,
sets DJANGO_ENV/DJANGO_SETTINGS_MODULE defaults, and then executes
manage.py runserver --noreload on the host/port provided by Electron.
Runtime behavior¶
electron/main.js controls application startup:
Uses
get-portto find an available port (prefers8000).Calls
resolvePythonto prioritize the bundled interpreter and only uses the system Python when necessary.Spawns Django via
run_django.pywhen a bundle exists, or directly runsmanage.py runserverotherwise.Sets
DJANGO_ENV=localandDJANGO_SETTINGS_MODULE=djdesk.settings.localunless they are already defined.Polls
http://127.0.0.1:<port>/up to 30 times before showing an error, ensuring the renderer window connects only after Django responds.Opens DevTools automatically when
NODE_ENV=development.
Building platform installers¶
npm run build (surfaced via just electron-build-*) regenerates the bundle and
then invokes Electron Builder using electron/electron-builder.json. Platform
outputs are:
macOS – DMG + ZIP
Windows – NSIS installer + ZIP
Linux – AppImage, DEB, and TAR.GZ
Electron Builder excludes django-bundle/ from the application ASAR but re-adds it
through extraResources so installers always contain the Python runtime and Django
payload alongside resources/app.asar.
Production checklist¶
Database location.
djdesk.settings.basepoints SQLite atBASE_DIR / "db.sqlite3". Inside an installer this resolves todjango-bundle/db.sqlite3, which may be read-only. OverrideDATABASES['default']['NAME']via environment variables to a writable path underapp.getPath('userData').Migrations. The bundled launcher applies
manage.py migrateon startup (seerun_django.py). If you require migrations to run during bundling instead, extendbuild-django.jsaccordingly; the reference build performs them when the Electron shell boots.Updates. Releases are manual (no auto-updater yet).
just electron-workflow-runtriggers CI builds, but you still need to distribute the resulting artifacts.Logging & diagnostics.
main.jsstreams Django stdout/stderr to the terminal. For production telemetry, capture logs underapp.getPath('logs')or integrate a logging service.Secrets.
DJANGO_SECRET_KEYdefaults to a development value. Set it during bundling via environment variables if you need unique secrets per build.Bundle size. Run
du -sh electron/django-bundleanddu -sh electron/distto understand disk usage before shipping.
Automation & CI¶
.github/workflows/electron-desktop.yml builds installers for macOS, Linux, and
Windows:
Jobs run on
macos-latest,ubuntu-latest, andwindows-latest.actions/setup-nodeinstalls Node.js 22 with npm caching.astral-sh/setup-uvprovides theuvCLI required bybuild-django.js.Ubuntu installs
libfuse2so AppImage packaging succeeds.npm ciinstalls dependencies inelectron/.npm run bundlecreateselectron/django-bundle/(npm run buildrunsbundleagain; the duplication is safe but could be optimized later).npm run build -- --<platform>runs Electron Builder, which copiesdjango-bundle/into each installer.actions/upload-artifactpublishesdjdesk-macos,djdesk-linux, anddjdesk-windowspackages fromelectron/dist/.
The workflow is currently manual-only (trigger: workflow_dispatch). Launch it
via the GitHub UI or run just electron-workflow-run locally. To build on pushes,
extend the on: block with the desired paths.
Alternatives¶
Approach |
Typical bundle size |
Complexity |
Best for |
|---|---|---|---|
Electron (DJDesk) |
150 MB+ |
Moderate |
Django apps needing desktop UX + offline capability |
PyInstaller / Nuitka |
40 MB+ |
Low–Moderate |
Pure Python CLIs or GUIs without an embedded browser |
Tauri |
20 MB+ |
High (Rust toolchain) |
Lightweight web UIs with tight bundle-size budgets |
Traditional web deploy |
N/A |
Low |
Multi-user or internet-facing deployments |
Adding or updating dependencies¶
Edit [project].dependencies in pyproject.toml and rerun
just electron-bundle. Because dependencies install via pip inside the bundled
Python 3.14 interpreter, ensure wheels exist for every target platform. Packages with
native extensions (NumPy, pandas, etc.) inflate both bundle size and build time but
otherwise require no code changes—npm run build will carry the updated bundle into
the next set of installers.
Utility recipes¶
just electron-runs– list recent GitHub Actions run history.just electron-download-latest– download the newest successful artifacts intodist-artifacts/(requires GitHub CLI).just electron-clean-artifacts– remove downloaded artifacts.
Run just -l to explore every helper available in the repository.