Python Docker image variants or how to reduce image size with 95%
This repo describes various methods to reduce Docker image size for a regular FastAPI
Python demo app.
Base images
To make sure we use the same base OS, we can use the following Debian Bullseye (11) based images:
python:3.9-bullseye
914MB
python:3.9-slim-bullseye
125MB
debian:bullseye-slim
80.4MB
gcr.io/distroless/base-debian11
20.3MB
Note that the distroless image is very small, it is a stripped version of Debian without a shell, but including SSL libraries and certificates, dynamic loader and base libraries and some timezone and locale stuff.
Image builds
The process of the image build basically collects the final application components into the final image. So the process is relatively simple:
- install the
FastAPI
demo app and all dependencies - collect all app components into a build dir
- collect all shared libaries into a build dir, except the ones that are present in the final image
- optionally use UPX to compress the executable
- use
strip
where possible to strip symbols and debug info from binaries and shared libraries. We could create "unstripped" image variants to help debugging issues. - compile the final image
Image variants
There are a couple tools available to collect the app components and libraries:
Nuitka
Python compiler compiles the app into C code and creates dynamic executable with python packages as shared libraries. This is really an alternative python interpreter/compilercx_Freeze
compiles Python code into Python bytecode and collects dependencies and librariesPyInstaller
compiles Python code into Python bytecode and collects dependencies and librariesRegular
compiles Python code into Python bytecode and uses the base Python imageSlim
compiles Python code into Python bytecode and uses theslimmed
Python imageBuildPacks
compiles usingpack
, does not require aDockerfile
Notes
UPX
creates a static executable, but after decompression it becomes a dynamic executablecx_Freeze
struggles with someuvicorn
modules and doesn't copy all (system) libs likelibreadline.so.1
,libexpat.so.1
andlibz.so.1
. So we need to add a routine to copy the libraries and setLD_LIBRARY_PATH
PyInstaller
seems to be able to copy all dependant shared libraries etc. Also can useUPX
when available. You might need to setLD_LIBRARY_PATH
.
Final Image sizes
This is the output of docker images
sorted by size:
REPOSITORY TAG IMAGE ID CREATED SIZE
rest-app pyinstaller 2df87860d887 About an hour ago 40.5MB
rest-app freeze d473482901c2 58 seconds ago 43.1MB
rest-app nuitka 29702638154e 54 seconds ago 54.3MB
rest-app slim cb9750b00759 24 hours ago 179MB
rest-app latest db20cf008e29 24 hours ago 968MB
rest-app buildpack dd199e7c23e9 42 years ago 382MB
Note the difference between python-slim (:slim
) images and regular python
images (:latest
).
Also note that it is possible to create very compact images with PyInstaller
and
Google distroless base Debian image. The base / regular Python image is 20 times larger than
the distroless image!
TODO
- compile Python code to bytecode and remove the regular Python code
- use
python3 -mcompileall -b
for legacy.pyc
bytecode file locations - test
Cython
- test
PyOxydizer
- perhaps experiment with Python
-O
and-OO
options - perhaps use
UPX
everywhere