These are notes from creating and running a Docker container for a simple Flask app. I did the following in more or less chronological order.

I am assuming you understand the basics of docker and know how to build and run docker images.

Created four images that are layered on each other, as follows. Tested and debugged each image as it was built by using docker run --interactive --tty in order to have a terminal logged into the container running the image. In the terminal I could confirm that software packages had been installed as intended as well as any other setup. I did this for each image except for the last one, which runs the Flask app automatically when a container for the image is run.

0) Ubuntu. Use Ubuntu 20.04, aka focal, as the base for the first image created here.

1) Python 3 and Pip 3. Install python 3.x and pip 3.x.

# Use this dockerfile to build image python3-pip3

FROM ubuntu:focal

RUN apt-get update
RUN apt-get --yes install python3
RUN apt-get --yes install python3-pip

2) Flask. Install Flask 1.1.2, et al, using pip with requirements.txt providing the list of python packages to install.

# Use this dockerfile to build image flask112

FROM python3-pip3

WORKDIR /espresso
COPY requirements.txt .
RUN pip3 install -r requirements.txt

3) Espresso. Espresso was the name of our app. In this image all of its files are copied into the container, environment variables are set, and port 5000 within the container is exposed for Flask to listen to.

For development purposes it was simpler to do all the setup for the app and not have the app run here. That way I was able to easily confirm all the files were in place, the environment variables set up, and the desired port exposed.

# Use this dockerfile to build image espresso

FROM flask112

WORKDIR /espresso
COPY . /espresso

ENV FLASK_APP=espresso.py
ENV FLASK_DEBUG=true
ENV FLASK_ENV=development

EXPOSE 5000

4) Run Espresso. Run the app. At this point all of the setup has been done by the underlying images previously built. This image creates user earl and runs the container as that user. Otherwise, the container by default runs as root, which is considered a security risk. The dockerfile entrypoint command invokes flask upon creation of the container.

Note that the host for the url request is specified as 0.0.0.0 which means any host for Flask to respond to as long as the request uses port 5000. Otherwise, the host value defaults to 127.0.0.1 which is localhost, i.e. the container itself. This does not work because requests originate from outside the container.

Specifying any url host is not a security risk since the container is enclosed by a real (host) system and can only be accessed through requests forwarded by it.

Port 5000 is the default for Flask but this invocation of flask makes the port number explicit.

# Use this dockerfile to build image espresso-run

FROM espresso

WORKDIR /espresso
RUN useradd -u 1000 earl
USER earl

ENTRYPOINT flask run --host 0.0.0.0 --port 5000


The rest of this post is about the options provided to docker run for instantiating a container that runs the espresso app in the last image described above.

--mount

Mount the .env file on the host system (source) that contains application secrets to a corresponding .env file inside the container (target). readonly is a good security practice.

--mount type=bind,source=/home/earl/PycharmProjects/espresso/.env,target=/espresso/.env,readonly

-p

Map port 5055 from the host system to port 5000 of the container. I chose port 5055 arbitrarily. Port 5000 matches the port that was exposed in one of the images above as well as the port number specified for the invocation of Flask in the final image.

-p 5055:5000


The remaining options are for mitigating security risks, so not strictly necessary to run the image.

--cap-drop

Disable use of the setgid() and setuid() functions inside the container. This prevents any malicious code from running as a different user or a different group.

--cap-drop SETGID --cap-drop SETUID

--pids-limit

Set the maximum number processes that can run in the container. Prevents malicious code from forking off an unlimited number of processes, aka “fork bomb”. I am just guessing that 256 is a large enough value.

--pids-limit 256

--memory

On my Ubuntu laptop, this limits real memory but not swap memory so it’s of limited effect. However, on a production server the situation may be different, and this constraint is hopefully truly effective in that swap memory is also limited.

--memory 1G

--cpus

Limit the number of CPUs used to 1. You can increase the number if your app needs more.

--cpus 1

--read-only

Set the container’s root filesystem to read-only.

--read-only


Finally, putting all of these options together into a complete command including the image name we have:

docker run --mount type=bind,source=/home/earl/PycharmProjects/espresso/.env,target=/espresso/.env,readonly -p 5055:5000 --cap-drop SETGID --cap-drop SETUID --pids-limit 256 --memory 1G --cpus 1 --read-only espresso-run


Sources:

Don’t Embed Configuration or Secrets in Docker Images
https://medium.com/@mccode/dont-embed-configuration-or-secrets-in-docker-images-7b2e0f916fdd

Docker Container Security 101: Risks and 33 Best Practices
https://www.stackrox.com/post/2019/09/docker-security-101/

Docker Hardening Guide
https://docs.paloaltonetworks.com/cortex/cortex-xsoar/5-5/cortex-xsoar-admin/docker/docker-hardening-guide.html