Python AppImage Distribution — Deep Dive
AppImage runtime internals
The AppImage runtime is a small (< 200 KB) ELF binary prepended to the SquashFS image. When executed:
- FUSE check — verifies FUSE is available. If not, falls back to extracting to a temp directory.
- Mount — mounts the SquashFS portion of itself at a temp mount point (
/tmp/.mount_AppNaXXXXXX). - Environment setup — sets
APPDIR,APPIMAGE,ARGV0, andOWD(original working directory). - AppRun execution — executes
$APPDIR/AppRunwith the original command-line arguments. - Cleanup — on exit, unmounts the filesystem.
# Inspect an AppImage
./MyApp.AppImage --appimage-extract # Extract without FUSE
ls squashfs-root/ # The extracted AppDir
# Get runtime information
./MyApp.AppImage --appimage-version
./MyApp.AppImage --appimage-offset # SquashFS offset in the file
The offset information is useful for manual inspection:
# Mount manually
OFFSET=$(./MyApp.AppImage --appimage-offset)
sudo mount -o loop,offset=$OFFSET MyApp.AppImage /mnt/appimage
ls /mnt/appimage/
Building from scratch with appimagetool
For maximum control, build the AppDir manually:
#!/bin/bash
set -euo pipefail
APP="MyPythonApp"
PYTHON_VERSION="3.11"
APPDIR="${APP}.AppDir"
# 1. Create directory structure
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" "$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/256x256/apps"
# 2. Download portable Python
wget "https://github.com/niess/python-appimage/releases/download/python${PYTHON_VERSION}/python${PYTHON_VERSION}.15-cp${PYTHON_VERSION//./}-cp${PYTHON_VERSION//./}-manylinux2014_x86_64.AppImage" \
-O python.AppImage
chmod +x python.AppImage
./python.AppImage --appimage-extract
mv squashfs-root/usr/* "$APPDIR/usr/"
rm -rf squashfs-root python.AppImage
# 3. Install your application
"$APPDIR/usr/bin/python${PYTHON_VERSION}" -m pip install \
--prefix="$APPDIR/usr" \
--no-warn-script-location \
-r requirements.txt .
# 4. Create AppRun
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
export PATH="$HERE/usr/bin:$PATH"
export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
export PYTHONPATH="$HERE/usr/lib/python3.11/site-packages:$PYTHONPATH"
export PYTHONHOME="$HERE/usr"
exec "$HERE/usr/bin/python3.11" -m myapp "$@"
EOF
chmod +x "$APPDIR/AppRun"
# 5. Add desktop integration files
cp myapp.desktop "$APPDIR/usr/share/applications/"
cp myapp.desktop "$APPDIR/"
cp icon.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/myapp.png"
cp icon.png "$APPDIR/myapp.png"
# 6. Package with appimagetool
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage "$APPDIR" "${APP}-x86_64.AppImage"
Library bundling and compatibility
The trickiest part of AppImage creation is ensuring all shared library dependencies are bundled. The target is to support Linux distributions from the last 3-4 years.
Finding dependencies
# List all shared library dependencies
find "$APPDIR" -name "*.so*" -exec ldd {} \; | grep "not found"
# Copy missing libraries
for lib in libssl.so.3 libcrypto.so.3; do
cp "/usr/lib/x86_64-linux-gnu/$lib" "$APPDIR/usr/lib/"
done
The glibc problem
AppImages depend on the host’s glibc (C library). An AppImage built on Ubuntu 22.04 (glibc 2.35) will not run on CentOS 7 (glibc 2.17). The solution: build on the oldest supported system.
# Dockerfile for maximum compatibility builds
FROM centos:7
RUN yum install -y gcc make openssl-devel bzip2-devel libffi-devel
# Build Python from source against old glibc
RUN wget https://www.python.org/ftp/python/3.11.8/Python-3.11.8.tgz && \
tar xzf Python-3.11.8.tgz && \
cd Python-3.11.8 && \
./configure --prefix=/usr/local --enable-optimizations && \
make -j$(nproc) && make install
The python-appimage project provides pre-built Python AppImages built on manylinux containers, which target glibc 2.17 — compatible with most Linux systems from 2014 onwards.
Using linuxdeploy for automatic bundling
# linuxdeploy automatically finds and bundles dependencies
./linuxdeploy-x86_64.AppImage \
--appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/python3.11" \
--library /usr/lib/x86_64-linux-gnu/libpython3.11.so.1.0 \
--desktop-file myapp.desktop \
--icon-file myapp.png \
--output appimage
# linuxdeploy runs ldd, copies missing libraries, patches RPATH
Delta updates with AppImageUpdate
AppImages can self-update using zsync (delta updates — only download changed blocks):
Embedding update information
# Build with update information
./appimagetool-x86_64.AppImage \
--updateinformation "gh-releases-zsync|username|repo|latest|MyApp-*x86_64.AppImage.zsync" \
"$APPDIR" MyApp-x86_64.AppImage
The update information string tells the updater where to check:
gh-releases-zsync|user|repo|tag|pattern— GitHub Releasesbintray-zsync|user|repo|package|pattern— Bintray (deprecated)zsync|https://example.com/MyApp-latest-x86_64.AppImage.zsync— Custom server
Generating zsync files
# appimagetool generates .zsync files automatically with --updateinformation
# Or generate manually:
zsyncmake -u "https://github.com/user/repo/releases/download/v1.0/MyApp-x86_64.AppImage" \
MyApp-x86_64.AppImage
Update from within the application
import subprocess
import os
def check_for_updates():
appimage_path = os.environ.get("APPIMAGE")
if not appimage_path:
return # Not running as AppImage
result = subprocess.run(
["AppImageUpdate", appimage_path],
capture_output=True, text=True
)
if result.returncode == 0:
# Restart with new version
os.execv(appimage_path, [appimage_path] + sys.argv[1:])
Code signing
AppImages support GPG and ELF signing for integrity verification:
# Sign with GPG
gpg --detach-sign MyApp-x86_64.AppImage
# Creates MyApp-x86_64.AppImage.sig
# Embed signature in the AppImage (uses appimagetool)
./appimagetool-x86_64.AppImage --sign "$APPDIR" MyApp-x86_64.AppImage
# Verify
./MyApp-x86_64.AppImage --appimage-signature
gpg --verify MyApp-x86_64.AppImage.sig MyApp-x86_64.AppImage
CI/CD pipeline
# GitHub Actions
name: Build AppImage
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-20.04 # Use older Ubuntu for glibc compatibility
steps:
- uses: actions/checkout@v4
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y libfuse2
pip install python-appimage
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool
- name: Build AppImage
run: |
python-appimage build app -p 3.11 .
- name: Test AppImage
run: |
chmod +x *.AppImage
./*.AppImage --help
./*.AppImage --self-test
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v1
with:
files: "*.AppImage*"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Handling edge cases
FUSE not available
Some systems (containers, restrictive servers) lack FUSE. AppImages handle this:
# Extract and run without FUSE
./MyApp.AppImage --appimage-extract-and-run
# Or extract permanently
./MyApp.AppImage --appimage-extract
./squashfs-root/AppRun
Wayland and modern display servers
# AppRun with Wayland support
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
export GDK_BACKEND="${GDK_BACKEND:-wayland,x11}"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland;xcb}"
# ... rest of environment setup
exec "$HERE/usr/bin/python3.11" -m myapp "$@"
Multi-architecture support
ARM64 AppImages are increasingly important (Raspberry Pi, ARM laptops):
# GitHub Actions matrix build
jobs:
build:
strategy:
matrix:
include:
- arch: x86_64
runner: ubuntu-20.04
- arch: aarch64
runner: ubuntu-20.04-arm64 # ARM runner
runs-on: ${{ matrix.runner }}
steps:
- name: Build
run: python-appimage build app -p 3.11 .
Size optimization
AppImages can be large due to bundling Python and all libraries. Optimization strategies:
# Remove unnecessary files from AppDir
rm -rf "$APPDIR/usr/lib/python3.11/test" # Test suite (~30 MB)
rm -rf "$APPDIR/usr/lib/python3.11/ensurepip" # pip bootstrapper
rm -rf "$APPDIR/usr/lib/python3.11/idlelib" # IDLE editor
rm -rf "$APPDIR/usr/lib/python3.11/tkinter" # Tkinter (if unused)
rm -rf "$APPDIR/usr/lib/python3.11/__pycache__" # Will be regenerated
find "$APPDIR" -name "*.pyc" -delete # Remove .pyc files
find "$APPDIR" -name "*.a" -delete # Remove static libraries
# Strip shared libraries
find "$APPDIR" -name "*.so*" -exec strip --strip-unneeded {} \;
# Use UPX on the runtime (optional, reduces by ~50%)
upx --best "$APPDIR/AppRun" 2>/dev/null || true
Typical sizes:
| Content | Before optimization | After optimization |
|---|---|---|
| Python 3.11 runtime only | 80 MB | 35 MB |
| + Flask + requests | 95 MB | 45 MB |
| + NumPy + pandas | 180 MB | 100 MB |
| + PyQt5 | 200 MB | 120 MB |
The SquashFS compression (used by appimagetool) typically achieves 2:1 compression, so the final .AppImage is roughly half the uncompressed AppDir size.
One thing to remember: AppImage’s power is its radical simplicity — one file, no installation, no root, no store — and mastering the AppDir construction, library bundling, and update mechanisms lets you deliver professional Python applications with the lowest possible friction for Linux users.
See Also
- Python Briefcase Native Apps Imagine a travel agent who repacks your suitcase for each country's customs — Briefcase converts your Python app into proper native packages for every platform.
- Python Flatpak Packaging Flatpak wraps your Python app in a safe bubble that works on every Linux system — like a snow globe that keeps your program perfect inside.
- Python Mypyc Compilation Your type hints are not just for documentation — mypyc turns them into speed boosts by compiling typed Python into fast C extensions.
- Python Nuitka Compilation What if your Python code could run as fast as a race car instead of a bicycle? Nuitka translates Python into C to make that happen.
- Python Pex Executables Imagine zipping your entire Python project into a single magic file that runs anywhere Python lives — that's what PEX does.