Docker Compose + nginx + uWSGI
I’ve been working on a personal project that serves static video files alongside a Python API and AI backend (more on that in the future). Each component is it’s own Docker container, managed by Docker Compose.
Philosophically, personal projects should teach modern tools and techniques without causing excessive time to be spent on details that would be important for high traffic applications.
So, to that end, I chose nginx as a modern option with widespread support and pyuwsgi. (Previously this project had used Lighttpd and FastCGI, but those configurations proved to be troublesome.) I would expect this combination to not be that unusual, but I found few examples, so I will share what’s worked for me.
Component Overview
This application has two basic things to serve: A Flask API, and static MP4 video clips, both served on the same host. Nginx will serve the static files directly, and then proxy the API via the uWSGI protocol to the uWSGI app running in it’s own Docker container.
uWSGI config
Configuration for the uWSGI service is stored in a uwsgi.ini
file, mostly to separate the concern from the Docker compose.yaml
, and is pretty simple:
uwsgi-socket = :8000
master = true:w
processes = 4
module = api:app
The module
line refers to the app
variable inside api.py
.
The main point is that we use uwsgi-socket
to match the corresponding uwsgi_pass
directive in the nginx config (below). If we wanted to use an http socket, we’d set http-socket
here and use proxy_pass
in the nginx config. Some articles suggest sharing a Docker volume to access a shared unix socket, but that seems brittle as it would require the two docker containers to run on the same host, which seems too strict.
nginx config
As mentioned above, we want to use the uWSGI protocol between the webserver and application. We then want that application to be mapped to our application directory, in this case watcher
. The caveat is that we need to rewrite the URL path to remove the watcher
path, since the application code does not know about that mapping, so we need a URL rewrite. (Without the rewrite rule, the entire URL will be passed to uWSGI just as the client calls it.)
The relevant http
section of ngnix.conf
is then:
http {
# ... other boilerplate ...
upstream api {
server api:8000;
}
server {
listen 80;
location /watcher {
include uwsgi_params;
uwsgi_pass api;
rewrite ^/watcher(/.*)$ $1 break;
}
location / {
root /data/video;
}
}
}
Here, server api
refers to the api
service in the Docker compose.yaml
.
Docker Compose
Now that the above is complete, all that’s left is to configure docker compose:
api:
build:
context: .
dockerfile: Dockerfile-io
profiles: ["web"]
command: uwsgi --ini /usr/local/etc/uwsgi.ini
volumes:
- ./data:/data
- ./log:/var/log
- ./etc:/usr/local/etc:ro
environment:
- WATCHER_CONFIG=/usr/local/etc/watcher.cfg
web:
image: nginx
profiles: ["web"]
volumes:
- ./data/video:/data/video:ro
- ./etc/nginx:/etc/nginx:ro
ports:
- "80:80"
environment:
- NGINX_HOST=example.com
- NGINX_PORT=80
Note that the api container has no ports
section, since nothing in that container is directly exposed to the world. Rather, the api
name is mapped on the internal Docker network, which is what we reference in the nginx.conf
above.
Furthermore, this compose file mounts the etc
directory from the host, rather than copying it into the container. That’s worked for my R&D project, but for environment reproducibility it should be copied in a production environment.
Run
That’s it. All that’s left is docker compose --profile web up -d
. Oh, and write the rest of the app. ;)