Building a Python package, and a docker image via Pipenv

Yoan Blanc
3 min readJul 22, 2018

If you follow the very prolific Kenneth Reitz, you might have already started using pipenv, the Python Dev Workflow for Humans. If you don’t, give it a try, as it hides away many of the issues of Python development, like setting up a virtual env or juggling with the requirements.txt files.

The goal here is to setup a basic Flask web application.

# hello_world/__init__.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

That will be our “Hello world” application. Now starts the hard part, using Pipenv.

# creating the Pipfile and Pipfile.lock
% pipenv --python 3.7
# installing the dependencies
% pipenv install Flask
# run the application
% pipenv run python hello_world/__init__.py

At this point, we have a running application in the browser at the port given by Flask, usually http://localhost:5000.

Producing a requirements.txt file

One of the tedious work of a Python package maintainer is to deal with requirement dependencies, either in a setup.py or requirements.txt file. Pipenv fixes that part but, we would still like to have such files for the packaging or installation of the application. The lock feature does that.

# install it as a development dependency using --dev
% pipenv lock -r > requirements.txt

Building the Python package

In 2018, Python packages are known as wheels, which replaced the old format, formerly eggs. But, this means that we still have to write a boring setup.py file. Let’s keep it to the bare minimum with pbr.

% pipenv install --dev pbr

With the simplest setup.py one could come up with.

# setup.py
from setuptools import setup
setup(setup_requires=["pbr"],
pbr=True)

That’s all there is for setup.py, as the config goes into setup.cfg.

[metadata]
name = hello-world
[files]
packages =
hello_world

Now, it’s time to roll that wheel.

% pipenv run python setup.py bdist_wheelException: Versioning for this project requires either an sdist tarball, or access to an upstream git repository.

pbr can relies on Git tags for the versioning of the project. Which is probably the easiest way to do it.

% git init .% pipenv run python setup.py bdist_wheel
...
adding 'hello_world/__init__.py'
...
% ls dist/
hello_world-0.0.0-py3-none-any.whl

Isn’t it brilliant?

Building the Docker image

Because we can, we shall now build and deploy our hello world application via a container. And it will even serve a static file, the Flask logo.

% mkdir static% wget https://flask.palletsprojects.com/en/1.0.x/_static/flask-icon.png \
-O static/flask.png

Let’s change the hello function to render some HTML with the image we just downloaded.

from flask import Flask, make_response
app = Flask(__name__)
@app.route("/")
def hello():
return make_response("""\
<!DOCTYPE html>
<meta charset="utf-8">
<title>Flask is fun</title>
<img src="flask.png" alt="Flask">""")

It will be a multistage dockerfile to separate the packages building with its usage. The first stage uses Kenneth’s pipenv image to create the .whl file

FROM kennethreitz/pipenv as buildADD . /app
WORKDIR /app
RUN pipenv install --dev \
&& pipenv lock -r > requirements.txt \
&& pipenv run python setup.py bdist_wheel

And in the same file, we will use that wheel to create the uWSGI container.

# in the same DockerfileFROM ubuntu:bionicCOPY --from=build /app/dist/*.whl .ARG DEBIAN_FRONTEND=noninteractiveRUN set -xe \
&& apt-get update -q \
&& apt-get install -y -q \
python3-wheel \
python3-pip \
uwsgi-plugin-python3 \
&& python3 -m pip install *.whl \
&& apt-get remove -y python3-pip python3-wheel \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -f *.whl \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app \
&& useradd _uwsgi --no-create-home --user-group
USER _uwsgi
ADD static /app/static
ENTRYPOINT ["/usr/bin/uwsgi", \
"--master", \
"--die-on-term", \
"--plugin", "python3"]
CMD ["--http-socket", "0.0.0.0:8000", \
"--processes", "4", \
"--chdir", "/app", \
"--check-static", "static", \
"--module", "hello_world:app"]

It’s build time! And run time, a bit later.

% docker build -t hello-world .% docker run -p 8000:8000 hello-world

It everything went well, you’ll have the following.

The docker image is around 180MB big which is reasonable. Check out the source code on GitHub.

--

--