<h1>How to deploy dash/flask applications on kubernetes</h1>
<p>I have developed a dash application consisting of three main components:</p>
<ol>
<li>A python backend script</li>
<li>A flask webserver</li>
<li>A dash frontend</li>
</ol>
<p>The backend fetches data from a web API and POSTs it to various webserver endpoints. The frontend GETs data from various endpoints and presents the data in a dashboard.</p>
<p>All components are deployed in kubernetes. The frontend and the flask webserver are deployed as services. Both are based on the Flask's runtime development webserver, which I am noticing is not capable of handle a lot of traffic. I would like to have both of these deployed with nginx and gunicorn, but I am not sure how achieve this for my kubernetes setup.</p>
<p>Below are examples of source code (not a complete and executable example, but just to show how the application is designed) and examples of kubernetes manifests for <code>Deployment</code> and <code>Service</code>.</p>
<h2>Source code</h2>
<h3>Flask webserver</h3>
<pre class="lang-py prettyprint-override"><code>from flask import Flask, request, jsonify

current_data = {&quot;message&quot;: &quot;No timeseries data yet&quot;}
current_vessel_data = {&quot;message&quot;: &quot;No vessel timeseries data yet&quot;}
current_timing = {&quot;message&quot;: &quot;No timing data yet&quot;}
current_gangway_status = {&quot;message&quot;: &quot;No gangway status yet&quot;}

HOST = &quot;0.0.0.0&quot;
PORT = 9898

app = Flask(__name__)

@app.route(&quot;/post_timeseries_data&quot;, methods=[&quot;POST&quot;])
def receive_data() -&gt; str:
    # log(&quot;This is the backend posting timeseries data.&quot;, prefix=logger_prefix)
    global current_data
    data = request.data
    current_data = data
    return jsonify({&quot;message&quot;: &quot;Data received successfully&quot;})

@app.route(&quot;/get_timeseries_data&quot;, methods=[&quot;GET&quot;])
def get_timeseries_data() -&gt; str:
    # log(&quot;This is a frontend instance requesting timeseries data.&quot;, prefix=logger_prefix)
    global current_data
    return current_data

@app.route(&quot;/post_timing_data&quot;, methods=[&quot;POST&quot;])
def receive_timing_data() -&gt; str:
    # log(&quot;This is the backend posting timing data.&quot;, prefix=logger_prefix)
    global current_timing
    timing = request.data
    current_timing = timing
    return jsonify({&quot;message&quot;: &quot;Timing received successfully&quot;})

@app.route(&quot;/get_timing_data&quot;, methods=[&quot;GET&quot;])
def get_timing_data() -&gt; str:
    # log(&quot;This is a frontend instance requesting timing data.&quot;, prefix=logger_prefix)
    global current_timing
    return current_timing

@app.route(&quot;/post_gangway_status&quot;, methods=[&quot;POST&quot;])
def receive_gangway_status() -&gt; str:
    # log(&quot;This is the backend posting gangway status.&quot;, prefix=logger_prefix)
    global current_gangway_status
    gangway_status = request.data
    current_gangway_status = gangway_status
    return jsonify({&quot;message&quot;: &quot;Gangway status received successfully.&quot;})

@app.route(&quot;/get_gangway_status&quot;, methods=[&quot;GET&quot;])
def get_gangway_status() -&gt; str:
    # log(&quot;This is a frontend instance requesting gangway status.&quot;, prefix=logger_prefix)
    global current_gangway_status
    return current_gangway_status

@app.route(&quot;/post_timeseries_data_vessel&quot;, methods=[&quot;POST&quot;])
def post_timeseries_data_vessel() -&gt; str:
    global current_vessel_data
    vessel_data = request.data
    current_vessel_data = vessel_data
    return jsonify({&quot;message&quot;: &quot;Vessel timeseries data received successfully.&quot;})

@app.route(&quot;/get_timeseries_data_vessel&quot;, methods=[&quot;GET&quot;])
def get_timeseries_data_vessel() -&gt; str:
    global current_vessel_data
    return current_vessel_data

@app.route(&quot;/&quot;)
def root_content() -&gt; str:
    &quot;&quot;&quot;For the readiness probe.&quot;&quot;&quot;
    return &quot;This is the root page of the W2W file server.&quot;


if __name__ == &quot;__main__&quot;:
    app.run(host=HOST, port=PORT)
</code></pre>
<h3>Python script backend</h3>
<p>I don't want to give the entire backend code, but essentially it is a while loop continuously running fetching data from a web API and posting timeseries data and timings to the POST endpoints defined in the webserver above. The skeleton is something like this:</p>
<pre class="lang-py prettyprint-override"><code>import requests
import pandas as pd
import time

host = &quot;&quot;
post = 9898
post_path = &quot;&quot;

while True:
    now = pd.Timestamp.now(tz=&quot;CET&quot;).floor(&quot;1s&quot;)
    start= now - pd.Timedelta(minutes=30)
    
    t0 = time.perf_counter()
    df = get_data(start=start, end=now)
    timing = time.perf_counter() - t0

    data = {&quot;df&quot;: df.to_json(date_format=&quot;iso&quot;, &quot;timing&quot;: timing)

    requests.post(post_path, json=json.dumps(data))
</code></pre>
<h3>Dash frontend</h3>
<p>The dash app is a fairly standard multipage app. The main file  <code>app.py</code>looks like this, but with the callback details removed for clarity. The <code>main()</code> function returns the Dash instance.</p>
<pre class="lang-py prettyprint-override"><code>import dash
import ids
import requests
import json
import utils
import time

import pandas as pd
import dash_bootstrap_components as dbc

from logger import log
from dash import Dash, html, dcc, callback, ctx
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

from typing import Tuple, Union

logger_prefix = &quot;W2W Frontend&quot;
APP_TITLE = &quot;W2W Vessel Insight Dashbord&quot;
INTERVAL_REFRESH = 5 * 1000

###################################
######  P A R A M E T E R S  ######
###################################
DEBUG = False
APP_PORT = 9898 if not DEBUG else 8080
DATA_PORT = 9898
DATA_HOST = &quot;walk-to-work-fileserver-service.walk-to-work.svc.cluster.local&quot; if not DEBUG else &quot;127.0.0.1&quot;
APP_HOST = &quot;0.0.0.0&quot; if not DEBUG else &quot;127.0.0.1&quot;

path_timeseries_data = f&quot;http://{DATA_HOST}:{DATA_PORT}/get_timeseries_data&quot;
path_timeseries_data_vessel = f&quot;http://{DATA_HOST}:{DATA_PORT}/get_timeseries_data_vessel&quot;
path_timing_data = f&quot;http://{DATA_HOST}:{DATA_PORT}/get_timing_data&quot;
path_gangway_status = f&quot;http://{DATA_HOST}:{DATA_PORT}/get_gangway_status&quot;
###################################

def main() -&gt; Dash:
    &quot;&quot;&quot;Main function for creating the application.
    
    Returns:
        Dash: Dash application instance
    &quot;&quot;&quot;
    app = Dash(__name__, external_stylesheets=[dbc.themes.SLATE], use_pages=True)
    app.title = APP_TITLE

    @callback(
    [
        Output(ids.STORE_CURRENT_DATA_VESSEL, &quot;data&quot;),
        Output(ids.STORE_PREVIOUS_DATA_VESSEL, &quot;data&quot;)
    ],
    [
        Input(ids.INTERVAL_TIMER, &quot;n_intervals&quot;),
        Input(ids.STORE_DROPDOWN_DATA_LENGTH_VALUE, &quot;data&quot;)
    ],
    State(ids.STORE_CURRENT_DATA_VESSEL, &quot;data&quot;)
    )
    def get_data(n: int, data_length: str, previous_data: str) -&gt; Tuple[str]:
        &lt;code here&gt;
        
    app.layout = html.Div(children=[
        # Page container necessary for multipage
        dash.page_container,

        # Automatic refresh interval
        dcc.Interval(id=ids.INTERVAL_TIMER, interval=INTERVAL_REFRESH, n_intervals=0),
        
        # Store data fetched from Vessel Insight API
        dcc.Store(id=ids.STORE_CURRENT_DATA),
        dcc.Store(id=ids.STORE_PREVIOUS_DATA),
        dcc.Store(id=ids.STORE_BRIDGE_STATUS),
        dcc.Store(id=ids.STORE_TIMING_DATA),
        dcc.Store(id=ids.STORE_DROPDOWN_DATA_LENGTH_VALUE),  # Used to pass dropdown selection to this callback.
        dcc.Store(id=ids.STORE_CALLBACK_START_OF_TIMING),
        dcc.Store(id=ids.STORE_CURRENT_DATA_VESSEL),
        dcc.Store(id=ids.STORE_PREVIOUS_DATA_VESSEL)
    ])

    return app


if __name__ == &quot;__main__&quot;:
    app = main()
    app.run(debug=True, port=APP_PORT, host=APP_HOST, dev_tools_hot_reload=False)

</code></pre>
<h2>Kubernetes manifests</h2>
<p>The frontend and webserver are deployed as services, while the backend is not. The deployment and service manifests are more or less identical for all three components, so I just give one example of each:</p>
<h3><code>deployment.yaml</code></h3>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${myappname_component}-fileserver
  namespace: ${namespace}
  labels:
    app.kubernetes.io/name: ${myappname_component}-fileserver
    app.kubernetes.io/created-by: ${application}
    app.kubernetes.io/component: ${myappname_component}-fileserver
spec:
  selector:
    matchLabels:
      app: ${myappname_component}-fileserver
  template:
    metadata:
      labels:
        app: ${myappname_component}-fileserver
        app.kubernetes.io/name: ${myappname_component}-fileserver
        app.kubernetes.io/created-by: ${application}
        app.kubernetes.io/component: ${myappname_component}-fileserver
    spec:
      serviceAccountName: ${keyvault_mi_service_account_name}
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
        runAsNonRoot: true
        seccompProfile:
          type: &quot;RuntimeDefault&quot;
      containers:
        - name: ${fileserver_image_name}
          image: ${container_registry}/${application_image_repository}/${fileserver_image_name}:${fileserver_image_version}
          imagePullPolicy: Always
          ports:
            - name: http-app
              containerPort: ${app_port}
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: ${app_port}
            initialDelaySeconds: 10
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 30
          readinessProbe:
            httpGet:
              path: /
              port: ${app_port}
            initialDelaySeconds: 10
            periodSeconds: 30
            successThreshold: 1
            timeoutSeconds: 30
          resources:
            limits:
              cpu: 400m
              memory: 400Mi
            requests:
              cpu: 200m
              memory: 200Mi
          securityContext:
            runAsUser: 1000
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities:
              drop: [&quot;ALL&quot;]

</code></pre>
<h3><code>service.yaml</code></h3>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: v1
kind: Service
metadata:
  name: ${myappname_component}-fileserver-service
  namespace: ${namespace}
  labels:
    app.kubernetes.io/created-by: ${application}
    app.kubernetes.io/name: ${myappname_component}
    app.kubernetes.io/component: ${myappname_component}
spec:
  selector:
    app.kubernetes.io/component: ${myappname_component}-fileserver
  ports:
    - name: ${myappname_component}-fileserver-service
      port: ${app_port}
      targetPort: ${app_port}

</code></pre>

Yoda
<p>You probably ran your <a href="https://flask.palletsprojects.com/en/2.3.x/tutorial/factory/" rel="nofollow noreferrer">Flask app</a> directly with the Flask built-in server:</p>
<pre class="lang-py prettyprint-override"><code>if __name__ == &quot;__main__&quot;:
    app.run(host='0.0.0.0')
</code></pre>
<p>You should prepare your Flask app to be <a href="https://stackoverflow.com/q/34478320/6309">compatible with Gunicorn</a>.</p>
<ul>
<li>Make sure your Flask app's main instance is named <code>app</code>.</li>
<li>Test the Flask application locally with Gunicorn:
<pre class="lang-bash prettyprint-override"><code>gunicorn app:app --bind 0.0.0.0:8000 --workers 4
</code></pre>
</li>
</ul>
<p>Then you can start changing Docker.<br />
Your Dockerfile might have been as follows, using Flask's built-in server:</p>
<pre><code>FROM python:3.7
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD [&quot;python&quot;, &quot;app.py&quot;]
</code></pre>
<p>You would need to use <a href="https://gunicorn.org/" rel="nofollow noreferrer">Gunicorn</a> to serve your Flask application in the Docker container.</p>
<p>For that, modify your <code>Dockerfile</code> to include Gunicorn:</p>
<pre><code>FROM python:3.7
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
CMD [&quot;gunicorn&quot;, &quot;app:app&quot;, &quot;--bind&quot;, &quot;0.0.0.0:8000&quot;, &quot;--workers&quot;, &quot;4&quot;]
</code></pre>
<p>And build, then test the Docker image locally:</p>
<pre class="lang-bash prettyprint-override"><code>docker build -t your-image-name .
docker run -p 8000:8000 your-image-name
</code></pre>
<p>Finally, change your basic Kubernetes deployment that is currently exposes the Flask app.</p>
<p>You can introduce Nginx as a reverse proxy alongside your Flask app in the same pod. That would also involve a <code>ConfigMap</code> for Nginx configuration.<br />
See for example &quot;<strong><a href="https://www.kisphp.com/kubernetes/deploy-a-flask-application-with-nginx-on-kubernetes" rel="nofollow noreferrer">Deploy a flask application with nginx on Kubernetes</a></strong>&quot;</p>
<p>Create a <code>ConfigMap</code> for Nginx: <code>nginx-configmap.yaml</code></p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
data:
  nginx.conf: |
    user nginx;
    ...
    # Rest of the Nginx configuration
</code></pre>
<p>Apply it:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl apply -f nginx-configmap.yaml
</code></pre>
<p>Then, modify your <code>deployment.yaml</code> to include both the Flask (Gunicorn) and Nginx containers.<br />
For instance, you would go from your current code:</p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    ...
    spec:
      containers:
      - name: flask-app
        image: your-flask-image
        ports:
        - containerPort: 5000
</code></pre>
<p>To:</p>
<pre class="lang-yaml prettyprint-override"><code>apiVersion: apps/v1
kind: Deployment
...
spec:
  ...
  template:
    ...
    spec:
      volumes:
      - name: nginx-config-volume
        configMap:
          name: nginx-config
      containers:
      - name: flask-app
        image: your-flask-image
        ports:
        - containerPort: 8000

      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
        volumeMounts:
        - name: nginx-config-volume
          mountPath: /etc/nginx/nginx.conf
          subPath: nginx.conf
</code></pre>
<p>And apply the updated deployment:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl apply -f deployment.yaml
</code></pre>
<p>Finally, you can make sure everything is running with:</p>
<pre class="lang-bash prettyprint-override"><code>kubectl get pods
kubectl logs &lt;pod_name&gt; -c &lt;container_name&gt;
</code></pre>
<p>Access your application using the appropriate Kubernetes service endpoint or ingress to validate it works as expected.</p>
