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:

  1. FUSE check — verifies FUSE is available. If not, falls back to extracting to a temp directory.
  2. Mount — mounts the SquashFS portion of itself at a temp mount point (/tmp/.mount_AppNaXXXXXX).
  3. Environment setup — sets APPDIR, APPIMAGE, ARGV0, and OWD (original working directory).
  4. AppRun execution — executes $APPDIR/AppRun with the original command-line arguments.
  5. 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 Releases
  • bintray-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:

ContentBefore optimizationAfter optimization
Python 3.11 runtime only80 MB35 MB
+ Flask + requests95 MB45 MB
+ NumPy + pandas180 MB100 MB
+ PyQt5200 MB120 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.

pythonappimagedistributionlinuxportable

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.