Deploying Uwsgi to host Dash apps
When I set up this site, I wanted to host web apps, not just static pages, and my initial goal was a Shiny Server. While somewhat challenging, the good news was that once it is working, just dropping a new directory into /srv/shiny-server/
and the new app springs to life.
I started making dash apps earlier this year. The various .ini files, systemctl services, etc. was considerably more complicated than following the instructions for a shiny server. I found some excellent blog posts that helped me configure my various files but I can never seem to rediscover the posts that helped me the most, so I’m going to walk through my setup here in hopes it helps others. I use nginx
so I assume you have a basic working nginx
install for a static web server. If you do not, there are plenty of tutorials on that, such as this one from Digital Ocean.
I have done this most recently on Ubuntu 20.04 LTS. I deployed uwsgi in emperor mode with each of my dash apps as a vassal, this makes it easier to deploy additional dash apps.
- Install UWSGI
- Configure the Emperor
- Set up your vassal(s)
- Set up the Emperor as a service
- Configure NGINX
- Adding additional apps
- Conclusion / Common pitfalls
Install UWSGI
It looks like I installed uwsgi with:
sudo apt-get install build-essential python3-dev
sudo pip3 install uwsgi
Configure the Emperor
The first step is configuring the emperor via an ini file. Mine very straigthforward, in /etc/uwsgi/emperor.ini
[uwsgi]
emperor = /etc/uwsgi/vassals
uid = www-data
gid = www-data
#plugins = logfile
logger = file:/var/log/uwsgi/emperor.log
You can probably test this via
sudo uwsgi --master --enable-threads --ini /etc/uwsgi/emperor.ini
Which hopefully runs and looks nominal. You’ll need to control-c to get out of this.
Set up your vassal(s)
Here is a vassal ini file for my dash dataframe live example.
[uwsgi]
socket = /srv/dash_dataframe_table/uwsgi.sock
chmod-socket=664
chdir = /srv/dash_dataframe_table/
binary-path = /usr/local/bin/uwsgi
module = example:server
uid = www-data
gid = www-data
processes = 1
threads = 1
master = true
logger = file:/var/log/uwsgi/dashtable.log
idle = 60
die-on-idle = true
cheap = true
reload-mercy=5
worker-reload-mercy=5
I also have a corresponding dataframe.socket
text file in /etc/uwsgi/vassals
that says simply has the text /srv/dash_dataframe_table/uwsgi.sock
. This is for the emperor on demand functionality. For a simple demo app like this, it’ll shutdown / be idle until it gets a connection. That’s the cheap/lazy/die-on-idle settings you see above.
The app code itself needs to be in /srv/dash_dataframe_table
. (The www-data
group needs to have read/write to everything in that folder.) The line module = example:server
tells it to look for the server object in the example.py file. It is important for dash apps that something in your source code say server = app.server
so that the Flask server itself is exposed.
At this point you should be able to test run emperor on the command line with:
$ /usr/local/bin/uwsgi --master --enable-threads --ini /etc/uwsgi/emperor.ini --emperor-on-demand-extension .socket
which should also launch and show it’s waiting for a socket connection. Cancel out of this with ctrl-c.
Set up the Emperor as a service
You want a service in systemd that runs the emperor automatically. My /etc/systemd/system/emperor.uwsgi.service
file looks like this:
[Unit]
Description=uWSGI Emperor
After=syslog.target
[Service]
ExecStart=/usr/local/bin/uwsgi --master --enable-threads --ini /etc/uwsgi/emperor.ini --emperor-on-demand-extension .socket
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all
[Install]
WantedBy=multi-user.target
You can now get the service running by sudo systemctl start emperor.uwsgi.service
Configure NGINX
You can add separate blocks like this to the nginx file; this hard codes a specific URL to your application:
location /dash/table_example/ {
include uwsgi_params;
uwsgi_pass unix:///srv/dash_dataframe_table/uwsgi.sock;
}
Test the file with ngnix -t
and then restart the server with (most likely):
sudo systemctl restart nginx.service
In this example, the dash app at /srv/dash_dataframe_table
is now at the url dash/table_example/
UPDATE: A better method (Originally updated: August 2022)
I have found a better system that doesn’t involved editing the nginx configuration every time.
location /dash/
{
include uwsgi_params;
if ($request_uri ~* "/dash/([a-zA-Z_]+)/*") {
uwsgi_pass unix:///srv/$1/uwsgi.sock;
}
}
Now so long as the URL and the app directory match, I don’t have to ever edit the configuration of nginx
. In this case, https://example.com/dash/cool_app/
will look for an socket in /srv/cool_app/uwsgi.sock
.
The code above restricts the directory names and URLs to letters and _
but I think that’s a reasonable restriction. I could probably tweak the regex to allow for more options in the directory names, like dashes.
Now when I deploy a new app as vassal, I still create the .ini/.socket
pair but then it just works without much fuss. I am hoping to create a script (shell or python) to help me deploy new apps even more easily.
Adding additional apps
Adding a new app involves:
- Cloning the python dash app code itself to
/srv/
. - Making sure the ownership is right for the directory and everything inside with
www-data
. - Creating a new
.ini/.socket
file pair in/etc/uwsgi/vassals
(Remember, the.socket
file is just a text file with one line of something like/srv/new_app/uwsgi.sock
, basically whateversocket=
lists in the.ini
file, and this is only for “on demand” apps.) - Creating a new
nginx
block to make a proxy url to theuwsgi.sock
. UPDATE See above, I don’t have to do this anymore.
It is important to note that the url_base_pathname
of the actual dash application must be consistent with the nginx path.
For example, this is how the dataframe table example dash app is instantiated:
app = dash.Dash("example of dataframe",
external_stylesheets=[dbc.themes.YETI],
title="Example Data Frame",
meta_tags=[
{
"name": "viewport",
"content": "width=device-width, initial-scale=1"
},
],
url_base_pathname='/dash/table_example/')
January 2023 Update
Since I want the url and the folder name to always match now, I’m doing this, now with Pathlib
from pathlib import Path
parent_dir = Path().absolute().stem
such that I can set url_base_pathname=f'/dash/{parent_dir}/
Advanced INI files with magic variables
If you can get a consistent convention of naming, you can save yourself time by using magic variables in the .ini
files. For example, %n
is the file name of the .ini
file minus the extension. So if you keep your directory structure and .ini
file names consistent, than a template like the one below can be reused with minimal changes. I have only started to do this a little. It makes me think that I should always have my app in the same file name app.py
then I would not even have to change the module line.
[uwsgi]
#customize this part
module = app:server
processes = 1
threads = 1
#leave these on if you want the app to shut itself down when idle.
idle = 60
die-on-idle = true
cheap = true
# below here should be the same every time using the file name to set the path and the logger name
reload-mercy=5
worker-reload-mercy=5
socket = /srv/%n/uwsgi.sock
chmod-socket=664
chdir = /srv/%n/
binary-path = /usr/local/bin/uwsgi
uid = www-data
gid = www-data
master = true
logger = file:/var/log/uwsgi/%n.log
Conclusion / Common pitfalls
Permissions can be an issue, I have set everything up such that the apps run as the user www-data, and the directories must all have www-data
as their owner and group. I added my regular account to the www-data group so I can pull the repos normally to update the account.
I’ve written some simple aliases / bash scripts to pull and relaunch the app when I make changes. Something like:
#!/usr/bin/bash
cd /srv/$1
git pull
sudo touch /etc/uwsgi/vassals/$1.ini
sleep 2
tail /var/log/uwsgi/$1.log
Will pull the repo, touch the ini
file so the emperor restarts the app, and then waits and tails the log so I can see if everything worked ok. The $1
lets me pass in an app directory name on the command line, instead of needing a different script for every app.
I can then run this command over ssh to update the app deployment.