Dash/Plotly in production with nginx gunicorn

Plotly 5.19.0 - Dash 2.16.0 - python 3.10.11 - debian 12

I am having a nightmare getting to run this in production. I want the app to respond to the mematest.mydomain.it address and nginx should route it to the localhost:4200 port where the app is listening.

The current config you see below is more or less running the app under the werkzeug dev server but my attempts at running under gunicorn have all failed. Can you spot what I’m doing wrong? Thanks a lot in advance.

nginx with the following configuration:

server {
    server_name mematest.mydomain.it;
    client_max_body_size 25m;

    access_log /var/log/nginx/memaapp.access.log;
    error_log /var/log/nginx/memaapp.error.log;

    location / {
        proxy_pass http://127.0.0.1:4200;  # Ensure this points to your Dash app
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Prefix /;

        #proxy_http_version 1.1;
        #proxy_set_header Upgrade $http_upgrade;
        #proxy_set_header Connection "upgrade";
        #proxy_set_header Host $host;
        #proxy_set_header X-Real-IP $remote_addr;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/mematest.mydomain.it/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mematest.mydomain.it/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

server {
    listen 80;
    server_name mematest.mydomain.it;
    return 301 https://$host$request_uri; # Direct HTTPS redirection without using 'if'
}

server {
    listen 80 default_server;
    server_name _;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    location / {
        proxy_pass http://127.0.0.1:4100;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

while the Dockerfile is as follows:

# ---- Base Layer ----
FROM python:3.10.13-slim-bullseye as base

# Environment settings and maintainer label
LABEL maintainer="bob@mydomain.com"
ENV TZ=Europe/Rome \
    POETRY_VERSION=1.8.0 \
    POETRY_HOME="/usr/local/bin"

# Set timezone and package preferences
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone && \
    echo 'APT::Install-Suggests "0";' > /etc/apt/apt.conf.d/00-docker && \
    echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/00-docker

# Install necessary Debian packages
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
    apt-get install -y nvi net-tools tzdata curl git wget tar iputils-ping gunicorn && \
    rm -rf /var/lib/apt/lists/* 

RUN pip install poetry==1.8.0
ENV POETRY_NO_INTERACTION=1 \
    POETRY_VIRTUALENVS_IN_PROJECT=1 \
    POETRY_VIRTUALENVS_CREATE=1 \
    POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app
COPY pyproject.toml poetry.lock README.md ./
RUN poetry install --no-root --no-interaction --no-ansi --no-dev -vvv


# ---- Application Layer ----
FROM base as application

COPY config    /app/config
COPY resources /app/resources
COPY src       /app/src

RUN poetry install --only-root --no-interaction --no-ansi --no-dev -vvv

EXPOSE 4200
WORKDIR /app

#CMD ["poetry", "run", "gunicorn", "-w", "4", "-b", "0.0.0.0:4200", "src.app.mema_app:server"]
CMD poetry run python src/app/mema_app.py

which does launch the app with Flask’s default werkzeug server.
The code itself is as follows:

server = Flask(__name__)
server.secret_key = secrets.token_urlsafe(16)
server.wsgi_app = ProxyFix(server.wsgi_app, x_for=1, x_host=1)
app = dash.Dash(__name__,
                server=server,
                external_stylesheets=theme["stylesheets"],
                url_base_pathname='/'
) 
....
if __name__ == '__main__':
    app.run(debug=False, host="0.0.0.0", port=4200)

Hello @rjalexander,

Have you tried disabling your other 80 listener?

Are you sure that the app is running? Are you getting a 502 error from nginx?

1 Like

@jinnyzor thanks for chiming in! Good points.

The app is running:

mema@mema3:~/docker_configs$ docker compose ps mema-app
NAME                        IMAGE                COMMAND                  SERVICE    CREATED        STATUS        PORTS
docker_configs-mema-app-1   mema-app:dev-0.5.2   "/bin/sh -c 'poetry …"   mema-app   10 hours ago   Up 10 hours   0.0.0.0:4200->4200/tcp, :::4200->4200/tcp

and this is a log from it obtained with “docker compose logs -f mema-app”:

mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:44] "GET / HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:44] "GET /_dash-layout HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:45] "GET /_dash-dependencies HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:45] "POST /_dash-update-component HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:55] "POST /_dash-update-component HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:55] "GET /search HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:55] "GET /assets/icon.avif?m=1709655348.7907581 HTTP/1.0" 304 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:56] "GET /_dash-layout HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:56] "GET /_dash-dependencies HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:56] "POST /_dash-dist HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:56] "GET /_dash-component-suites/dash/dcc/async-slider.js HTTP/1.0" 304 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:02:56] "GET /_dash-component-suites/dash/dcc/async-dropdown.js HTTP/1.0" 304 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:03:33] "POST /_dash-update-component HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:03:38] "POST /_dash-update-component HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:04:05] "POST /_dash-update-component HTTP/1.0" 200 -
mema-app-1  | 195.250.226.1 - - [08/Mar/2024 08:04:06] "POST /_dash-update-component HTTP/1.0" 200 -

which shows a normal functioning.

But when I try running the same codebase and same Dockerfile with only the last line of the Dockerfile changed to run it under gunicorn (commenting the flask line) as follows:

CMD ["poetry", "run", "gunicorn", "-w", "4", "-b", "0.0.0.0:4200", "src.app.mema_app:server"]
#CMD poetry run python src/app/mema_app.py

and rebuilding/restarting the container accordingly, I get the following:

mema@mema3:~/docker_configs$ docker compose logs -f mema-app
mema-app-1  | [2024-03-08 08:09:37 +0100] [1] [INFO] Starting gunicorn 21.2.0
mema-app-1  | [2024-03-08 08:09:37 +0100] [1] [INFO] Listening at: http://0.0.0.0:4200 (1)
mema-app-1  | [2024-03-08 08:09:37 +0100] [1] [INFO] Using worker: sync
mema-app-1  | [2024-03-08 08:09:37 +0100] [9] [INFO] Booting worker with pid: 9
mema-app-1  | [2024-03-08 08:09:37 +0100] [10] [INFO] Booting worker with pid: 10
mema-app-1  | [2024-03-08 08:09:37 +0100] [11] [INFO] Booting worker with pid: 11
mema-app-1  | [2024-03-08 08:09:37 +0100] [12] [INFO] Booting worker with pid: 12
mema-app-1  | [2024-03-08 08:10:19,004] ERROR in app: Exception on /_dash-component-suites/dash_bootstrap_components/_components/dash_bootstrap_components.v1_5_0m1709881713.min.js [GET]
mema-app-1  | Traceback (most recent call last):
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/flask/app.py", line 1463, in wsgi_app
mema-app-1  |     response = self.full_dispatch_request()
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/flask/app.py", line 872, in full_dispatch_request
mema-app-1  |     rv = self.handle_user_exception(e)
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/flask/app.py", line 870, in full_dispatch_request
mema-app-1  |     rv = self.dispatch_request()
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/flask/app.py", line 855, in dispatch_request
mema-app-1  |     return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/dash/dash.py", line 1005, in serve_component_suites
mema-app-1  |     _validate.validate_js_path(self.registered_paths, package_name, path_in_pkg)
mema-app-1  |   File "/app/.venv/lib/python3.10/site-packages/dash/_validate.py", line 365, in validate_js_path
mema-app-1  |     raise exceptions.DependencyException(
mema-app-1  | dash.exceptions.DependencyException: Error loading dependency. "dash_bootstrap_components" is not a registered library.
mema-app-1  | Registered libraries are:
mema-app-1  | ['dash', 'plotly']

any insights? Thank you very much

Doesn’t this look like the problems which popped up with dash 2.16.0?

dash.exceptions.DependencyException: Error loading dependency. "dash_bootstrap_components" is not a registered library.
mema-app-1  | Registered libraries are:
mema-app-1  | ['dash', 'plotly']

Try updating your dash to 2.16.1.

But I’m just guessing :see_no_evil:

2 Likes

@AIMPED thanks a lot. Have upgraded to 2.16.1 and also ironed out a duplicate gunicorn in the container (one was installed by Poetry in the .venv and another by apt install on the OS).

Also added a --timeout 120 to gunicorn since the underlying app is very slow.

So far things seem to workish … thanks a lot.

2 Likes