Python Web Applications
To reliably and safely serve a Python web application, such as one built with Flask or another web framework, needs some infrastructure around it. We’ll go through each step in detail, but the quick list is:
- Select which port(s) your container will expose (and, optionally, publish).
- Create and configure an Nginx Proxy container.
- Python WSGI/ASGI Server
Container Setup
By default, our Python containers don’t expose a port. You can change this in the container’s Settings. You can expose ports, and then select whether to publish them. When you save your changes, your container will spend a few seconds restarting.
Nginx Proxy
Unlike our web images, your Python + Miniconda service image needs to be configured with a separate Nginx proxy container to ensure that it serves as expected. This is not hard to do - the container comes with an example configuration for connecting to a NodeJS container, and the same principles apply here.
-
Create a container running the Nginx Proxy web image, and add an SSH user.
-
SSH into the container and edit the config file at
/container/config/nginx/sites-available/default
. -
Uncomment the ‘upstream’ portion of the config file.
-
Change the name of the upstream server from
nodejs44
to something relevant to your Python container (for examplepyapp
). You don’t need to use your container name or labels here. -
You’ll see
NODE_CONTAINER_HOSTNAME:8080;
- change this to show the name of your Python application container and the number of the port that you have opened. (Keep the colon:
and semi-colon:
in place.) -
Uncomment the example line that says
proxy_pass http://nodejs44/;
-
Change
nodejs44
to again match your upstream server name - using the same example, it should look likeproxy_pass http://pyapp/;
.
From your SSH session, run nginx -t
to ensure your changes are valid. You want to see this line:
You will also see two other messages, Permission denied
and configuration file test failed
. These are safe to ignore. They happen because because the SSH user in the container does not have permissions over the Nginx service. What’s important is that the syntax is ok. If it is, it’s safe to restart the Nginx Proxy container to apply the new configuration.
If you’d like to understand more about why you saw those two failure messages, read up on SSH User Privileges.
Python WSGI/ASGI Server
There are a number of WSGI (Web Server Gateway Interface) and ASGI (Asynchronous Server Gateway Interface) middleman servers designed to serve Python applications. Gunicorn is the best known option, and it’s recommended by most Python frameworks.
There are plenty of tutorials that outline how to configure Gunicorn (or any other gateway), but there are a few things to keep in mind:
- If you’re running an application that uses asynchronous calls or function definitions, you should use async-capable workers (such as Uvicorn) with Gunicorn as the master process.
- Gunicorn will, by default, serve a distinct application per worker instance. This means that if you’re using large datasets that are loaded into memory (such as a large language model), you will probably need to configure shared memory.
- If you’re using a database, especially using an ORM such as SQLAlchemy, ensure that you’re doing this in a thread-safe way. Django does this by default, Flask-SQLAlchemy solves this problem for you, and you can always use something like SQLAlchemy’s
sessionmaker
to ensure that sessions are thread-local.
Basic Example Deployment
Generally, you should follow the deployment instructions for your specific framework to configure your gateway server. An example step-by-step deployment for a very simple Flask app is included below.
We’ll assume that:
- Your application is in
/container/application
, - Your app is a file named
myapp.py
, - The Flask module is called
app
, - Your Conda environment is called
flaskapp
.
An example myapp.py
might look like this:
-
SSH into your Python container and activate your Conda environment:
-
Install the gateway package(s) you need alongside the framework, for example:
-
Edit the
/container/config/supervisord.conf
file. Either uncomment the example[program:pyapp]
block, or create a new one with the settings that you want. -
Make sure that your block is executing in the root directory for your application. We recommend leaving this as
/container/application
and setting your app up in there. -
Ensure that the command for your Supervisor program first activates the Conda environment using source, NOT conda (
source activate flaskapp
), and then runs your application via your WSGI server (python -m gunicorn --workers 2 --bind 0.0.0.0:80 myapp:app
). An explanation of what this does:python -m gunicorn
tells Python to load the Gunicorn module.--workers 2
tells Gunicorn to load up two service workers. You can change this based on available resources.--bind 0.0.0.0:80
tells Gunicorn to listen to all network traffic on port 80. If you’ve exposed a different port, change this to match.myapp:app
tells Guncorn to look for the Python file namedmyapp.py
and load app from it (in our example,app = Flask(__name__)
)
-
Chaining these into a single runtime command would make the line look like this:
-
Make any other changes you want to the Supervisor config, and then save them.
-
Reload Supervisor, either with
supervisorctl reload
or by restarting the container. -
Check the URL that your Nginx Proxy is listening for. Your example app should be live!