Skip to content

Instantly share code, notes, and snippets.

@ashald
Forked from lukassup/zipapp.md
Created November 28, 2018 16:03
Show Gist options
  • Save ashald/c5520ce5324d93efb14833b4160e367a to your computer and use it in GitHub Desktop.
Save ashald/c5520ce5324d93efb14833b4160e367a to your computer and use it in GitHub Desktop.
Python zipapp

Python zipapp web apps

What's a zipapp?

This concept is very much like .jar or .war archives in Java.

NOTE: The built .pyz zipapp can run on both Python 2 & 3 but you can only build .pyz zipapps with Python 3.5 or later.

Initial setup

There is a single subdirectory called flaskr in the beginning. It's an example Flask app that I'm going to package into a Python zipapp.

$ tree -L 2 --dirsfirst -F .
.
└── flaskr/
    ├── flaskr/
    ├── flaskr.egg-info/
    ├── venv/
    ├── LICENSE
    ├── MANIFEST.in
    ├── README.rst
    ├── requirements.txt
    └── setup.py

4 directories, 5 files

Install to virtualenv

Now let's create a new Python virtual environment next to flaskr subdirectory and install Flask and gunicorn into the new virtual environment:

$ virtualenv -p python3 venv
$ source ./venv/bin/activate
$ pip install Flask gunicorn

Now there should be two subdirectories: flaskr and venv:

$ tree -L 1 --dirsfirst -F .
.
├── flaskr/
└── venv/

2 directories, 0 files

Install all dependencies to project directory

Let's create a new directory called app which will be packaged into a zipapp as a whole.

In order to create a zipapp you must have all dependencies installed in the same directory as your application source code (e.g our app directory), not in a virtualenv. I'm going to create a requirements.txt file to pin the versions of each installed dependent Python package that I've just installed in my virtualenv and then install all of them into the new app directory.

Alternatively, you could skip the virtualenv step and run pip install -t ./app Flask gunicorn if you don't want to pin versions.

$ mkdir app
$ cd app
$ pip freeze > requirements.txt
$ pip install -t . -r requirements.txt

Add application code

Let's copy our application code into the new app directory:

$ cp -r ../flaskr .

Or install it if you have a nice distributable Python package that has a setup.py:

$ pip install -t . ../flaskr

Cleanup

To save disk space you can remove extra pip files and cache before creating a zipapp:

$ rm -rf ./__pycache__ ./*.dist-info
$ cd ..

Now your project should look like this.

$ tree -L 2 --dirsfirst -F .
.
├── app/
│   ├── click/
│   ├── flask/
│   ├── gunicorn/
│   ├── jinja2/
│   ├── markupsafe/
│   ├── werkzeug/
│   ├── itsdangerous.py
│   └── requirements.txt
├── flaskr/
│   ├── flaskr/
│   ├── flaskr.egg-info/
│   ├── venv/
│   ├── LICENSE
│   ├── MANIFEST.in
│   ├── README.rst
│   ├── requirements.txt
│   └── setup.py
└── venv/
    ├── bin/
    ├── include/
    ├── lib/
    └── pip-selfcheck.json

15 directories, 8 files

Create a .pyz app archive

Zipapp module command has this syntax:

$ python3 -m zipapp APP_DIR -m ENTRYPOINT_MODULE:ENTRYPOINT_FUNCTION -p PYTHON_INTERPRETER

I'm going to use gunicorn to serve my Flask app because it's pure Python and will work accross any supported platform without worrying about binary compilation (e.g. uwsgi).

When you run gunicorn command on the command line this is the entrypoint that gets called:

gunicorn.app.wsgiapp:run

So the command line flag for my zipapp entrypoint will be -m 'gunicorn.app.wsgiapp:run'.

I want to use Python 3 so I also set the interpreter with -p '/usr/bin/env python3'.

Let's run our zipapp command

$ python3 -m zipapp app -m 'gunicorn.app.wsgiapp:run' -p '/usr/bin/env python3'

You should get an executable .pyz archive with all dependencies bundled inside:

$ ls -lh app.pyz
-rwxr--r--  1 user  user   4.0M Dec 18 09:38 app.pyz

$ file app.pyz
app.pyz: a /usr/bin/env python3 script executable (binary data)

Run the app from archive

In order to run my app I have to provide the entrypoint for gunicorn like I would run any WSGI Python app. In my case it's flaskr:app:

$ ./app.pyz flaskr:app
[2017-12-18 09:38:35 +0200] [39081] [INFO] Starting gunicorn 19.7.1
[2017-12-18 09:38:35 +0200] [39081] [INFO] Listening at: http://127.0.0.1:8000 (39081)
[2017-12-18 09:38:35 +0200] [39081] [INFO] Using worker: sync
[2017-12-18 09:38:35 +0200] [39084] [INFO] Booting worker with pid: 39084

Hooray, it's running! 🎉

BONUS: Default entrypoint

Let's say I don't want to set my flaskr:app entrypoint each time I run the .pyz archive.

You can do that by creating a custom app entrypoint app/__main__.py with this content:

# -*- coding: utf-8 -*-
import sys
from gunicorn.app.wsgiapp import run
sys.argv.append('flaskr:app')
sys.exit(run())

The repackage your app

$ python3 -m zipapp app -p '/usr/bin/env python3'

And run it

$ ./app.pyz
[2017-12-18 13:35:30 +0200] [44258] [INFO] Starting gunicorn 19.7.1
[2017-12-18 13:35:30 +0200] [44258] [INFO] Listening at: http://127.0.0.1:8000 (44258)
[2017-12-18 13:35:30 +0200] [44258] [INFO] Using worker: sync
[2017-12-18 13:35:30 +0200] [44261] [INFO] Booting worker with pid: 44261
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment