Skip to content

Instantly share code, notes, and snippets.

@lukassup
Last active September 12, 2023 02:17
Show Gist options
  • Save lukassup/cf289fdd39124d5394513a169206631c to your computer and use it in GitHub Desktop.
Save lukassup/cf289fdd39124d5394513a169206631c 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/  # Python package dir
    ├── flaskr/  # Flask app source code
    ├── README.rst
    ├── requirements.txt
    └── setup.py

2 directories, 3 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
@DrKraw
Copy link

DrKraw commented Feb 19, 2018

Can this be done with a Django application?

@lukassup
Copy link
Author

lukassup commented Apr 2, 2018

I think it should work for Django as well. Django provides WSGI entrypoints just like Flask so you can point Gunicorn to them.

@Abdur-rahmaanJ
Copy link

Abdur-rahmaanJ commented Jan 16, 2020

Do you have the demo project somewhere? Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment