Shared Django dev image: Python 3.14, uv, ruff, mypy, Celery, Postgres 18. Pulled, never built.
823
The shared development image for every Django project built from
django-template. Pulled, never built, on your laptop.
Python 3.14 · uv · ruff · mypy · pre-commit · Celery · PostgreSQL 18 client · GitHub CLI · Starship — non-root, multi-arch
docker pull alihaidar199527/django-devcontainer:latest
Building a full Django dev environment — compilers, database clients, a dozen CLI tools — takes 20–30 minutes and drifts from machine to machine. This repo does that work once, in CI, and publishes the result to Docker Hub.
When you create a project from django-template and click Reopen in
Container, VS Code only pulls this image. Nothing is built locally; the
container is ready in the time it takes to download.
Improve the image here and every project gets the upgrade on its next pull, without touching project code.
This repo has one job: build, test, and publish that image. It contains no
Django files, no pyproject.toml, and no application code — Django, DRF,
psycopg, redis-py and every other project package are installed per project
with uv add.
django-devcontainer ──(CI: test · build · sign · push)──▶ Docker Hub
│ docker pull
django-template ──(Use this template)──▶ your project ──────┘
(app · celery · celery-beat all run this image;
postgres · redis · mailpit are stock images)
| Repo | Responsibility |
|---|---|
django-devcontainer ← you are here | The environment: Python, uv, tools, shell — built and published by CI |
django-template | The project scaffold: compose stack, devcontainer.json, hooks, app CI, production Dockerfile |
Base: official python:3.14-slim-trixie (Debian 13), pinned by digest. All
common native build headers are present (libpq, libssl, libffi,
libjpeg, libwebp, freetype, libxml2, …), so psycopg, Pillow, and
lxml compile without extra setup.
| Category | Tool | Notes |
|---|---|---|
| Runtime | Python 3.14 (latest patch) | Every new Python release arrives as its own Dependabot PR |
| Packages | uv + uvx | Pinned version, copied from the official uv image |
| Code quality | ruff · mypy · pre-commit | A project's own versions win once its .venv exists |
| Shells | IPython · Starship prompt · bash aliases | Git branch/status, Python version in the prompt |
| Database | PostgreSQL 18 client (psql, pg_dump, pg_isready) · pgcli | From the official PGDG repo — matches the template's Postgres 18 |
| Cache / queue | redis-cli · Celery · Flower · watchfiles | Worker/beat containers start before uv sync; see below |
| API testing | HTTPie | http GET localhost:8000/api/ |
| Git | git · git-lfs · GitHub CLI (gh) · openssh-client · gnupg | SSH push and signed commits from inside the container |
| Utilities | jq · vim · nano · less · htop · tree · ping · dig · nc · sudo |
Every Python CLI is installed with uv tool install into its own isolated
environment, so the system Python stays clean and tools never conflict with
each other or with your project. Their versions are pinned in
docker/requirements-tools.txt
(also inside the image at /opt/uv-tools/requirements-tools.txt) and kept
current by Dependabot.
Not inside, by design: Django and any other project package (uv add them),
the Celery app config (your project's config/celery.py), and Node.js.
What a project using this image can rely on:
| User | dev — UID/GID 1000, passwordless sudo. Declared in the image's devcontainer.metadata label, so devcontainer.json needs no remoteUser. |
| Workspace | /workspace (owned by dev) — mount your project here |
| Home | /home/dev — .ssh (0700), .cache/uv, .shell_history pre-created and owned by dev, so named volumes mounted there inherit the right ownership |
| PATH | /workspace/.venv/bin comes first — in every container and every shell, login shells included (/etc/profile would otherwise reset it; VS Code probes the environment with one). After uv sync, python, celery, mypy, pytest … resolve to the project's versions — no activation step. Before that, the image's global tools answer. |
| Env | UV_LINK_MODE=copy (cache and bind-mounted .venv live on different filesystems), PYTHONPATH=/workspace in interactive shells, PYTHONUNBUFFERED=1, UTF-8 locale |
| Ports | 8000 Django dev server · 5555 Flower |
| Command | sleep infinity — the container stays up for VS Code to attach |
services:
app:
image: alihaidar199527/django-devcontainer:latest
volumes:
- .:/workspace:cached
- uv-cache:/home/dev/.cache/uv
- shell-history:/home/dev/.shell_history
ports: ["8000:8000", "5555:5555"]
volumes:
uv-cache:
shell-history:
Git & SSH: don't mount ~/.ssh or ~/.gitconfig. VS Code Dev Containers
forwards your host ssh-agent and copies your .gitconfig into the
container automatically — keys never touch the container and there are no
Windows NTFS permission problems. Just make sure your key is loaded on the
host (ssh-add -l); see Troubleshooting.
| Tag | Published | Use |
|---|---|---|
latest | every build from main (push + weekly) | Default for projects |
YYYYMMDD | every build | Pin to a known-good week |
sha-xxxxxxx | every build | Trace an image back to its commit |
The image is rebuilt every Monday even without code changes, so latest
always carries current Debian security fixes and the latest apt packages
(GitHub CLI, PostgreSQL client, …) and Starship.
Platforms: linux/amd64 (Windows, Linux, cloud) and linux/arm64 (Apple
Silicon) — Docker pulls the right one automatically.
Every published image carries an SBOM and a SLSA provenance attestation
signed with Sigstore (keyless, via GitHub OIDC). The Docker workflow's run
summary prints the exact cosign verify command for each build. Inspect them:
docker buildx imagetools inspect alihaidar199527/django-devcontainer:latest --format '{{ json .Provenance }}'
docker buildx imagetools inspect alihaidar199527/django-devcontainer:latest --format '{{ json .SBOM }}'
Each published digest is also scanned by Trivy; results are in the repository's Security → Code scanning tab.
Baked in via scripts/shell_setup.sh for every user of the image.
| Group | Aliases |
|---|---|
| Django | pm manage.py · pmr runserver 0.0.0.0:8000 · pmm migrate · pmmk makemigrations · pms shell · pmsu createsuperuser · pmcs collectstatic · pmt test |
| Celery | cw worker (solo pool) · cb beat (DatabaseScheduler) · cf flower :5555 · cpurge purge — all -A config |
| uv | uvs sync · uva add · uvr remove · uvl pip list · uvf pip freeze |
| Git | gs status · ga add · gc commit -m · gp push · gl log graph · gco checkout · gb branch |
| Utilities | ll · la · cls · dps · dlogs |
Shell history (50 000 lines) persists when a volume is mounted at /home/dev/.shell_history.
Push to develop ──▶ Lint (hadolint · ShellCheck · actionlint · cspell · markdownlint)
Dependabot PR ────└─▶ Image tests: if the image inputs changed, native build on
amd64 + arm64 runners → tests/smoke.sh
PR develop → main ─▶ PR source + the Lint / Image tests results of develop's
head commit (required — nothing re-runs)
Merge to main ────────────▶ Publish ─▶ Scan (already tested on develop)
Weekly (Mon 05:00) ▶ Test ─▶ Publish ─▶ Scan
Manual dispatch ──▶ Test ─▶ Publish ─▶ Scan
│ └─ Trivy → Security tab
└─ docker/github-builder: native per-arch builds,
multi-arch manifest, SBOM, signed provenance,
push :latest · :YYYYMMDD · :sha-xxxxxxx
tests/smoke.sh checks the user,
every tool, the shell config, and .venv precedence on both architectures —
on every develop commit (required to release), or right before a
scheduled/manual publish. Nothing runs twice for the same commit.develop; main accepts PRs from develop
only, and every merge into main is a release.Details: .github/CONTRIBUTING.md.
django-devcontainer/
├── .github/
│ ├── ISSUE_TEMPLATE/ # Bug + feature forms (blank issues disabled)
│ ├── workflows/
│ │ ├── docker.yml # Test → publish → scan the image
│ │ ├── branch-policy.yml # PR source: only develop may open PRs into main
│ │ ├── lint.yml # hadolint · ShellCheck · actionlint · cspell · markdownlint
│ │ ├── dockerhub-description.yml# README.md → Docker Hub
│ │ └── labels.yml # labels.yml → GitHub labels
│ ├── actionlint.yaml # actionlint config
│ ├── CODEOWNERS · CONTRIBUTING.md · SECURITY.md · pull_request_template.md
│ ├── dependabot.yml # Weekly: Actions, Docker (python, uv), CLI tools
│ └── labels.yml # Labels as code
├── .cspell/
│ └── project-words.txt # Spell-check dictionary: tool, package, alias names
├── .vscode/
│ └── extensions.json # Recommended editor extensions (same checks as CI)
├── docker/
│ ├── Dockerfile.dev # The image recipe
│ └── requirements-tools.txt # Pinned versions of the global CLIs (Dependabot: pip)
├── scripts/
│ └── shell_setup.sh # Starship prompt, aliases, history (build time)
├── tests/
│ └── smoke.sh # Runtime contract test (CI, amd64 + arm64)
├── .dockerignore # Allowlist — only what the build COPYs
├── .editorconfig · .gitattributes # LF everywhere, consistent formatting
├── .hadolint.yaml # Dockerfile lint config
├── .markdownlint-cli2.jsonc # Markdown lint config (editor + CI)
├── cspell.json # Spell-check config (en + en-GB, code dictionaries)
├── LICENSE # MIT
└── README.md # This file — also the Docker Hub page
Push to develop; the Test jobs build and smoke-test both architectures.
When green, open a PR develop → main and merge it — the image publishes
automatically.
Updates arrive as Dependabot PRs into develop every Monday: GitHub
Actions, the Python and uv images, and the pinned CLI tools. Review, let CI go
green, merge — they reach main with the next develop → main release.
Python minor upgrade (e.g. 3.14 → 3.15) also arrives as its own Dependabot
PR, but its Test jobs fail on purpose until you update the Python version
check in tests/smoke.sh — a deliberate step, so confirm your projects'
dependencies support the new version first.
A system package: add it to the single apt-get install list in
Dockerfile.dev, grouped under a comment explaining why.
A global Python CLI: add name==version to docker/requirements-tools.txt
and a matching uv tool install -c "$c" <name> line in Dockerfile.dev. Only
tools useful to every project belong here — project dependencies go in the
project.
Before pushing, build and test locally:
docker build -f docker/Dockerfile.dev -t django-devcontainer:test .
docker run --rm -v "$PWD/tests:/tests:ro" django-devcontainer:test bash /tests/smoke.sh
Images published before the non-root switch ran as root with tools in the
system Python. Projects created from an older django-template need these
changes (in the project / template, not here):
| File | Change |
|---|---|
docker-compose.yml | Mount volumes under /home/dev instead of /root (uv-cache → /home/dev/.cache/uv, shell-history → /home/dev/.shell_history). Remove the ~/.ssh and ~/.gitconfig mounts and GIT_SSH_COMMAND — use ssh-agent forwarding. |
.devcontainer/devcontainer.json | Remove "remoteUser": "root" (the image label sets dev). |
scripts/entrypoint.dev.sh | Remove the /root/.ssh chmod block. |
scripts/welcome.sh | Remove the block that appends venv activation to /root/.bashrc — .venv/bin is already first on PATH. |
To stay on the old behaviour temporarily, pin the project to the last root
image, alihaidar199527/django-devcontainer:sha-7cd9005, instead of latest.
git push fails — Permission denied (publickey)The container uses your host's ssh-agent. On the host:
ssh-add -l # is a key loaded?
ssh-add ~/.ssh/id_ed25519 # macOS / Linux
ssh -T [email protected] # verify
On Windows (PowerShell as Administrator), start the agent once and load the key:
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519
Then Rebuild / Reopen in Container so VS Code picks up the agent.
/workspace owned by the wrong user (Linux hosts)Dev Containers re-maps dev to your host UID (updateRemoteUserUID, set by
the image label). Outside VS Code, run compose with a host user whose UID is
1000, or add user: "${UID}:${GID}" to the service.
Settings → Secrets and variables → Actions: confirm DOCKERHUB_USERNAME
and DOCKERHUB_TOKEN (Docker Hub → Account settings → Personal access tokens,
Read & Write), then re-run the workflow.
GitHub pauses scheduled workflows after 60 days without repository activity. Re-enable it under Actions → Docker → Enable workflow.
MIT © Ali Haidar
Content type
Image
Digest
sha256:ae3807579…
Size
305.3 MB
Last updated
2 days ago
docker pull alihaidar199527/django-devcontainer