Skip to content

Commit f2192cc

Browse files
authored
docs: add section about Docker under Deployment (#2610)
1 parent 7fb029d commit f2192cc

5 files changed

Lines changed: 176 additions & 37 deletions

File tree

docs/deployment/docker.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Dockerfile
2+
3+
**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.
4+
5+
While the example below won't fit every use case, it offers an excellent starting point that you can adapt to your specific needs.
6+
7+
8+
## Quickstart
9+
10+
For this example, we'll need to install [`docker`](https://docs.docker.com/get-docker/),
11+
[docker-compose](https://docs.docker.com/compose/install/) and
12+
[`uv`](https://docs.astral.sh/uv/getting-started/installation/).
13+
14+
Then, let's create a new project with `uv`:
15+
16+
```bash
17+
uv init app
18+
```
19+
20+
This will create a new project with a basic structure:
21+
22+
```bash
23+
app/
24+
├── main.py
25+
├── pyproject.toml
26+
└── README.md
27+
```
28+
29+
On `main.py`, let's create a simple ASGI application:
30+
31+
```python title="main.py"
32+
async def app(scope, receive, send):
33+
body = "Hello, world!"
34+
await send(
35+
{
36+
"type": "http.response.start",
37+
"status": 200,
38+
"headers": [
39+
[b"content-type", b"text/plain"],
40+
[b"content-length", len(body)],
41+
],
42+
}
43+
)
44+
await send(
45+
{
46+
"type": "http.response.body",
47+
"body": body.encode("utf-8"),
48+
}
49+
)
50+
```
51+
52+
We need to include `uvicorn` in the dependencies:
53+
54+
```bash
55+
uv add uvicorn
56+
```
57+
58+
This will also create a `uv.lock` file. :sunglasses:
59+
60+
??? tip "What is `uv.lock`?"
61+
62+
`uv.lock` is a `uv` specific lockfile. A lockfile is a file that contains the exact versions of the dependencies
63+
that were installed when the `uv.lock` file was created.
64+
65+
This allows for deterministic builds and consistent deployments.
66+
67+
Just to make sure everything is working, let's run the application:
68+
69+
```bash
70+
uv run uvicorn main:app
71+
```
72+
73+
You should see the following output:
74+
75+
```bash
76+
INFO: Started server process [62727]
77+
INFO: Waiting for application startup.
78+
INFO: ASGI 'lifespan' protocol appears unsupported.
79+
INFO: Application startup complete.
80+
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
81+
```
82+
83+
## Dockerfile
84+
85+
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.
86+
87+
```dockerfile title="Dockerfile"
88+
FROM python:3.12-slim
89+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
90+
91+
# Change the working directory to the `app` directory
92+
WORKDIR /app
93+
94+
# Install dependencies
95+
RUN --mount=type=cache,target=/root/.cache/uv \
96+
--mount=type=bind,source=uv.lock,target=uv.lock \
97+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
98+
uv sync --frozen --no-install-project
99+
100+
# Copy the project into the image
101+
ADD . /app
102+
103+
# Sync the project
104+
RUN --mount=type=cache,target=/root/.cache/uv \
105+
uv sync --frozen
106+
107+
# Run with uvicorn
108+
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
109+
```
110+
111+
A common question is **"how many workers should I run?"**. The image above uses a single Uvicorn worker.
112+
The recommended approach is to let your orchestration system manage the number of deployed containers rather than
113+
relying on the process manager inside the container.
114+
115+
You can read more about this in the
116+
[Decouple applications](https://docs.docker.com/build/building/best-practices/#decouple-applications) section
117+
of the Docker documentation.
118+
119+
!!! warning "For production, create a non-root user!"
120+
When running in production, you should create a non-root user and run the container as that user.
121+
122+
To make sure it works, let's build the image and run it:
123+
124+
```bash
125+
docker build -t my-app .
126+
docker run -p 8000:8000 my-app
127+
```
128+
129+
For more information on using uv with Docker, refer to the
130+
[official uv Docker integration guide](https://docs.astral.sh/uv/guides/integration/docker/).
131+
132+
## Docker Compose
133+
134+
When running in development, it's often useful to have a way to hot-reload the application when code changes.
135+
136+
Let's create a `docker-compose.yml` file to run the application:
137+
138+
```yaml title="docker-compose.yml"
139+
services:
140+
backend:
141+
build: .
142+
ports:
143+
- "8000:8000"
144+
environment:
145+
- UVICORN_RELOAD=true
146+
volumes:
147+
- .:/app
148+
tty: true
149+
```
150+
151+
You can run the application with `docker compose up` and it will automatically rebuild the image when code changes.
152+
153+
Now you have a fully working development environment! :tada:
Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To see the complete set of available options, use `uvicorn --help`:
2727
{{ uvicorn_help }}
2828
```
2929

30-
See the [settings documentation](settings.md) for more details on the supported options for running uvicorn.
30+
See the [settings documentation](../settings.md) for more details on the supported options for running uvicorn.
3131

3232
## Running programmatically
3333

@@ -141,28 +141,6 @@ stdout_logfile_maxbytes=0
141141

142142
Then run with `supervisord -n`.
143143

144-
### Circus
145-
146-
To use `circus` as a process manager, you should either:
147-
148-
* Hand over the socket to uvicorn using its file descriptor, which circus makes available as `$(circus.sockets.web)`.
149-
* Or use a UNIX domain socket for each `uvicorn` process.
150-
151-
A simple circus configuration might look something like this:
152-
153-
```ini title="circus.ini"
154-
[watcher:web]
155-
cmd = venv/bin/uvicorn --fd $(circus.sockets.web) main:App
156-
use_sockets = True
157-
numprocesses = 4
158-
159-
[socket:web]
160-
host = 0.0.0.0
161-
port = 8000
162-
```
163-
164-
Then run `circusd circus.ini`.
165-
166144
## Running behind Nginx
167145

168146
Using Nginx as a proxy in front of your Uvicorn processes may not be necessary, but is recommended for additional resilience. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.
@@ -175,7 +153,8 @@ When running your application behind one or more proxies you will want to make s
175153

176154
Here's how a simple Nginx configuration might look. This example includes setting proxy headers, and using a UNIX domain socket to communicate with the application server.
177155

178-
It also includes some basic configuration to forward websocket connections. For more info on this, check [Nginx recommendations][nginx_websocket].
156+
It also includes some basic configuration to forward websocket connections.
157+
For more info on this, check [Nginx recommendations](https://nginx.org/en/docs/http/websocket.html).
179158

180159
```conf
181160
http {
@@ -229,9 +208,9 @@ Content Delivery Networks can also be a low-effort way to provide HTTPS terminat
229208
## Running with HTTPS
230209

231210
To run uvicorn with https, a certificate and a private key are required.
232-
The recommended way to get them is using [Let's Encrypt][letsencrypt].
211+
The recommended way to get them is using [Let's Encrypt](https://letsencrypt.org/).
233212

234-
For local development with https, it's possible to use [mkcert][mkcert]
213+
For local development with https, it's possible to use [mkcert](https://github.com/FiloSottile/mkcert)
235214
to generate a valid certificate and private key.
236215

237216
```bash
@@ -246,10 +225,6 @@ It's also possible to use certificates with uvicorn's worker for gunicorn.
246225
$ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker main:app
247226
```
248227

249-
[nginx_websocket]: https://nginx.org/en/docs/http/websocket.html
250-
[letsencrypt]: https://letsencrypt.org/
251-
[mkcert]: https://github.com/FiloSottile/mkcert
252-
253228
## Proxies and Forwarded Headers
254229

255230
When running an application behind one or more proxies, certain information about the request is lost.

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ gunicorn example:app -w 4 -k uvicorn.workers.UvicornWorker
193193

194194
For a [PyPy][pypy] compatible configuration use `uvicorn.workers.UvicornH11Worker`.
195195

196-
For more information, see the [deployment documentation](deployment.md).
196+
For more information, see the [deployment documentation](deployment/index.md).
197197

198198
### Application factories
199199

docs/plugins/main.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import subprocess
5+
from functools import lru_cache
56

67
from mkdocs.config import Config
78
from mkdocs.structure.files import Files
@@ -32,10 +33,10 @@ def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) ->
3233

3334

3435
def uvicorn_print_help(markdown: str, page: Page) -> str:
35-
# if you don't filter to the specific route that needs this substitution, things will be very slow
36-
if page.file.src_uri not in ("index.md", "deployment.md"):
37-
return markdown
36+
return re.sub(r"{{ *uvicorn_help *}}", get_uvicorn_help(), markdown)
3837

38+
39+
@lru_cache
40+
def get_uvicorn_help():
3941
output = subprocess.run(["uvicorn", "--help"], capture_output=True, check=True)
40-
logfire_help = output.stdout.decode()
41-
return re.sub(r"{{ *uvicorn_help *}}", logfire_help, markdown)
42+
return output.stdout.decode()

mkdocs.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ theme:
2121
icon: "material/lightbulb-outline"
2222
name: "Switch to light mode"
2323
features:
24+
- search.suggest
25+
- search.highlight
26+
- content.tabs.link
27+
- content.code.annotate
2428
- content.code.copy # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#contentcodecopy
29+
- navigation.path
30+
- navigation.indexes
2531
- navigation.sections # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation
2632
- navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#back-to-top-button
33+
- navigation.tracking
2734
- navigation.footer # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#navigationfooter
2835
- toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#anchor-following
2936

@@ -40,7 +47,9 @@ validation:
4047
nav:
4148
- Introduction: index.md
4249
- Settings: settings.md
43-
- Deployment: deployment.md
50+
- Deployment:
51+
- Deployment: deployment/index.md
52+
- Docker: deployment/docker.md
4453
- Server Behavior: server-behavior.md
4554
- Release Notes: release-notes.md
4655
- Contributing: contributing.md
@@ -53,6 +62,7 @@ markdown_extensions:
5362
css_class: highlight
5463
- toc:
5564
permalink: true
65+
- pymdownx.details
5666
- pymdownx.inlinehilite
5767
- pymdownx.snippets
5868
- pymdownx.superfences

0 commit comments

Comments
 (0)