DEV Community

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

Posted on

1

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

Image of Docusign

Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs