From 2dd96efe423c709f4bea702692fc0de865ac6a84 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Tue, 21 Apr 2026 12:04:43 -0700 Subject: [PATCH 1/8] guides: add django Signed-off-by: Craig Osterhout --- content/guides/django.md | 520 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 520 insertions(+) create mode 100644 content/guides/django.md diff --git a/content/guides/django.md b/content/guides/django.md new file mode 100644 index 00000000000..3e2924f33c9 --- /dev/null +++ b/content/guides/django.md @@ -0,0 +1,520 @@ +--- +title: Containerize a Django application +linkTitle: Django +description: Learn how to containerize a Django application using Docker. +keywords: python, django, containerize, initialize, gunicorn, compose watch +summary: | + This guide shows how to containerize a Django application using Docker. + You'll scaffold the project with a bind-mount container, create a + production-ready Dockerfile with docker init, then add a development + stage and Compose Watch for fast iteration. +languages: [python] +tags: [dhi] +params: + time: 25 minutes +--- + +## Prerequisites + +- You have installed the latest version of [Docker Desktop](/get-started/get-docker.md). +- Python does not need to be installed on your local machine. You'll use a container to initialize the project. + +> **New to Docker?** +> Start with the [Docker basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide to get familiar with key concepts like images, containers, and Dockerfiles. + +--- + +## Overview + +This guide walks you through containerizing a Django application with Docker. By the end, you will: + +- Initialize a Django project using a container with a bind mount, with no local Python install required. +- Create a production-ready Dockerfile using `docker init`. +- Add a `development` stage to your Dockerfile and configure Compose Watch for automatic code syncing. + +--- + +## Create the Django project + +Rather than cloning a sample application, you'll use a Python container with a bind mount to scaffold a new Django project directly on your local machine. + +1. Create a new project directory and navigate to it: + + ```console + $ mkdir django-docker-example && cd django-docker-example + ``` + +2. Run a Python container with a bind mount to initialize the project. The `--mount` flag makes the generated files appear on your host machine: + + {{< tabs >}} + {{< tab name="Mac / Linux" >}} + + ```console + $ docker run --rm \ + --mount type=bind,src=.,target=/app \ + -w /app \ + python:3.12-slim \ + sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + ``` + + {{< /tab >}} + {{< tab name="PowerShell" >}} + + ```powershell + $ docker run --rm ` + --mount "type=bind,src=.,target=/app" ` + -w /app ` + python:3.12-slim ` + sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + ``` + + {{< /tab >}} + {{< tab name="Command Prompt" >}} + + ```console + $ docker run --rm ^ + --mount "type=bind,src=%cd%,target=/app" ^ + -w /app ^ + python:3.12-slim ^ + sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + ``` + + {{< /tab >}} + {{< tab name="Git Bash" >}} + + ```console + $ docker run --rm \ + --mount type=bind,src="./",target=/app \ + -w //app \ + python:3.12-slim \ + sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + ``` + + {{< /tab >}} + {{< /tabs >}} + + This command installs Django and Gunicorn inside the container, scaffolds a new Django project in the current directory, and writes the installed packages to `requirements.txt`. + +Your directory should now contain the following files: + +```text +├── django-docker-example/ +│ ├── manage.py +│ ├── myapp/ +│ │ ├── __init__.py +│ │ ├── asgi.py +│ │ ├── settings.py +│ │ ├── urls.py +│ │ └── wsgi.py +│ └── requirements.txt +``` + +--- + +## Create a production Dockerfile + +Use `docker init` to generate a production-ready `Dockerfile`, `.dockerignore`, and `compose.yaml`. + +Inside the `django-docker-example` directory, run: + +```console +$ docker init +``` + +When prompted, answer as follows: + +| Question | Answer | +|---|---| +| What application platform does your project use? | Python | +| What version of Python do you want to use? | 3.12 | +| What port do you want your app to listen on? | 8000 | +| What is the command to run your app? | `gunicorn myapp.wsgi:application --bind 0.0.0.0:8000` | + +`docker init` generates a production-ready `Dockerfile` using the [Python Docker Official Image](https://hub.docker.com/_/python). No changes are needed to use it as-is. + +If you'd prefer a more secure, minimal base image, you can instead use a [Docker Hardened Image (DHI)](https://hub.docker.com/hardened-images/catalog/dhi/python). Docker Hardened Images are production-ready base images maintained by Docker that minimize attack surface and simplify compliance. For more details, see [Docker Hardened Images](/dhi/). + +{{< tabs >}} +{{< tab name="Docker Official Image" >}} + +The `Dockerfile` generated by `docker init` is ready to use. It already sets up a non-privileged user, uses a cache mount to speed up `pip` installs, and runs Gunicorn as the production server. No further edits are needed. + +{{< /tab >}} +{{< tab name="Docker Hardened Image" >}} + +Docker Hardened Images for Python are available in the [DHI catalog](https://hub.docker.com/hardened-images/catalog/dhi/python) and are freely available to all Docker users. + +DHI follows a multi-stage build pattern: a `-dev` image (which includes pip and build tools) is used to install dependencies into a virtual environment, and then a minimal runtime image (no `-dev`) is used for the final stage. The runtime image has no shell, no package manager, and already runs as the `nonroot` user, so no manual user setup is needed. + +1. Sign in to the DHI registry: + + ```console + $ docker login dhi.io + ``` + +2. Replace the contents of your `Dockerfile` with the following: + +```dockerfile {title="Dockerfile"} +# syntax=docker/dockerfile:1 + +# Build stage: the -dev image includes pip and build tools needed to +# install Python packages into a virtual environment. +FROM dhi.io/python:3.12-alpine3.21-dev AS builder + +# Prevent Python from writing .pyc files to disk. +ENV PYTHONDONTWRITEBYTECODE=1 +# Prevent Python from buffering stdout/stderr so logs appear immediately. +ENV PYTHONUNBUFFERED=1 +# Activate the virtual environment for all subsequent RUN commands. +ENV PATH="/app/venv/bin:$PATH" + +WORKDIR /app + +# Create a virtual environment so only the venv needs to be copied to +# the runtime stage, not the full dev toolchain. +RUN python -m venv /app/venv + +# Install dependencies into the virtual environment using a cache mount +# to speed up subsequent builds. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + pip install -r requirements.txt + +# Runtime stage: the minimal DHI image has no shell, no package manager, +# and already runs as the nonroot user. No manual user setup required. +FROM dhi.io/python:3.12-alpine3.21 + +WORKDIR /app + +# Prevent Python from buffering stdout/stderr so logs appear immediately. +ENV PYTHONUNBUFFERED=1 +# Activate the virtual environment copied from the build stage. +ENV PATH="/app/venv/bin:$PATH" + +# Copy application source code and the pre-built virtual environment +# from the builder stage. +COPY . . +COPY --from=builder /app/venv /app/venv + +EXPOSE 8000 + +# Run Gunicorn as the production WSGI server. +CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] +``` + +{{< /tab >}} +{{< /tabs >}} + +### Run the application + +From the `django-docker-example` directory, run: + +```console +$ docker compose up --build +``` + +Open a browser and navigate to [http://localhost:8000](http://localhost:8000). You should see the Django welcome page. + +Press `ctrl`+`c` to stop the application. + +--- + +## Set up a development environment + +The production setup uses Gunicorn and requires a full image rebuild to pick up code changes. For development, you can add a `development` stage to your Dockerfile that uses Django's built-in server, and configure Compose Watch to automatically sync code changes into the running container without a rebuild. + +### Update the Dockerfile + +Replace your `Dockerfile` with a multi-stage version that adds a `development` stage alongside `production`. Select the tab that matches your base image choice from the previous section: + +{{< tabs >}} +{{< tab name="Docker Official Image" >}} + +A shared `base` stage installs dependencies, then branches into a `development` stage and a `production` stage: + +```dockerfile {title="Dockerfile"} +# syntax=docker/dockerfile:1 + +ARG PYTHON_VERSION=3.12 +# The base stage is shared by both development and production. It installs +# dependencies once so both stages benefit from the same layer cache. +FROM python:${PYTHON_VERSION}-slim AS base + +# Prevent Python from writing .pyc files to disk. +ENV PYTHONDONTWRITEBYTECODE=1 +# Prevent Python from buffering stdout/stderr so logs appear immediately. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Create a non-privileged user to run the application. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Install dependencies using a cache mount to speed up subsequent builds and +# a bind mount so requirements.txt doesn't need to be copied into the image. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# Copy application source code into the image. +COPY . . + +# The development stage uses Django's built-in server, which reloads +# automatically when Compose Watch syncs source file changes into the container. +FROM base AS development +USER appuser +EXPOSE 8000 +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] + +# The production stage uses Gunicorn, a production-grade WSGI server. +FROM base AS production +USER appuser +EXPOSE 8000 +CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] +``` + +{{< /tab >}} +{{< tab name="Docker Hardened Image" >}} + +The `development` stage extends directly from `builder`, inheriting the `-dev` image, the virtual environment, and the application code. The `production` stage uses the minimal runtime image as before: + +```dockerfile {title="Dockerfile"} +# syntax=docker/dockerfile:1 + +# Build stage: the -dev image includes pip and build tools needed to +# install Python packages into a virtual environment. +FROM dhi.io/python:3.12-alpine3.21-dev AS builder + +# Prevent Python from writing .pyc files to disk. +ENV PYTHONDONTWRITEBYTECODE=1 +# Prevent Python from buffering stdout/stderr so logs appear immediately. +ENV PYTHONUNBUFFERED=1 +# Activate the virtual environment for all subsequent RUN commands. +ENV PATH="/app/venv/bin:$PATH" + +WORKDIR /app + +# Create a virtual environment so only the venv needs to be copied to +# the runtime stage, not the full dev toolchain. +RUN python -m venv /app/venv + +# Install dependencies into the virtual environment using a cache mount +# to speed up subsequent builds. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + pip install -r requirements.txt + +# Copy application source code into the image. +COPY . . + +# The development stage inherits the -dev image and virtual environment +# from the builder, and runs Django's built-in server which reloads +# when Compose Watch syncs source file changes into the container. +FROM builder AS development +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] + +# The production stage uses the minimal runtime image, which has no shell, +# no package manager, and already runs as the nonroot user. +FROM dhi.io/python:3.12-alpine3.21 AS production + +WORKDIR /app + +# Prevent Python from buffering stdout/stderr so logs appear immediately. +ENV PYTHONUNBUFFERED=1 +# Activate the virtual environment copied from the build stage. +ENV PATH="/app/venv/bin:$PATH" + +# Copy only the application code and the pre-built virtual environment +# from the builder stage. +COPY . . +COPY --from=builder /app/venv /app/venv + +EXPOSE 8000 + +# Run Gunicorn as the production WSGI server. +CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] +``` + +{{< /tab >}} +{{< /tabs >}} + +### Update the Compose file + +Replace your `compose.yaml` with the following. It targets the `development` stage, adds a PostgreSQL database, and configures Compose Watch: + +```yaml {title="compose.yaml"} +services: + web: + build: + context: . + # Build the development stage from the multi-stage Dockerfile. + target: development + ports: + - "8000:8000" + environment: + # Enable Django's debug mode for detailed error pages and auto-reload. + - DEBUG=1 + # Database connection settings passed to Django via environment variables. + - POSTGRES_DB=myapp + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 + # Wait for the database to pass its healthcheck before starting the web service. + depends_on: + db: + condition: service_healthy + develop: + watch: + # Sync source file changes directly into the container so Django's + # dev server can reload them without a full image rebuild. + - action: sync + path: . + target: /app + ignore: + - __pycache__/ + - "*.pyc" + - .git/ + # Rebuild the image when dependencies change. + - action: rebuild + path: requirements.txt + db: + image: postgres:17 + restart: always + # Run as the postgres user rather than root. + user: postgres + volumes: + # Persist database data across container restarts. + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=myapp + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + # Expose the port only to other services on the Compose network, + # not to the host machine. + expose: + - 5432 + # Only report healthy once PostgreSQL is ready to accept connections, + # so the web service doesn't start before the database is available. + healthcheck: + test: ["CMD", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + db-data: +``` + +The `sync` action pushes file changes directly into the running container so Django's dev server reloads them automatically. A change to `requirements.txt` triggers a full image rebuild instead. + +> [!NOTE] +> To learn more about Compose Watch, see [Use Compose Watch](/manuals/compose/how-tos/file-watch.md). + +### Add the PostgreSQL driver + +Add the `psycopg` adapter to your project using a bind-mount container, the same approach you used to initialize the project: + +{{< tabs >}} +{{< tab name="Mac / Linux" >}} + +```console +$ docker run --rm \ + --mount type=bind,src=.,target=/app \ + -w /app \ + python:3.12-slim \ + sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +``` + +{{< /tab >}} +{{< tab name="PowerShell" >}} + +```powershell +$ docker run --rm ` + --mount "type=bind,src=.,target=/app" ` + -w /app ` + python:3.12-slim ` + sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +``` + +{{< /tab >}} +{{< tab name="Command Prompt" >}} + +```console +$ docker run --rm ^ + --mount "type=bind,src=%cd%,target=/app" ^ + -w /app ^ + python:3.12-slim ^ + sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +``` + +{{< /tab >}} +{{< tab name="Git Bash" >}} + +```console +$ docker run --rm \ + --mount type=bind,src="./",target=/app \ + -w //app \ + python:3.12-slim \ + sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +``` + +{{< /tab >}} +{{< /tabs >}} + +Then update the `DATABASES` setting in `myapp/settings.py` to read from environment variables: + +```python {title="myapp/settings.py"} +import os + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("POSTGRES_DB", "myapp"), + "USER": os.environ.get("POSTGRES_USER", "postgres"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD", "password"), + "HOST": os.environ.get("POSTGRES_HOST", "localhost"), + "PORT": os.environ.get("POSTGRES_PORT", "5432"), + } +} +``` + +### Run with Compose Watch + +Start the development stack: + +```console +$ docker compose watch +``` + +Open a browser and navigate to [http://localhost:8000](http://localhost:8000). + +Try editing a file, for example add a view to `myapp/views.py`. Compose Watch syncs the change into the container and Django's dev server reloads automatically. If you add a package to `requirements.txt`, Compose Watch triggers a full image rebuild. + +Press `ctrl`+`c` to stop. + +--- + +## Summary + +In this guide, you: + +- Bootstrapped a Django project using a Docker container and a bind mount, with no local Python installation required. +- Used `docker init` to generate Docker assets and updated the `Dockerfile` to use Gunicorn for production. +- Added a `development` stage to the `Dockerfile` and configured Compose Watch for fast iterative development with a PostgreSQL database. + +Related information: + +- [Dockerfile reference](/reference/dockerfile.md) +- [Compose file reference](/reference/compose-file/_index.md) +- [Use Compose Watch](/manuals/compose/how-tos/file-watch.md) +- [Docker Hardened Images](/dhi/) +- [Multi-stage builds](/manuals/build/building/multi-stage.md) From 61d15eb72ad0aa06d7525f47c50bfc5e0f7b3311 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Tue, 21 Apr 2026 12:16:02 -0700 Subject: [PATCH 2/8] update vale vocab Signed-off-by: Craig Osterhout --- _vale/config/vocabularies/Docker/accept.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/_vale/config/vocabularies/Docker/accept.txt b/_vale/config/vocabularies/Docker/accept.txt index 6df84d15ee7..3d1401e3d47 100644 --- a/_vale/config/vocabularies/Docker/accept.txt +++ b/_vale/config/vocabularies/Docker/accept.txt @@ -106,6 +106,7 @@ Gravatar gRPC Groq Grype +Gunicorn HyperKit inferencing initializer From ded2cfaee2c36ef255891cc382b07ffbd6a439f9 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Tue, 21 Apr 2026 13:28:51 -0700 Subject: [PATCH 3/8] agent feedback Signed-off-by: Craig Osterhout --- content/guides/django.md | 109 ++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/content/guides/django.md b/content/guides/django.md index 3e2924f33c9..ce7b57a9432 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -19,8 +19,11 @@ params: - You have installed the latest version of [Docker Desktop](/get-started/get-docker.md). - Python does not need to be installed on your local machine. You'll use a container to initialize the project. -> **New to Docker?** -> Start with the [Docker basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide to get familiar with key concepts like images, containers, and Dockerfiles. +> [!TIP] +> +> If you're new to Docker, start with the [Docker +> basics](/get-started/docker-concepts/the-basics/what-is-a-container.md) guide +> to get familiar with key concepts like images, containers, and Dockerfiles. --- @@ -132,7 +135,7 @@ When prompted, answer as follows: `docker init` generates a production-ready `Dockerfile` using the [Python Docker Official Image](https://hub.docker.com/_/python). No changes are needed to use it as-is. -If you'd prefer a more secure, minimal base image, you can instead use a [Docker Hardened Image (DHI)](https://hub.docker.com/hardened-images/catalog/dhi/python). Docker Hardened Images are production-ready base images maintained by Docker that minimize attack surface and simplify compliance. For more details, see [Docker Hardened Images](/dhi/). +If you'd prefer a more secure, minimal base image, you can instead use a [Docker Hardened Image (DHI)](https://hub.docker.com/hardened-images/catalog/dhi/python). Docker Hardened Images are production-ready base images maintained by Docker that minimize attack surface. For more details, see [Docker Hardened Images](/dhi/). {{< tabs >}} {{< tab name="Docker Official Image" >}} @@ -154,53 +157,53 @@ DHI follows a multi-stage build pattern: a `-dev` image (which includes pip and 2. Replace the contents of your `Dockerfile` with the following: -```dockerfile {title="Dockerfile"} -# syntax=docker/dockerfile:1 - -# Build stage: the -dev image includes pip and build tools needed to -# install Python packages into a virtual environment. -FROM dhi.io/python:3.12-alpine3.21-dev AS builder - -# Prevent Python from writing .pyc files to disk. -ENV PYTHONDONTWRITEBYTECODE=1 -# Prevent Python from buffering stdout/stderr so logs appear immediately. -ENV PYTHONUNBUFFERED=1 -# Activate the virtual environment for all subsequent RUN commands. -ENV PATH="/app/venv/bin:$PATH" - -WORKDIR /app - -# Create a virtual environment so only the venv needs to be copied to -# the runtime stage, not the full dev toolchain. -RUN python -m venv /app/venv - -# Install dependencies into the virtual environment using a cache mount -# to speed up subsequent builds. -RUN --mount=type=cache,target=/root/.cache/pip \ - --mount=type=bind,source=requirements.txt,target=requirements.txt \ - pip install -r requirements.txt - -# Runtime stage: the minimal DHI image has no shell, no package manager, -# and already runs as the nonroot user. No manual user setup required. -FROM dhi.io/python:3.12-alpine3.21 - -WORKDIR /app - -# Prevent Python from buffering stdout/stderr so logs appear immediately. -ENV PYTHONUNBUFFERED=1 -# Activate the virtual environment copied from the build stage. -ENV PATH="/app/venv/bin:$PATH" - -# Copy application source code and the pre-built virtual environment -# from the builder stage. -COPY . . -COPY --from=builder /app/venv /app/venv - -EXPOSE 8000 - -# Run Gunicorn as the production WSGI server. -CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] -``` + ```dockerfile {title="Dockerfile"} + # syntax=docker/dockerfile:1 + + # Build stage: the -dev image includes pip and build tools needed to + # install Python packages into a virtual environment. + FROM dhi.io/python:3.12-alpine3.21-dev AS builder + + # Prevent Python from writing .pyc files to disk. + ENV PYTHONDONTWRITEBYTECODE=1 + # Prevent Python from buffering stdout/stderr so logs appear immediately. + ENV PYTHONUNBUFFERED=1 + # Activate the virtual environment for all subsequent RUN commands. + ENV PATH="/app/venv/bin:$PATH" + + WORKDIR /app + + # Create a virtual environment so only the venv needs to be copied to + # the runtime stage, not the full dev toolchain. + RUN python -m venv /app/venv + + # Install dependencies into the virtual environment using a cache mount + # to speed up subsequent builds. + RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + pip install -r requirements.txt + + # Runtime stage: the minimal DHI image has no shell, no package manager, + # and already runs as the nonroot user. No manual user setup required. + FROM dhi.io/python:3.12-alpine3.21 + + WORKDIR /app + + # Prevent Python from buffering stdout/stderr so logs appear immediately. + ENV PYTHONUNBUFFERED=1 + # Activate the virtual environment copied from the build stage. + ENV PATH="/app/venv/bin:$PATH" + + # Copy application source code and the pre-built virtual environment + # from the builder stage. + COPY . . + COPY --from=builder /app/venv /app/venv + + EXPOSE 8000 + + # Run Gunicorn as the production WSGI server. + CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] + ``` {{< /tab >}} {{< /tabs >}} @@ -361,7 +364,7 @@ services: ports: - "8000:8000" environment: - # Enable Django's debug mode for detailed error pages and auto-reload. + # Enable Django's verbose debug error pages (the dev server always auto-reloads). - DEBUG=1 # Database connection settings passed to Django via environment variables. - POSTGRES_DB=myapp @@ -470,11 +473,13 @@ $ docker run --rm \ {{< /tab >}} {{< /tabs >}} -Then update the `DATABASES` setting in `myapp/settings.py` to read from environment variables: +Then update `myapp/settings.py` to read `DEBUG` and `DATABASES` from environment variables: ```python {title="myapp/settings.py"} import os +DEBUG = os.environ.get('DEBUG', '0') == '1' + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", From 2e4b860f9d1ea029adb07bcf6b080c15c1f22eaf Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Wed, 22 Apr 2026 12:25:13 -0700 Subject: [PATCH 4/8] add dev db note Signed-off-by: Craig Osterhout --- content/guides/django.md | 1 + 1 file changed, 1 insertion(+) diff --git a/content/guides/django.md b/content/guides/django.md index ce7b57a9432..ae3390a6d02 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -391,6 +391,7 @@ services: - action: rebuild path: requirements.txt db: + # Official image; use a Docker Hardened Image in production (hub.docker.com/hardened-images/catalog/dhi/postgres). image: postgres:17 restart: always # Run as the postgres user rather than root. From 36b801dbdae9695a5b6546f5a82dcef4645db161 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 23 Apr 2026 10:17:21 -0700 Subject: [PATCH 5/8] feedback1 Signed-off-by: Craig Osterhout --- content/guides/django.md | 441 +++++++++++++++------------------------ 1 file changed, 171 insertions(+), 270 deletions(-) diff --git a/content/guides/django.md b/content/guides/django.md index ae3390a6d02..015ef93e389 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -2,12 +2,12 @@ title: Containerize a Django application linkTitle: Django description: Learn how to containerize a Django application using Docker. -keywords: python, django, containerize, initialize, gunicorn, compose watch +keywords: python, django, containerize, initialize, gunicorn, compose watch, uv summary: | This guide shows how to containerize a Django application using Docker. - You'll scaffold the project with a bind-mount container, create a - production-ready Dockerfile with docker init, then add a development - stage and Compose Watch for fast iteration. + You'll scaffold the project with uv, create a production-ready Dockerfile + using a Docker Hardened Image, then add a development stage and Compose Watch + for fast iteration. languages: [python] tags: [dhi] params: @@ -16,8 +16,10 @@ params: ## Prerequisites -- You have installed the latest version of [Docker Desktop](/get-started/get-docker.md). -- Python does not need to be installed on your local machine. You'll use a container to initialize the project. +- You have installed the latest version of [Docker + Desktop](/get-started/get-docker.md). +- You have [uv](https://docs.astral.sh/uv/) installed, or you can use Docker to + scaffold the project without a local Python or uv installation. > [!TIP] > @@ -29,125 +31,90 @@ params: ## Overview -This guide walks you through containerizing a Django application with Docker. By the end, you will: +This guide walks you through containerizing a Django application with Docker. By +the end, you will: -- Initialize a Django project using a container with a bind mount, with no local Python install required. -- Create a production-ready Dockerfile using `docker init`. -- Add a `development` stage to your Dockerfile and configure Compose Watch for automatic code syncing. +- Initialize a Django project using uv, either locally or inside a Docker + Hardened Image container. +- Create a production-ready Dockerfile using [Docker Hardened Images + (DHI)](/dhi/). +- Add a `development` stage to your Dockerfile and configure Compose Watch for + automatic code syncing. --- ## Create the Django project -Rather than cloning a sample application, you'll use a Python container with a bind mount to scaffold a new Django project directly on your local machine. +You can bootstrap the project with a local uv installation, or entirely inside a +container using the same DHI image the Dockerfile uses, with no local Python +required. -1. Create a new project directory and navigate to it: +{{< tabs >}} {{< tab name="Local (uv)" >}} + +1. Initialize the project pinned to Python 3.12, then navigate into it: ```console - $ mkdir django-docker-example && cd django-docker-example + $ uv init --python 3.12 django-docker + $ cd django-docker ``` -2. Run a Python container with a bind mount to initialize the project. The `--mount` flag makes the generated files appear on your host machine: - - {{< tabs >}} - {{< tab name="Mac / Linux" >}} +2. Add Django and Gunicorn, then scaffold the Django project: ```console - $ docker run --rm \ - --mount type=bind,src=.,target=/app \ - -w /app \ - python:3.12-slim \ - sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + $ uv add django gunicorn + $ uv run django-admin startproject myapp . ``` - {{< /tab >}} - {{< tab name="PowerShell" >}} +{{< /tab >}} {{< tab name="Container (DHI)" >}} - ```powershell - $ docker run --rm ` - --mount "type=bind,src=.,target=/app" ` - -w /app ` - python:3.12-slim ` - sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" - ``` +The DHI dev image already has Python 3.12, so the bootstrapped project will +match the Dockerfile exactly. - {{< /tab >}} - {{< tab name="Command Prompt" >}} +1. Create the project directory and navigate into it: ```console - $ docker run --rm ^ - --mount "type=bind,src=%cd%,target=/app" ^ - -w /app ^ - python:3.12-slim ^ - sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + $ mkdir django-docker && cd django-docker ``` - {{< /tab >}} - {{< tab name="Git Bash" >}} +2. Initialize the project, add dependencies, and scaffold. All in one container + run: ```console - $ docker run --rm \ - --mount type=bind,src="./",target=/app \ - -w //app \ - python:3.12-slim \ - sh -c "pip install --quiet django gunicorn && django-admin startproject myapp . && pip freeze > requirements.txt" + $ docker run --rm -v $PWD:$PWD -w $PWD \ + dhi.io/python:3.12-alpine3.22-dev \ + sh -c "pip install --quiet --root-user-action=ignore uv && export UV_LINK_MODE=copy && uv init --name django-docker --python 3.12 . && uv add django gunicorn && uv run django-admin startproject myapp ." ``` - {{< /tab >}} - {{< /tabs >}} + > [!NOTE] The above command uses Mac/Linux shell syntax. On Windows, adjust + > the path: PowerShell uses `${PWD}`, Command Prompt uses `%cd%`, Git Bash + > requires `MSYS_NO_PATHCONV=1` with `$(pwd -W)`. - This command installs Django and Gunicorn inside the container, scaffolds a new Django project in the current directory, and writes the installed packages to `requirements.txt`. +{{< /tab >}} {{< /tabs >}} Your directory should now contain the following files: ```text -├── django-docker-example/ -│ ├── manage.py -│ ├── myapp/ -│ │ ├── __init__.py -│ │ ├── asgi.py -│ │ ├── settings.py -│ │ ├── urls.py -│ │ └── wsgi.py -│ └── requirements.txt +├── .python-version +├── main.py +├── manage.py +├── myapp/ +│ ├── __init__.py +│ ├── asgi.py +│ ├── settings.py +│ ├── urls.py +│ └── wsgi.py +├── pyproject.toml +├── uv.lock +└── README.md ``` --- ## Create a production Dockerfile -Use `docker init` to generate a production-ready `Dockerfile`, `.dockerignore`, and `compose.yaml`. - -Inside the `django-docker-example` directory, run: - -```console -$ docker init -``` - -When prompted, answer as follows: - -| Question | Answer | -|---|---| -| What application platform does your project use? | Python | -| What version of Python do you want to use? | 3.12 | -| What port do you want your app to listen on? | 8000 | -| What is the command to run your app? | `gunicorn myapp.wsgi:application --bind 0.0.0.0:8000` | - -`docker init` generates a production-ready `Dockerfile` using the [Python Docker Official Image](https://hub.docker.com/_/python). No changes are needed to use it as-is. - -If you'd prefer a more secure, minimal base image, you can instead use a [Docker Hardened Image (DHI)](https://hub.docker.com/hardened-images/catalog/dhi/python). Docker Hardened Images are production-ready base images maintained by Docker that minimize attack surface. For more details, see [Docker Hardened Images](/dhi/). - -{{< tabs >}} -{{< tab name="Docker Official Image" >}} - -The `Dockerfile` generated by `docker init` is ready to use. It already sets up a non-privileged user, uses a cache mount to speed up `pip` installs, and runs Gunicorn as the production server. No further edits are needed. - -{{< /tab >}} -{{< tab name="Docker Hardened Image" >}} - -Docker Hardened Images for Python are available in the [DHI catalog](https://hub.docker.com/hardened-images/catalog/dhi/python) and are freely available to all Docker users. - -DHI follows a multi-stage build pattern: a `-dev` image (which includes pip and build tools) is used to install dependencies into a virtual environment, and then a minimal runtime image (no `-dev`) is used for the final stage. The runtime image has no shell, no package manager, and already runs as the `nonroot` user, so no manual user setup is needed. +Docker Hardened Images are production-ready base images maintained by Docker +that minimize attack surface. For more details, see [Docker Hardened +Images](/dhi/). 1. Sign in to the DHI registry: @@ -155,68 +122,83 @@ DHI follows a multi-stage build pattern: a `-dev` image (which includes pip and $ docker login dhi.io ``` -2. Replace the contents of your `Dockerfile` with the following: +2. Create a `.dockerignore` file to exclude local artifacts from the build + context: + + ```text {title=".dockerignore"} + .venv/ + __pycache__/ + *.pyc + .git/ + ``` + +3. Create a `Dockerfile` with the following contents: ```dockerfile {title="Dockerfile"} # syntax=docker/dockerfile:1 - - # Build stage: the -dev image includes pip and build tools needed to - # install Python packages into a virtual environment. - FROM dhi.io/python:3.12-alpine3.21-dev AS builder - + + # Build stage: the -dev image includes tools needed to install packages. + FROM dhi.io/python:3.12-alpine3.22-dev AS builder + # Prevent Python from writing .pyc files to disk. ENV PYTHONDONTWRITEBYTECODE=1 # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 - # Activate the virtual environment for all subsequent RUN commands. - ENV PATH="/app/venv/bin:$PATH" - - WORKDIR /app - - # Create a virtual environment so only the venv needs to be copied to - # the runtime stage, not the full dev toolchain. - RUN python -m venv /app/venv - - # Install dependencies into the virtual environment using a cache mount - # to speed up subsequent builds. - RUN --mount=type=cache,target=/root/.cache/pip \ - --mount=type=bind,source=requirements.txt,target=requirements.txt \ - pip install -r requirements.txt - - # Runtime stage: the minimal DHI image has no shell, no package manager, - # and already runs as the nonroot user. No manual user setup required. - FROM dhi.io/python:3.12-alpine3.21 - + + RUN pip install --quiet --root-user-action=ignore uv + # Use copy mode since the cache and build filesystem are on different volumes. + ENV UV_LINK_MODE=copy + WORKDIR /app - + + # Install dependencies into a virtual environment using cache and bind mounts + # so neither uv nor the lock files need to be copied into the image. + RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project + + # Runtime stage: minimal DHI image with no shell or package manager, + # already runs as the nonroot user. + FROM dhi.io/python:3.12-alpine3.22 + # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 # Activate the virtual environment copied from the build stage. - ENV PATH="/app/venv/bin:$PATH" - - # Copy application source code and the pre-built virtual environment - # from the builder stage. + ENV PATH="/app/.venv/bin:$PATH" + + WORKDIR /app + + # Copy the pre-built virtual environment and application source code. + COPY --from=builder /app/.venv /app/.venv COPY . . - COPY --from=builder /app/venv /app/venv - + EXPOSE 8000 - + # Run Gunicorn as the production WSGI server. CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] ``` -{{< /tab >}} -{{< /tabs >}} +4. Create a `compose.yaml` file: + + ```yaml {title="compose.yaml"} + services: + web: + build: . + ports: + - "8000:8000" + ``` ### Run the application -From the `django-docker-example` directory, run: +From the `django-docker` directory, run: ```console $ docker compose up --build ``` -Open a browser and navigate to [http://localhost:8000](http://localhost:8000). You should see the Django welcome page. +Open a browser and navigate to [http://localhost:8000](http://localhost:8000). +You should see the Django welcome page. Press `ctrl`+`c` to stop the application. @@ -224,122 +206,64 @@ Press `ctrl`+`c` to stop the application. ## Set up a development environment -The production setup uses Gunicorn and requires a full image rebuild to pick up code changes. For development, you can add a `development` stage to your Dockerfile that uses Django's built-in server, and configure Compose Watch to automatically sync code changes into the running container without a rebuild. +The production setup uses Gunicorn and requires a full image rebuild to pick up +code changes. For development, you can add a `development` stage to your +Dockerfile that uses Django's built-in server, and configure Compose Watch to +automatically sync code changes into the running container without a rebuild. ### Update the Dockerfile -Replace your `Dockerfile` with a multi-stage version that adds a `development` stage alongside `production`. Select the tab that matches your base image choice from the previous section: - -{{< tabs >}} -{{< tab name="Docker Official Image" >}} - -A shared `base` stage installs dependencies, then branches into a `development` stage and a `production` stage: +Replace your `Dockerfile` with a multi-stage version that adds a `development` +stage alongside `production`: ```dockerfile {title="Dockerfile"} # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.12 -# The base stage is shared by both development and production. It installs -# dependencies once so both stages benefit from the same layer cache. -FROM python:${PYTHON_VERSION}-slim AS base +# Build stage: the -dev image includes tools needed to install packages. +FROM dhi.io/python:3.12-alpine3.22-dev AS builder # Prevent Python from writing .pyc files to disk. ENV PYTHONDONTWRITEBYTECODE=1 # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 -WORKDIR /app - -# Create a non-privileged user to run the application. -# See https://docs.docker.com/go/dockerfile-user-best-practices/ -ARG UID=10001 -RUN adduser \ - --disabled-password \ - --gecos "" \ - --home "/nonexistent" \ - --shell "/sbin/nologin" \ - --no-create-home \ - --uid "${UID}" \ - appuser - -# Install dependencies using a cache mount to speed up subsequent builds and -# a bind mount so requirements.txt doesn't need to be copied into the image. -RUN --mount=type=cache,target=/root/.cache/pip \ - --mount=type=bind,source=requirements.txt,target=requirements.txt \ - python -m pip install -r requirements.txt - -# Copy application source code into the image. -COPY . . - -# The development stage uses Django's built-in server, which reloads -# automatically when Compose Watch syncs source file changes into the container. -FROM base AS development -USER appuser -EXPOSE 8000 -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] - -# The production stage uses Gunicorn, a production-grade WSGI server. -FROM base AS production -USER appuser -EXPOSE 8000 -CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] -``` - -{{< /tab >}} -{{< tab name="Docker Hardened Image" >}} - -The `development` stage extends directly from `builder`, inheriting the `-dev` image, the virtual environment, and the application code. The `production` stage uses the minimal runtime image as before: - -```dockerfile {title="Dockerfile"} -# syntax=docker/dockerfile:1 - -# Build stage: the -dev image includes pip and build tools needed to -# install Python packages into a virtual environment. -FROM dhi.io/python:3.12-alpine3.21-dev AS builder - -# Prevent Python from writing .pyc files to disk. -ENV PYTHONDONTWRITEBYTECODE=1 -# Prevent Python from buffering stdout/stderr so logs appear immediately. -ENV PYTHONUNBUFFERED=1 -# Activate the virtual environment for all subsequent RUN commands. -ENV PATH="/app/venv/bin:$PATH" +RUN pip install --quiet --root-user-action=ignore uv +# Use copy mode since the cache and build filesystem are on different volumes. +ENV UV_LINK_MODE=copy WORKDIR /app -# Create a virtual environment so only the venv needs to be copied to -# the runtime stage, not the full dev toolchain. -RUN python -m venv /app/venv +# Install dependencies into a virtual environment using cache and bind mounts +# so neither uv nor the lock files need to be copied into the image. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project -# Install dependencies into the virtual environment using a cache mount -# to speed up subsequent builds. -RUN --mount=type=cache,target=/root/.cache/pip \ - --mount=type=bind,source=requirements.txt,target=requirements.txt \ - pip install -r requirements.txt +# The development stage inherits the -dev image and virtual environment from +# the builder. Django's built-in server reloads when Compose Watch syncs files. +FROM builder AS development -# Copy application source code into the image. -COPY . . +ENV PATH="/app/.venv/bin:$PATH" -# The development stage inherits the -dev image and virtual environment -# from the builder, and runs Django's built-in server which reloads -# when Compose Watch syncs source file changes into the container. -FROM builder AS development +COPY . . +EXPOSE 8000 CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] # The production stage uses the minimal runtime image, which has no shell, # no package manager, and already runs as the nonroot user. -FROM dhi.io/python:3.12-alpine3.21 AS production - -WORKDIR /app +FROM dhi.io/python:3.12-alpine3.22 AS production # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 # Activate the virtual environment copied from the build stage. -ENV PATH="/app/venv/bin:$PATH" +ENV PATH="/app/.venv/bin:$PATH" + +WORKDIR /app -# Copy only the application code and the pre-built virtual environment -# from the builder stage. +# Copy only the pre-built virtual environment and application source code. +COPY --from=builder /app/.venv /app/.venv COPY . . -COPY --from=builder /app/venv /app/venv EXPOSE 8000 @@ -347,12 +271,10 @@ EXPOSE 8000 CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"] ``` -{{< /tab >}} -{{< /tabs >}} - ### Update the Compose file -Replace your `compose.yaml` with the following. It targets the `development` stage, adds a PostgreSQL database, and configures Compose Watch: +Replace your `compose.yaml` with the following. It targets the `development` +stage, adds a PostgreSQL database, and configures Compose Watch: ```yaml {title="compose.yaml"} services: @@ -387,18 +309,18 @@ services: - __pycache__/ - "*.pyc" - .git/ + - .venv/ # Rebuild the image when dependencies change. - action: rebuild - path: requirements.txt + path: pyproject.toml + - action: rebuild + path: uv.lock db: - # Official image; use a Docker Hardened Image in production (hub.docker.com/hardened-images/catalog/dhi/postgres). - image: postgres:17 + image: dhi.io/postgres:18 restart: always - # Run as the postgres user rather than root. - user: postgres volumes: # Persist database data across container restarts. - - db-data:/var/lib/postgresql/data + - db-data:/var/lib/postgresql environment: - POSTGRES_DB=myapp - POSTGRES_USER=postgres @@ -418,63 +340,35 @@ volumes: db-data: ``` -The `sync` action pushes file changes directly into the running container so Django's dev server reloads them automatically. A change to `requirements.txt` triggers a full image rebuild instead. +The `sync` action pushes file changes directly into the running container so +Django's dev server reloads them automatically. A change to `pyproject.toml` or +`uv.lock` triggers a full image rebuild instead. -> [!NOTE] -> To learn more about Compose Watch, see [Use Compose Watch](/manuals/compose/how-tos/file-watch.md). +> [!NOTE] To learn more about Compose Watch, see [Use Compose +> Watch](/manuals/compose/how-tos/file-watch.md). ### Add the PostgreSQL driver -Add the `psycopg` adapter to your project using a bind-mount container, the same approach you used to initialize the project: - -{{< tabs >}} -{{< tab name="Mac / Linux" >}} - -```console -$ docker run --rm \ - --mount type=bind,src=.,target=/app \ - -w /app \ - python:3.12-slim \ - sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" -``` - -{{< /tab >}} -{{< tab name="PowerShell" >}} - -```powershell -$ docker run --rm ` - --mount "type=bind,src=.,target=/app" ` - -w /app ` - python:3.12-slim ` - sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" -``` +Add the `psycopg` adapter to your project: -{{< /tab >}} -{{< tab name="Command Prompt" >}} +{{< tabs >}} {{< tab name="Local (uv)" >}} ```console -$ docker run --rm ^ - --mount "type=bind,src=%cd%,target=/app" ^ - -w /app ^ - python:3.12-slim ^ - sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +$ uv add 'psycopg[binary]' ``` -{{< /tab >}} -{{< tab name="Git Bash" >}} +{{< /tab >}} {{< tab name="Container (DHI)" >}} ```console -$ docker run --rm \ - --mount type=bind,src="./",target=/app \ - -w //app \ - python:3.12-slim \ - sh -c "pip install --quiet -r requirements.txt 'psycopg[binary]' && pip freeze > requirements.txt" +$ docker run --rm -v $PWD:$PWD -w $PWD \ + dhi.io/python:3.12-alpine3.22-dev \ + sh -c "pip install --quiet --root-user-action=ignore uv && UV_LINK_MODE=copy uv add 'psycopg[binary]'" ``` -{{< /tab >}} -{{< /tabs >}} +{{< /tab >}} {{< /tabs >}} -Then update `myapp/settings.py` to read `DEBUG` and `DATABASES` from environment variables: +Then update `myapp/settings.py` to read `DEBUG` and `DATABASES` from environment +variables: ```python {title="myapp/settings.py"} import os @@ -503,7 +397,10 @@ $ docker compose watch Open a browser and navigate to [http://localhost:8000](http://localhost:8000). -Try editing a file, for example add a view to `myapp/views.py`. Compose Watch syncs the change into the container and Django's dev server reloads automatically. If you add a package to `requirements.txt`, Compose Watch triggers a full image rebuild. +Try editing a file, for example add a view to `myapp/views.py`. Compose Watch +syncs the change into the container and Django's dev server reloads +automatically. If you update `pyproject.toml` or `uv.lock`, Compose Watch +triggers a full image rebuild. Press `ctrl`+`c` to stop. @@ -513,9 +410,12 @@ Press `ctrl`+`c` to stop. In this guide, you: -- Bootstrapped a Django project using a Docker container and a bind mount, with no local Python installation required. -- Used `docker init` to generate Docker assets and updated the `Dockerfile` to use Gunicorn for production. -- Added a `development` stage to the `Dockerfile` and configured Compose Watch for fast iterative development with a PostgreSQL database. +- Bootstrapped a Django project using uv, with options for both local and + containerized setup. +- Created a production-ready Dockerfile using Docker Hardened Images and uv for + dependency management. +- Added a `development` stage to the `Dockerfile` and configured Compose Watch + for fast iterative development with a PostgreSQL database. Related information: @@ -524,3 +424,4 @@ Related information: - [Use Compose Watch](/manuals/compose/how-tos/file-watch.md) - [Docker Hardened Images](/dhi/) - [Multi-stage builds](/manuals/build/building/multi-stage.md) +- [uv documentation](https://docs.astral.sh/uv/) From 935fcd92cb42633aa45e41ac230fe19c30c60493 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 23 Apr 2026 10:22:30 -0700 Subject: [PATCH 6/8] fix notes Signed-off-by: Craig Osterhout --- content/guides/django.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/content/guides/django.md b/content/guides/django.md index 015ef93e389..c78815a8070 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -85,7 +85,9 @@ match the Dockerfile exactly. sh -c "pip install --quiet --root-user-action=ignore uv && export UV_LINK_MODE=copy && uv init --name django-docker --python 3.12 . && uv add django gunicorn && uv run django-admin startproject myapp ." ``` - > [!NOTE] The above command uses Mac/Linux shell syntax. On Windows, adjust + > [!NOTE] + > + > The above command uses Mac/Linux shell syntax. On Windows, adjust > the path: PowerShell uses `${PWD}`, Command Prompt uses `%cd%`, Git Bash > requires `MSYS_NO_PATHCONV=1` with `$(pwd -W)`. @@ -344,7 +346,9 @@ The `sync` action pushes file changes directly into the running container so Django's dev server reloads them automatically. A change to `pyproject.toml` or `uv.lock` triggers a full image rebuild instead. -> [!NOTE] To learn more about Compose Watch, see [Use Compose +> [!NOTE] +> +> To learn more about Compose Watch, see [Use Compose > Watch](/manuals/compose/how-tos/file-watch.md). ### Add the PostgreSQL driver From 9c47af51369828ae8ed7aa3a209509b450822509 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 23 Apr 2026 10:29:57 -0700 Subject: [PATCH 7/8] vale nit Signed-off-by: Craig Osterhout --- content/guides/django.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/guides/django.md b/content/guides/django.md index c78815a8070..abf074e0a58 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -87,7 +87,7 @@ match the Dockerfile exactly. > [!NOTE] > - > The above command uses Mac/Linux shell syntax. On Windows, adjust + > The previous command uses Mac/Linux shell syntax. On Windows, adjust > the path: PowerShell uses `${PWD}`, Command Prompt uses `%cd%`, Git Bash > requires `MSYS_NO_PATHCONV=1` with `$(pwd -W)`. From 4cbdcc1fa36c1b34180ea51cc6a13fcfcf583c17 Mon Sep 17 00:00:00 2001 From: Craig Osterhout Date: Thu, 23 Apr 2026 13:48:27 -0700 Subject: [PATCH 8/8] feedback2 Signed-off-by: Craig Osterhout --- content/guides/django.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/content/guides/django.md b/content/guides/django.md index abf074e0a58..fa242ece841 100644 --- a/content/guides/django.md +++ b/content/guides/django.md @@ -51,10 +51,10 @@ required. {{< tabs >}} {{< tab name="Local (uv)" >}} -1. Initialize the project pinned to Python 3.12, then navigate into it: +1. Initialize the project pinned to Python 3.14, then navigate into it: ```console - $ uv init --python 3.12 django-docker + $ uv init --python 3.14 django-docker $ cd django-docker ``` @@ -67,7 +67,7 @@ required. {{< /tab >}} {{< tab name="Container (DHI)" >}} -The DHI dev image already has Python 3.12, so the bootstrapped project will +The DHI dev image already has Python 3.14, so the bootstrapped project will match the Dockerfile exactly. 1. Create the project directory and navigate into it: @@ -81,8 +81,9 @@ match the Dockerfile exactly. ```console $ docker run --rm -v $PWD:$PWD -w $PWD \ - dhi.io/python:3.12-alpine3.22-dev \ - sh -c "pip install --quiet --root-user-action=ignore uv && export UV_LINK_MODE=copy && uv init --name django-docker --python 3.12 . && uv add django gunicorn && uv run django-admin startproject myapp ." + -e UV_LINK_MODE=copy \ + dhi.io/python:3.14-alpine3.23-dev \ + sh -c "pip install --quiet --root-user-action=ignore uv && uv init --name django-docker --python 3.14 . && uv add django gunicorn && uv run django-admin startproject myapp ." ``` > [!NOTE] @@ -140,7 +141,7 @@ Images](/dhi/). # syntax=docker/dockerfile:1 # Build stage: the -dev image includes tools needed to install packages. - FROM dhi.io/python:3.12-alpine3.22-dev AS builder + FROM dhi.io/python:3.14-alpine3.23-dev AS builder # Prevent Python from writing .pyc files to disk. ENV PYTHONDONTWRITEBYTECODE=1 @@ -162,7 +163,7 @@ Images](/dhi/). # Runtime stage: minimal DHI image with no shell or package manager, # already runs as the nonroot user. - FROM dhi.io/python:3.12-alpine3.22 + FROM dhi.io/python:3.14-alpine3.23 # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 @@ -222,7 +223,7 @@ stage alongside `production`: # syntax=docker/dockerfile:1 # Build stage: the -dev image includes tools needed to install packages. -FROM dhi.io/python:3.12-alpine3.22-dev AS builder +FROM dhi.io/python:3.14-alpine3.23-dev AS builder # Prevent Python from writing .pyc files to disk. ENV PYTHONDONTWRITEBYTECODE=1 @@ -254,7 +255,7 @@ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] # The production stage uses the minimal runtime image, which has no shell, # no package manager, and already runs as the nonroot user. -FROM dhi.io/python:3.12-alpine3.22 AS production +FROM dhi.io/python:3.14-alpine3.23 AS production # Prevent Python from buffering stdout/stderr so logs appear immediately. ENV PYTHONUNBUFFERED=1 @@ -365,8 +366,9 @@ $ uv add 'psycopg[binary]' ```console $ docker run --rm -v $PWD:$PWD -w $PWD \ - dhi.io/python:3.12-alpine3.22-dev \ - sh -c "pip install --quiet --root-user-action=ignore uv && UV_LINK_MODE=copy uv add 'psycopg[binary]'" + -e UV_LINK_MODE=copy \ + dhi.io/python:3.14-alpine3.23-dev \ + sh -c "pip install --quiet --root-user-action=ignore uv && uv add 'psycopg[binary]'" ``` {{< /tab >}} {{< /tabs >}}