Skip to content

Dockerfile

Docker is a popular choice for modern application deployment. However, creating a good Dockerfile from scratch can be challenging. This guide provides a solid foundation that works well for most Python projects.

While the example below won't fit every use case, it offers an excellent starting point that you can adapt to your specific needs.

Quickstart

For this example, we'll need to install docker, docker-compose and uv.

Then, let's create a new project with uv:

uv init app

This will create a new project with a basic structure:

app/
├── main.py
├── pyproject.toml
└── README.md

On main.py, let's create a simple ASGI application:

main.py
async def app(scope, receive, send):
    body = "Hello, world!"
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [
                [b"content-type", b"text/plain"],
                [b"content-length", len(body)],
            ],
        }
    )
    await send(
        {
            "type": "http.response.body",
            "body": body.encode("utf-8"),
        }
    )

We need to include uvicorn in the dependencies:

uv add uvicorn

This will also create a uv.lock file. 😎

What is uv.lock?

uv.lock is a uv specific lockfile. A lockfile is a file that contains the exact versions of the dependencies that were installed when the uv.lock file was created.

This allows for deterministic builds and consistent deployments.

Just to make sure everything is working, let's run the application:

uv run uvicorn main:app

You should see the following output:

INFO:     Started server process [62727]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Dockerfile

We'll create a cache-aware Dockerfile that optimizes build times. The key strategy is to install dependencies first, then copy the project files. This approach leverages Docker's caching mechanism to significantly speed up rebuilds.

Dockerfile
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Change the working directory to the `app` directory
WORKDIR /app

# Install dependencies
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

# Copy the project into the image
ADD . /app

# Sync the project
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen

# Run with uvicorn
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

A common question is "how many workers should I run?". The image above uses a single Uvicorn worker. The recommended approach is to let your orchestration system manage the number of deployed containers rather than relying on the process manager inside the container.

You can read more about this in the Decouple applications section of the Docker documentation.

For production, create a non-root user!

When running in production, you should create a non-root user and run the container as that user.

To make sure it works, let's build the image and run it:

docker build -t my-app .
docker run -p 8000:8000 my-app

For more information on using uv with Docker, refer to the official uv Docker integration guide.

Docker Compose

When running in development, it's often useful to have a way to hot-reload the application when code changes.

Let's create a docker-compose.yml file to run the application:

docker-compose.yml
services:
  backend:
    build: .
    ports:
      - "8000:8000"
    environment:
      - UVICORN_RELOAD=true
    volumes:
      - .:/app
    tty: true

You can run the application with docker compose up and it will automatically rebuild the image when code changes.

Now you have a fully working development environment! 🎉