DEV Community

Cover image for How to add flask, gunicorn, packages to a distroless docker container
Lionel Marco
Lionel Marco

Posted on

How to add flask, gunicorn, packages to a distroless docker container

The flask, gunicorn, nginx, stack is a powerfull toolchain.There are many tutorial about how to dockerizing it, but it is different. We will do it from a distroless way, with a security concern point of view.

Table of Contents

1) Distroless Concept

What are distroless containers ?

Think in slim down linux distribution, where are removed all unused things.
Don't have shell or package manager, come without: apt, apk, pip, bash, etc..
When remove all unnecessary things get a bonus of size saving, halving or even more the final size.So is more faster to upload it. From a security concern we obtain a small surface attack area.

To get more info about it: Google Container Tools

The concept is clear: Just only add the application runtime and the accesories libraries.

2) How to work with theys

The gcr.io/distroless/python3 is a batteries not included toy.

The classic recipe resolve all in one saucepan:

FROM python:3.6
ADD . /app
WORKDIR /app
RUN pip install flask gunicorn
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app"]
Enter fullscreen mode Exit fullscreen mode

Now the build process change, is more like a crispy home fries recipe, first boil the potatoes in water, then drain and cool, add spices and finally send to oven.

First we will take a base system python:3.6-slim and call build-env, over this it will be added the needed packages. Second move the packages and our application to the distroless image gcr.io/distroless/python3.

To do it, here is where multistage build come to help us: Docker: Use multi-stage builds

2.2) Package installing

The packages to install:

#file:requirements.txt
flask==1.1.4
gunicorn>19
Enter fullscreen mode Exit fullscreen mode

To build our images, two stages are needed:

# file: flask_gunicorn_distroless.dockerfile

FROM python:3.6-slim  AS build-env
WORKDIR /app

# First install packages to avoid re-run pip install 
COPY requirements.txt ./
RUN pip install --disable-pip-version-check -r requirements.txt --target /packages

COPY ./versions.py /app
COPY ./app.py /app
COPY ./standalone.py /app

FROM gcr.io/distroless/python3
WORKDIR /app
COPY --from=build-env /app /app
COPY --from=build-env /packages /packages
#instead of COPY --from=build-env /packages /usr/local/lib/python/site-packages

ENV PYTHONPATH=/packages
#instead of ENV PYTHONPATH=/usr/local/lib/python/site-packages

CMD ["standalone.py"]
Enter fullscreen mode Exit fullscreen mode

Some Tips:

Inside distroless the nexts command does not works:

RUN mkdir /app
RUN cp ./source /app

Instead must to use:

WORKDIR /app it performs mkdir and cd implicitly.
COPY ./app /app will create target directory

2.2) See what is inside

The image gcr.io/distroless/python3 come with some preinstalled packages, just to see what is inside of it, a small python file is writed.

# file version.py
import sys
import os
import argparse
from pathlib import Path

print("Versions:------------------")
parser = argparse.ArgumentParser()
parser.add_argument('folder', type=str, help='The folder to list.', nargs='?', default=os.getcwd())

#--------------------------
import flask
print("Flask Version:",flask.__version__)

import gunicorn 
print("Gunicorn Version:",gunicorn.__version__)

#--------------------------
print("Getcwd: ",os.getcwd()) 
print("Sys.path count:",str(len(sys.path)))
print('\n'.join(sys.path))


def listFolders(str_path: str):
    dir_path=Path(str_path).expanduser()
    print(" Packages:------------------")
    try:
       for path in dir_path.iterdir():            
         if path.is_dir(): 
            print(path)
    except PermissionError:
       pass

def main(args):
    print('Folder:'+args.folder)
    listFolders(args.folder)


if __name__ == "__main__":
    main(parser.parse_args())
    #listFolders('/packages')
Enter fullscreen mode Exit fullscreen mode

In the dockerfile change the last line to:

CMD ["versions.py", "/usr/lib/python3.9"]

Rebuild the image:

docker build --tag flask_gunicorn_distroless --file flask_gunicorn_distroless.dockerfile .

And execute:

docker run flask_gunicorn_distroless

In the console will be listed the paths and the pre installed packages.

:docker run flask_gunicorn_distroless
Versions:------------------
Flask Version: 1.1.4
Gunicorn Version: 20.1.0
Getcwd:  /app
Sys.path count: 5
/app
/packages
/usr/lib/python39.zip
/usr/lib/python3.9
/usr/lib/python3.9/lib-dynload
Folder:/usr/lib/python3.9
Folders:------------------
/usr/lib/python3.9/multiprocessing
/usr/lib/python3.9/zoneinfo
/usr/lib/python3.9/sqlite3
/usr/lib/python3.9/distutils
/usr/lib/python3.9/venv
/usr/lib/python3.9/http
/usr/lib/python3.9/xml
/usr/lib/python3.9/asyncio
/usr/lib/python3.9/collections
/usr/lib/python3.9/wsgiref
/usr/lib/python3.9/encodings
/usr/lib/python3.9/pydoc_data
/usr/lib/python3.9/ctypes
/usr/lib/python3.9/xmlrpc
/usr/lib/python3.9/test
/usr/lib/python3.9/email
/usr/lib/python3.9/importlib
/usr/lib/python3.9/curses
/usr/lib/python3.9/html
/usr/lib/python3.9/json
/usr/lib/python3.9/logging
/usr/lib/python3.9/dbm
/usr/lib/python3.9/lib-dynload
/usr/lib/python3.9/urllib
/usr/lib/python3.9/concurrent
/usr/lib/python3.9/unittest
/usr/lib/python3.9/__pycache__
Enter fullscreen mode Exit fullscreen mode

2.3) Running Flask

Just for sanity check, see if our flask application can run:

#file:app.py
from time import gmtime, strftime   
from flask import Flask

application = Flask(__name__)

@application.route("/")
def index():    
    return {'status':'Available','time':strftime("%H:%M:%S", gmtime())  }    

if __name__ == "__main__":    
    application.run(host="0.0.0.0", port=5000,debug=True)

Enter fullscreen mode Exit fullscreen mode

In the dockerfile change the last line to:

CMD ["app.py"]

Rebuild the image:

docker build --tag flask_gunicorn_distroless --file flask_gunicorn_distroless.dockerfile .

And execute:

docker run -p 80:5000 flask_gunicorn_distroless

In your preferred web browser open the adress: http://localhost, if all is ok, a json will be showed.

Json Output

2.4) Running Gunicorn

In the classic recipe, the usual way to run Gunicorn is:

CMD ["gunicorn", "-b", "0.0.0.0:8000", "app"]
Enter fullscreen mode Exit fullscreen mode

But now the ENTRYPOINT is /usr/bin/python3.9

So need to run gunicorn from inside python:

Gunicorn Custom Application

The idea is subclass gunicorn.app.base.BaseApplication and overload load_config and load.

# file:standalone.py
from app import application
import multiprocessing
import gunicorn.app.base

def number_of_workers():
    return  (multiprocessing.cpu_count() * 2) + 1

class StandaloneApplication(gunicorn.app.base.BaseApplication):

    def __init__(self, app, options=None):
        self.options = options or {}
        self.application = app
        super().__init__()

    def load_config(self):
        config = {key: value for key, value in self.options.items()
                  if key in self.cfg.settings and value is not None}
        for key, value in config.items():
            self.cfg.set(key.lower(), value)

    def load(self):
        return self.application

if __name__ == '__main__':
    options = {
        'bind': '%s:%s' % ('0.0.0.0', '8000'),
        'workers': number_of_workers(),
    }
    StandaloneApplication(application, options).run()

Enter fullscreen mode Exit fullscreen mode

In the dockerfile change the last line to:

CMD ["standalone.py"]

Rebuild the image:

docker build --tag flask_gunicorn_distroless --file flask_gunicorn_distroless.dockerfile .

And execute:

docker run -p 80:8000 flask_gunicorn_distroless

In your preferred web browser open the adress: http://localhost, if all is ok, a json will be showed.

Json Output

3) Vulnerability Scanning

We will use Clair to perform a static analysis of vulnerabilities.

1) Run the vulnerabilities database:

docker run -d --name db arminc/clair-db:latest

2) Run the service:

docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan

3) Run the client:

Download from:Clair Scanner

Retrieve the image id:

docker image ls flask_gunicorn_distroless
REPOSITORY                  TAG       IMAGE ID       CREATED         SIZE
flask_gunicorn_distroless   latest    fa824d3ab0a6   3 minutes ago   70.8MB

Enter fullscreen mode Exit fullscreen mode

Scan, passing the loopback our own ip, and the image id:

./clair-scanner_linux_386 --ip=127.0.0.1 fa824d3ab0a6 | grep Unapproved | wc -l

Get 0 vulnerabilities

flask_gunicorn_distroless

Compare with the python:3.9-slim

Retrieve the image id:

docker image ls python:3.9-slim
REPOSITORY   TAG        IMAGE ID       CREATED       SIZE
python       3.9-slim   5e714d33137a   5 days ago    122MB

Enter fullscreen mode Exit fullscreen mode

Scan, passing the loopback our own ip, and the image id:

./clair-scanner_linux_386 --ip=127.0.0.1 5e714d33137a | grep Unapproved | wc -l

Get 39 vulnerabilities:

python:3.9-slim

Top comments (0)