Jason Miller / Nuitka Packaging for Web Frameworks

Created Sat, 26 Jul 2025 22:27:11 -0700 Modified Thu, 31 Jul 2025 20:16:33 +0000
Compiling Newspapers

Guide to Compiling Python Web Frameworks with Nuitka

Unlock simplified deployment for Python web apps. This guide details compiling Flask, FastAPI, and Django with Nuitka and automating cross-platform releases with GitHub Actions, from executables to DEB/RPM packages.

Part 1: Framework Selection and Nuitka Compatibility Analysis

The decision to compile a Python web application into a standalone executable is driven by the need for simplified deployment, intellectual property protection, and potential performance gains. Nuitka, a Python-to-C compiler, is a powerful tool for achieving these goals.1 However, its effectiveness is deeply intertwined with the architecture of the web framework being compiled. This analysis evaluates three of Python’s most prominent web frameworks—FastAPI, Flask, and Django—to determine their suitability for Nuitka compilation, culminating in a definitive recommendation.

Section 1.1: The Python Web Landscape: A Pre-Compilation Performance Baseline

Before introducing Nuitka, it is essential to understand the inherent performance characteristics of each framework. Nuitka acts as an optimization and packaging layer, but it does not fundamentally alter the core architectural design that dictates a framework’s performance profile, particularly under concurrent loads.

  • FastAPI: Built upon the Starlette ASGI toolkit and Uvicorn server, FastAPI is engineered for high performance, especially in I/O-bound scenarios. Its native support for asynchronous operations allows it to handle thousands of concurrent requests efficiently, making it one of the fastest Python frameworks available according to independent benchmarks.3
  • Django: As a full-featured, “batteries-included” framework, Django was traditionally synchronous. However, with the introduction of native ASGI support, its performance capabilities have significantly improved. When deployed with an ASGI server like Uvicorn and multiple workers, Django can sustain high throughput, handling up to 3,000 requests per second in some test scenarios, a considerable figure for a synchronous framework.4
  • Flask: A minimalist and flexible micro-framework, Flask is synchronous by design. While it can be adapted to run in an ASGI context using middleware (e.g., asgiref.wsgi.WsgiToAsgi), this does not change its core request-handling model. Under high concurrent loads, Flask applications can become unstable and prone to deadlocks, typically capping out between 400 and 1,000 requests per second even with optimized configurations.4

The performance hierarchy is clear: FastAPI’s asynchronous architecture gives it a distinct advantage in concurrency, followed by Django’s robust ASGI implementation, with Flask being the least suited for high-concurrency workloads out of the box. Nuitka offers a performance boost by translating Python code into optimized C, thereby reducing the overhead of the Python interpreter.6 This improvement is often realized as a percentage gain—ranging from a modest 10-20% for applications dominated by I/O or C-based libraries, to a more substantial 2-4x for pure, computationally intensive Python functions.6 Applying a percentage-based optimization does not reorder the fundamental performance ranking. A compiled FastAPI application will invariably outperform a compiled Flask application in a high-concurrency scenario because the underlying architectural advantage of

asyncio remains. Therefore, the initial framework selection must be driven by the application’s architectural requirements. Nuitka should be viewed as a powerful tool for optimization and distribution, not as a panacea that can make an architecturally mismatched framework perform beyond its design limits.

Section 1.2: FastAPI with Nuitka: The Asynchronous High-Performer

Compiling a FastAPI application with Nuitka presents a unique set of challenges rooted in its asynchronous nature and reliance on the Uvicorn server.

  • Challenge 1: Programmatic Server Startup: The standard command-line method for launching a FastAPI application, uvicorn main:app, is incompatible with Nuitka. This command relies on Uvicorn dynamically importing the app object from the main module using a string reference. Nuitka, being a static compiler, cannot resolve this runtime import during its analysis phase, which leads to a Could not import module “main” error when the compiled binary is executed.9 The definitive solution is to launch Uvicorn programmatically from within the main script. This involves importing the uvicorn library and the FastAPI app instance directly, then calling uvicorn.run() from within a if __name__ == “__main__”: block, passing the app object itself as an argument (e.g., uvicorn.run(app,…)). This pattern provides a clear, traceable path for Nuitka’s static analysis engine.9
  • Challenge 2: Multi-Worker Concurrency: The most significant hurdle when compiling FastAPI applications is the use of multiple worker processes. Attempting to run a compiled binary with workers > 1 in the uvicorn.run() call frequently results in critical runtime failures, such as child processes dying unexpectedly or multiprocessing/resource_tracker.py warnings.9 This instability arises because Uvicorn’s multi-worker architecture spawns new processes that then attempt to re-import the application module. This mechanism is fundamentally incompatible with a self-contained, compiled executable. While adding multiprocessing.freeze_support() is a necessary prerequisite for any compiled multiprocessing application, it is often not sufficient to resolve this specific issue with Uvicorn.9 Some developers have reported success by replacing Uvicorn with Hypercorn, another ASGI server, though this is not a universally guaranteed solution.12

The persistent issues with Uvicorn’s process management model when applied to a compiled binary lead to a critical architectural conclusion. While Nuitka has robust support for Python’s standard multiprocessing library, the specific implementation used by Uvicorn to manage its worker pool creates an intractable conflict.13 In contrast, servers that employ a single-process, multi-threaded model, such as the WSGI server Waitress, are inherently more compatible with a compiled environment because threads share the same memory space and do not require module re-imports.14 Uvicorn’s default mode for a single worker (

workers=1) relies on a single process running an asyncio event loop, which can manage thousands of concurrent I/O operations without spawning new processes. This model is perfectly stable when compiled. Consequently, the most reliable and stable deployment strategy for a Nuitka-compiled FastAPI application is to run it as a single-process application. Concurrency must be managed entirely by the asyncio event loop, not by scaling out with multiple Uvicorn worker processes. This is a crucial constraint that must be accepted to gain the benefits of compilation.

Section 1.3: Flask with Nuitka: The Versatile and Straightforward Choice

Flask’s simplicity and minimal design make it a surprisingly robust and reliable candidate for Nuitka compilation, presenting challenges that are more manageable than those of FastAPI.

  • Challenge: Packaging Data Files: The primary obstacle when compiling Flask applications is handling non-code assets. Flask projects typically rely on templates and static directories for Jinja2 templates, CSS, and JavaScript files. Nuitka’s compiler, by default, only traces Python import statements and does not automatically include these data directories. This results in runtime errors, most commonly jinja2.exceptions.TemplateNotFound, when the application tries to render a template that was not bundled with the executable.16 The solution is direct and declarative: use Nuitka’s data file inclusion flags during compilation. The --include-data-dir=templates=templates and --include-data-dir=static=static options instruct Nuitka to copy these entire directories into the final distribution folder, preserving the relative paths the Flask application expects.17
  • Server Integration: As with FastAPI, the Flask development server is not suitable for production and the server must be launched programmatically. The recommended approach is to use a production-grade, pure-Python WSGI server like Waitress. This is achieved by importing waitress and calling waitress.serve(app,…) from within the main script’s if __name__ == “__main__”: block.14

The nature of these challenges reveals a key advantage of Flask in the context of compilation. Flask’s main issue—asset management—is a static, compile-time problem with a well-documented and reliable solution provided directly by Nuitka’s command-line interface. In contrast, FastAPI’s primary issue—multi-process concurrency—is a complex, runtime architectural conflict between Uvicorn and the compiled binary’s execution model. Furthermore, the recommended server for a compiled Flask app, Waitress, uses a multi-threaded concurrency model that is inherently compatible with Nuitka and avoids the entire class of process-spawning problems encountered with Uvicorn.14 This means that while a compiled FastAPI application may offer higher raw performance for I/O-bound tasks, a compiled

Flask application is significantly more likely to be stable and straightforward to deploy successfully. For applications where extreme asynchronous throughput is not the primary requirement, Flask emerges as the superior choice specifically for the purpose of Nuitka compilation.

Section 1.4: Django with Nuitka: A Path of High Resistance

Compiling a full-featured Django application with Nuitka is an endeavor fraught with significant challenges, making it an impractical and unsupported option for most production scenarios. The difficulties stem from Django’s highly dynamic and convention-based architecture, which fundamentally clashes with Nuitka’s static analysis approach.

  • Challenge 1: Dynamicism and Implicit Imports: Django’s framework is built on “magic.” It dynamically discovers and loads code based on project structure and environment variables. For instance, it finds management commands by scanning management/commands directories and identifies the settings module via the DJANGO_SETTINGS_MODULE environment variable. Nuitka’s static analysis engine, which works by tracing explicit import statements, is unable to follow this dynamic, string-based logic. This leads to compilation failures, such as fatal errors when the compiler cannot infer the string value of the settings module path, or runtime ModuleNotFoundError exceptions because entire sections of the application were never included in the build.24
  • Challenge 2: The Autoreloader: The Django development server’s autoreload feature, which monitors files for changes, works by forking the main process. This mechanism is incompatible with a compiled binary and must be explicitly disabled by appending the --noreload flag when executing a compiled manage.py command (e.g., ./manage.bin runserver --noreload).25

While some users have reported partial success through a painstaking process of manual intervention—using flags like --include-package=django and --include-package=myapp to force-include entire modules and patching Django’s core logic—the process remains brittle and highly experimental.24 The lead developer of Nuitka has explicitly stated that providing robust, out-of-the-box support for Django is not a project priority without dedicated commercial funding, underscoring its status as an unsupported framework.25

The struggle to compile Django with Nuitka highlights a deeper architectural principle: the trade-off between “convention over configuration” and static analyzability. Django’s power and ease of use come from its reliance on conventions—a developer creates an admin.py file, and Django automatically discovers and registers it. Nuitka, however, requires explicit, traceable code paths. It needs to see a literal import myapp.admin statement to include that file in the compilation. Frameworks like Flask and FastAPI, which favor more explicit code—defining routes via decorators and dependencies via function calls—present a clear, traversable dependency graph for a static compiler. In essence, the act of compiling with Nuitka serves as an architectural audit. It reveals that frameworks prioritizing explicit declarations are far more amenable to static compilation than those that rely heavily on runtime discovery and convention. For projects where compilation is a primary goal, this suggests a design philosophy that favors explicitness at every level.

Table 1: Framework Compatibility and Nuitka Compilation Matrix

Feature FastAPI Flask Django
Baseline Performance High (Asynchronous) Medium (Synchronous) High (with ASGI)
Nuitka Compatibility Moderate High Very Low
Primary Challenge Multi-process concurrency with Uvicorn Asset & template packaging Dynamic imports & framework complexity
Solution Difficulty High (Runtime architectural conflict) Low (Compile-time flags) Very High (Requires code patches)
Recommended Server Uvicorn (single-worker mode) Waitress (multi-threaded) Gunicorn/Uvicorn (highly experimental)
Verdict Best for I/O-bound performance if single-process deployment is acceptable. Most reliable and stable choice for general-purpose compiled web applications. Not recommended for compilation.

Part 2: Implementation Deep Dive: From Python Code to Standalone Executable

This section provides the practical code, commands, and strategies required to successfully compile a web application with Nuitka. It focuses on the recommended frameworks, FastAPI and Flask, and addresses the most common and critical implementation hurdles.

Section 2.1: Foundational Nuitka Strategies for Web Applications

Before addressing framework-specific issues, it is crucial to understand the core Nuitka commands and concepts that form the foundation of any successful compilation.

  • Distribution Modes: Standalone vs. One-File: Nuitka offers two primary modes for creating distributable applications.
    • --standalone: This flag creates a distribution folder (e.g., myapp.dist) containing the main executable along with all necessary shared libraries (.so, .dll, .pyd) and data files. This is the most reliable and recommended starting point for web server applications, as it simplifies debugging and correctly handles complex dependencies.17
    • --onefile: This flag bundles the entire distribution folder into a single executable file. At runtime, this file first extracts its contents to a temporary directory before executing the application. While convenient for distribution, this can lead to slower startup times and can make it more difficult to diagnose issues related to missing files or incorrect paths.17 For server applications, --standalone is the preferred mode.
  • The Nuitka Plugin System: Nuitka employs a powerful plugin system to manage the intricacies of large and complex Python libraries that use implicit imports, dynamic data file loading, or other mechanisms that are opaque to static analysis. For example, GUI toolkits require specific plugins (–enable-plugin=pyside6, --enable-plugin=tk-inter) to ensure all necessary Qt plugins or Tcl/Tk libraries are correctly bundled.27 While there are no mandatory plugins for basic Flask or FastAPI applications, plugins like anti-bloat can be valuable for optimization by stripping unused components from libraries like requests or urllib3, thereby reducing the final binary size.27
  • Managing Dependencies: In standalone mode, Nuitka attempts to trace and include all imported modules automatically. However, if a library is loaded dynamically (e.g., via __import__() with a variable) or is an optional dependency that is not explicitly imported, Nuitka may miss it. To remedy this, Nuitka provides explicit inclusion flags:
    • --include-module=some.module: Forces the inclusion of a single module.
    • --include-package=some_package: Recursively finds and includes all modules within a given package. This is often necessary for libraries with complex internal structures.9

The process of compiling a Python application with Nuitka should not be viewed as a mere final step but rather as a form of static integration testing. In a standard Python environment, dependencies are resolved at runtime, and errors from missing modules or data files may only surface when a specific, rarely-used code path is executed. Nuitka forces this dependency resolution to happen at compile time. A successful compilation serves as a powerful validation that all required code and data dependencies are statically traceable from the application’s entry point. Compilation failures or runtime errors in a compiled application are not merely Nuitka bugs; they are signals of implicit, untracked dependencies within the application’s architecture. Adopting Nuitka early in the development cycle can enforce better dependency discipline and uncover hidden issues that might otherwise complicate even traditional container-based deployments.

Section 2.2: Embedding the Server: Programmatic Lifecycles

The single most critical pattern for successfully compiling a Python web application is to abandon command-line server invocation (e.g., uvicorn main:app) and instead launch the server programmatically from within the main Python script. This provides a direct, analyzable entry point for Nuitka.

A Robust main.py for FastAPI/Uvicorn

To compile a FastAPI application, the main script must be structured to directly control the Uvicorn server lifecycle. The key is to pass the app object itself to uvicorn.run(), avoiding the string-based import that breaks static analysis.

Python

# main.py from fastapi import FastAPI import uvicorn

# Define the FastAPI application instance app = FastAPI()

@app.get("/") def read_root(): return {“Hello”: “World”}

# The entry point for the compiled executable if __name__ == “__main__”: # This block is executed only when the script is run directly. # Nuitka will compile this as the main function of the executable.

\# Run uvicorn programmatically, passing the app object.
\# CRITICAL: Set workers=1 to avoid multiprocessing issues in compiled mode.
\# Concurrency is handled by the asyncio event loop, not by multiple processes.
uvicorn.run(
    app,
    host="0.0.0.0",
    port=8000,
    workers=1  \# This is the most stable configuration for Nuitka.
)

In this structure, the call to uvicorn.run() is explicit. Nuitka can see that the uvicorn module is used and that it takes the app object as an argument, allowing it to correctly bundle all necessary dependencies. Explicitly setting workers=1 is vital to prevent the problematic process-spawning behavior discussed in Part 1.9

A Standalone main.py for Flask/Waitress

Similarly, a Flask application should be launched using a production-grade WSGI server like Waitress. Waitress is an excellent choice for compiled applications as it is pure Python (requiring no C extensions of its own) and uses a multi-threaded model that is highly compatible with Nuitka.14

Python

# main.py from flask import Flask, render_template from waitress import serve

# Define the Flask application instance # Note: When compiling, paths to ’templates’ and ‘static’ folders # must be handled correctly. Nuitka needs to be told to include them. app = Flask(__name__)

@app.route(’/’) def index(): # This will fail at runtime if the ’templates’ folder is not included # in the Nuitka build. return render_template(‘index.html’, message=“Hello from compiled Flask!”)

# The entry point for the compiled executable if __name__ == ‘__main__’: # Launch the application using the Waitress WSGI server. # Waitress is a production-ready, multi-threaded server. serve( app, host=‘0.0.0.0’, port=8080, threads=8 # Configure the number of worker threads. )

This pattern provides a clean entry point for Nuitka and leverages a server architecture (single-process, multi-threaded) that sidesteps the multiprocessing complexities seen with Uvicorn, leading to a more stable and predictable compiled application.

Section 2.3: Taming Concurrency: A Guide to Multiprocessing with Nuitka

Properly handling multiprocessing is crucial for performance and stability, especially when compiling. Nuitka has excellent support for Python’s multiprocessing module, provided certain guidelines are followed.

  • The freeze_support() Prerequisite: On operating systems that use the ‘spawn’ or ‘forkserver’ start methods for multiprocessing (including Windows and macOS by default), it is mandatory to call multiprocessing.freeze_support() at the very beginning of the if __name__ == “__main__”: block. This function allows the newly spawned child process to correctly initialize itself when running as part of a frozen executable. Omitting this call will lead to runtime errors or infinite process spawning.9
  • Nuitka’s Multiprocessing Handling: Nuitka automatically detects the use of the multiprocessing module and applies necessary patches to ensure it functions correctly within the compiled environment. For standard library usage, such as creating a multiprocessing.Pool or Process, it generally works out of the box without special configuration.13 The issues observed with uvicorn --workers > 1 are not a failure of Nuitka’s general multiprocessing support but rather a specific incompatibility with Uvicorn’s worker management implementation.12
  • The Correct Pattern for CPU-Bound Tasks: Since scaling a compiled FastAPI application via Uvicorn workers is not viable, a different approach is needed for CPU-bound tasks that would otherwise block the asyncio event loop. The correct pattern is to use a ProcessPoolExecutor from Python’s concurrent.futures library and offload the blocking work to it using loop.run_in_executor(). This confines the multiprocessing to a well-defined pool managed by the standard library, which is fully compatible with Nuitka, all while the server itself remains a single-process application.

Code Snippet: Offloading CPU-Bound Work in a Compiled FastAPI App

The following example demonstrates how to perform a heavy, synchronous computation in a FastAPI endpoint without blocking the server’s event loop. This pattern is stable and effective in a Nuitka-compiled application.

Python

# main.py (expanded FastAPI example) import asyncio import time from concurrent.futures import ProcessPoolExecutor from multiprocessing import freeze_support

from fastapi import FastAPI import uvicorn

# -– CPU-bound function to be run in a separate process -– def heavy_computation(x: int, y: int) -> int: print(“Computation started in a child process…”) time.sleep(5) # Simulate a long, blocking task result = x * y print(“Computation finished.”) return result

# -– FastAPI Application -– app = FastAPI() # Create a process pool that will be managed by the application lifecycle process_pool = ProcessPoolExecutor()

@app.get("/compute") async def compute_endpoint(x: int = 10, y: int = 20): loop = asyncio.get_running_loop()

\# Offload the blocking function to the process pool.
\# The 'await' will pause this coroutine, allowing the event loop
\# to handle other requests, until the result is ready.
result \= await loop.run\_in\_executor(
    process\_pool, heavy\_computation, x, y
)

return {"result": result}

@app.get("/") def read_root(): return {“message”: “Server is responsive”}

# -– Entry Point -– if __name__ == “__main__”: # CRITICAL: Call freeze_support() first for multiprocessing compatibility. freeze_support()

uvicorn.run(
    app,
    host="0.0.0.0",
    port=8000,
    workers=1
)

This architecture correctly separates concerns: the Uvicorn server in a single process handles I/O-bound concurrency via asyncio, while the ProcessPoolExecutor handles CPU-bound concurrency via multiprocessing in a manner that is both efficient and compatible with Nuitka.

Section 2.4: Build Optimization and Final Touches

Once the application is structured correctly, several Nuitka options can be used to optimize the final build for speed and size.

  • Compiler Caching: The C++ compilation stage is often the most time-consuming part of a Nuitka build. To accelerate subsequent builds, a compiler cache should be used. Nuitka automatically integrates with ccache on Linux and macOS, and clcache on Windows when using the MSVC compiler. As long as these tools are installed and available in the system’s PATH, Nuitka will use them, dramatically reducing recompilation times for unchanged code.27
  • Link-Time Optimization (LTO): Modern C++ compilers support LTO, a form of whole-program optimization performed at the final linking stage. Enabling this with the --lto=yes flag can result in a more performant final binary. However, LTO significantly increases both the compilation time and the memory required by the linker. It represents a trade-off that should be evaluated based on the project’s needs.34
  • Binary Compression with UPX: The Ultimate Packer for eXecutables (UPX) is a tool that can compress executables and shared libraries, reducing their size on disk. Nuitka provides a plugin to automate this process. By adding the --enable-plugin=upx flag to the build command, Nuitka will automatically run UPX on the generated executable and all its bundled libraries, often resulting in a size reduction of 50-70%.27
  • Anti-Bloat Optimizations: Many popular libraries, such as requests, include modules that may not be necessary for every application (e.g., components for FTP support or extensive testing utilities). Nuitka’s anti-bloat plugin can apply a series of transformations to remove these unused parts, further trimming the size of the final distribution. This can be enabled with --enable-plugin=anti-bloat.27

Part 3: The Automated Factory: A Cross-Platform CI/CD Pipeline with GitHub Actions

Automating the compilation, packaging, and release of a Nuitka application is essential for maintaining a consistent and reliable distribution process. GitHub Actions provides a powerful and flexible platform for creating a complete CI/CD pipeline that targets multiple operating systems and package formats.

Section 3.1: Workflow Architecture: Designing a Multi-Platform Build and Release Pipeline

A robust CI/CD pipeline for a compiled application should be designed for clarity, efficiency, and reproducibility. The following architecture leverages GitHub Actions’ core features to build and release the application across multiple platforms.

  • Triggering Mechanism: The workflow will be configured to run automatically whenever a new tag matching the pattern v* (e.g., v1.0.0, v1.2.3-beta) is pushed to the repository. This ensures that releases are tied to specific, versioned commits.
  • Matrix Build Strategy: To build for multiple operating systems simultaneously, the workflow will use a strategy.matrix. This will create parallel jobs, each running on a different virtual environment (e.g., ubuntu-latest, macos-latest, windows-latest). This approach significantly reduces the total time required to produce cross-platform builds.
  • Job Orchestration: The pipeline will be structured as a series of dependent jobs to ensure a logical flow from compilation to final publication:
    1. build: This matrix job compiles the Python application using Nuitka on each target OS. The resulting standalone distribution folder (e.g., myapp.dist) is then archived and uploaded as a per-OS artifact.
    2. create-release: Once all build jobs have succeeded, this job creates a new, empty draft GitHub Release associated with the triggering tag. Creating it as a draft prevents users from seeing an incomplete release while packages are being generated.
    3. package-and-upload: This job downloads the build artifacts from the build job. It then uses packaging tools to create .tar.gz archives, Linux packages (.deb, .rpm, .apk), and any other desired formats. Finally, it uploads all these packages as assets to the draft GitHub Release.
    4. update-homebrew-tap (Optional): For macOS and Linux distribution, this job updates a formula in a separate Homebrew tap repository to point to the new release asset.
    5. publish-release: The final job in the sequence. After all packages have been successfully created and uploaded, this job updates the GitHub Release from a draft to a published state, making it publicly visible and triggering release notifications.

Section 3.2: Step 1 & 2: Compiling and Creating Releases

The initial stages of the workflow involve setting up the environment, compiling the application with Nuitka, and preparing the GitHub Release.

  • Core GitHub Actions:
    • actions/checkout@v4: Essential for checking out the repository’s source code onto the runner.
    • actions/setup-python@v5: Configures the runner with a specific version of Python, ensuring a consistent build environment.
    • Nuitka/Nuitka-Action@main: This official action simplifies the process of running Nuitka. It handles the installation of Nuitka and its dependencies and provides a clean interface for passing compilation flags.29
    • actions/upload-artifact@v4 & actions/download-artifact@v4: These actions are used to pass the compiled application between jobs. The build job uploads the dist folder, and the package-and-upload job downloads it.
    • shogo82148/actions-create-release@v1: A community action used to programmatically create the draft GitHub Release.37
    • shogo82148/actions-upload-release-asset@v1: Used to upload the final package files to the release created in the previous step.37

YAML Snippet: build and create-release Jobs

YAML

name: Build and Release

on: push: tags: - ‘v*’

jobs: build: name: Build on ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }}

steps:
  \- name: Checkout repository
    uses: actions/checkout@v4

  \- name: Set up Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.11'

  \- name: Install dependencies
    run: pip install \-r requirements.txt

  \- name: Build with Nuitka
    uses: Nuitka/Nuitka-Action@main
    with:
      nuitka-version: main
      script-name: main.py
      standalone: true
      \# Add other necessary flags from Part 2, e.g., for data files
      \# include-data-dir: |
      \#   templates=templates
      \#   static=static

  \- name: Upload artifact
    uses: actions/upload-artifact@v4
    with:
      name: myapp-${{ matrix.os }}
      path: main.dist/

create-release: name: Create GitHub Release runs-on: ubuntu-latest needs: build # This job runs only after all ‘build’ matrix jobs succeed permissions: contents: write # Required to create a release steps: - name: Create Release id: create_release uses: shogo82148/actions-create-release@v1 with: tag_name: ${{ github.ref_name }} release_name: Release ${{ github.ref_name }} draft: true # Create as a draft first

outputs:
  upload\_url: ${{ steps.create\_release.outputs.upload\_url }}

Section 3.3: Step 3: Generating Linux Packages with FPM

FPM (Effing Package Management) is an indispensable tool for simplifying the creation of native Linux packages. Its ability to create various package types from a simple directory source (-s dir) makes it a perfect match for packaging the output of a Nuitka --standalone build.38 To ensure the correct build environment and toolchains are available, FPM should be run within Docker containers corresponding to the target distributions.

  • Alpine Linux (.apk) Packages: While Alpine Linux has its own complex packaging system based on APKBUILD scripts and the abuild tool 41, FPM provides a much simpler path. FPM supports .apk as a direct output target (-t apk), allowing for the creation of Alpine packages without needing to write an APKBUILD file.42

YAML Snippet & FPM Commands for the package-and-upload Job

This job will download the Linux artifact, create an archive, and then use Docker to run FPM commands to generate .deb, .rpm, and .apk packages.

YAML

#… continuation of the workflow… package-and-upload: name: Package and Upload Assets runs-on: ubuntu-latest needs: create-release

steps:
  \- name: Download Linux build artifact
    uses: actions/download-artifact@v4
    with:
      name: myapp-ubuntu-latest
      path:./myapp-linux

  \- name: Create Tarball
    run: |
      tar \-czvf myapp-linux-amd64.tar.gz \-C./myapp-linux.

  \- name: Install FPM
    run: sudo gem install fpm

  \- name: Build.deb package
    run: |
      fpm \-s dir \-t deb \\
        \-n myapp \-v ${{ github.ref\_name }} \\
        \--prefix /opt/myapp \\
        \-C./myapp-linux \\
        \-p myapp\_${{ github.ref\_name }}\_amd64.deb \\
       .

  \- name: Build.rpm package
    run: |
      fpm \-s dir \-t rpm \\
        \-n myapp \-v ${{ github.ref\_name }} \\
        \--prefix /opt/myapp \\
        \-C./myapp-linux \\
        \-p myapp-${{ github.ref\_name }}.x86\_64.rpm \\
       .

  \- name: Build.apk package
    run: |
      fpm \-s dir \-t apk \\
        \-n myapp \-v ${{ github.ref\_name }} \\
        \--prefix /opt/myapp \\
        \-C./myapp-linux \\
        \-p myapp-${{ github.ref\_name }}.apk \\
       .

  \- name: Upload Release Assets
    uses: shogo82148/actions-upload-release-asset@v1
    with:
      upload\_url: ${{ needs.create-release.outputs.upload\_url }}
      asset\_path:./\*.{tar.gz,deb,rpm,apk}

Table 2: FPM Command Reference for Nuitka Packaging

Package Type FPM Command Example Key Flags Explained Notes
DEB fpm -s dir -t deb -n myapp -v 1.0.0 --prefix /opt/myapp -C./myapp.dist. --depends ’libc6 > 2.17’ (specifies runtime dependencies), --after-install script.sh (runs a post-install script), --config-files /opt/myapp/config.ini (marks a file as a configuration file). Requires Debian/Ubuntu build environment. dpkg will not resolve dependencies automatically; apt-get install -f is needed. 44
RPM fpm -s dir -t rpm -n myapp -v 1.0.0 --prefix /opt/myapp -C./myapp.dist. --rpm-summary “My App” (sets the package summary), --depends ‘glibc’ (specifies runtime dependencies). Note: Interactive post-install scripts are discouraged as they may fail in automated environments. 45 Requires Red Hat/CentOS build environment with rpm-build installed.
APK fpm -s dir -t apk -n myapp -v 1.0.0 --prefix /opt/myapp -C./myapp.dist. --depends ’libc-utils’ (specifies runtime dependencies). FPM has fewer APK-specific flags compared to DEB/RPM. Requires an Alpine build environment. FPM handles the creation of the control and data tarballs. 43

Section 3.4: Step 4: Distributing on macOS and Linux with a Homebrew Tap

Homebrew is the de facto package manager for macOS and is widely used on Linux. Distributing an application via a custom “tap” provides a seamless installation experience for users.

  • Homebrew Tap Architecture: A tap is simply a Git repository, conventionally named homebrew-<tap_name>, that contains package definitions called “formulae.” These formulae are Ruby scripts located in a Formula/ subdirectory.46
  • Formula for Binary Releases: Instead of building from source, the formula for a Nuitka-compiled application will point directly to a pre-compiled binary artifact. The key components of the formula are:
    • url: A direct link to the downloadable .tar.gz archive hosted on the GitHub Release.
    • sha256: The SHA256 checksum of the archive, which Homebrew uses to verify the integrity of the download.
    • install: A simple block of code that copies the executable from the unpacked archive into the user’s bin directory (e.g., bin.install “myapp”).48
  • Automation with GitHub Actions: Manually updating the formula’s URL and checksum for every release is tedious and error-prone. The Homebrew Releaser GitHub Action (MibexSoftware/homebrew-releaser or similar community actions) automates this entire process. It can be configured to:
    1. Identify the latest release and its assets.
    2. Download the correct .tar.gz for macOS/Linux.
    3. Calculate the sha256 checksum.
    4. Generate a new or updated Ruby formula file with the correct url and sha256.
    5. Commit and push the updated formula directly to the specified tap repository.49

YAML Snippet: update-homebrew-tap Job

This job runs after the packages have been uploaded and uses a dedicated action to update the Homebrew tap.

YAML

#… continuation of the workflow… update-homebrew-tap: name: Update Homebrew Tap runs-on: ubuntu-latest needs: package-and-upload

steps:
  \- name: Update Homebrew Tap
    uses: MibexSoftware/homebrew-releaser@v1
    with:
      \# A Personal Access Token with 'repo' scope for the tap repository.
      \# Store this as a secret in GitHub Actions settings.
      token: ${{ secrets.HOMEBREW\_TAP\_TOKEN }}
      tap: 'YourGitHubUsername/homebrew-tapname'
      formula: 'myapp.rb'
      \# The action will automatically find the release and assets.
      \# It constructs the download URL and calculates the checksum.
      install: |
        bin.install "myapp"

Section 3.5: The Complete Workflow: A Final release.yml

The final step is to assemble all the preceding jobs into a single, cohesive workflow file. This file represents the complete, end-to-end automation pipeline, from source code to a multi-platform, multi-format release. Job dependencies (needs: […]) are used to enforce the correct execution order.

YAML

#.github/workflows/release.yml name: Build and Release Application

on: push: tags: - ‘v*’

jobs: # Job 1: Build on all platforms build: name: Build on ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ‘3.11’ - run: pip install -r requirements.txt - name: Build with Nuitka uses: Nuitka/Nuitka-Action@main with: nuitka-version: main script-name: main.py standalone: true - name: Prepare Artifacts run: | # Rename dist folder for clarity mv main.dist myapp-dist # Create archives for each OS if [ “${{ runner.os }}” == “Linux” ] ||; then tar -czvf myapp-${{ runner.os }}.tar.gz -C myapp-dist. else 7z a myapp-${{ runner.os }}.zip./myapp-dist/* fi - uses: actions/upload-artifact@v4 with: name: myapp-${{ runner.os }} path:./*.{tar.gz,zip}

# Job 2: Create a draft release create-release: name: Create GitHub Release runs-on: ubuntu-latest needs: build permissions: contents: write outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - uses: shogo82148/actions-create-release@v1 id: create_release with: tag_name: ${{ github.ref_name }} release_name: Release ${{ github.ref_name }} draft: true

# Job 3: Create Linux packages and upload all assets package-and-upload: name: Package and Upload Assets runs-on: ubuntu-latest needs: create-release permissions: contents: write steps: - uses: actions/download-artifact@v4 with: path:./artifacts - name: Install FPM run: sudo gem install fpm - name: Create Linux Packages run: | tar -xzvf./artifacts/myapp-Linux/myapp-Linux.tar.gz fpm -s dir -t deb -n myapp -v ${{ github.ref_name }} --prefix /opt/myapp -C. -p myapp_${{ github.ref_name }}_amd64.deb. fpm -s dir -t rpm -n myapp -v ${{ github.ref_name }} --prefix /opt/myapp -C. -p myapp-${{ github.ref_name }}.x86_64.rpm. - name: Upload All Release Assets uses: shogo82148/actions-upload-release-asset@v1 with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: | ./artifacts/myapp-macOS/myapp-macOS.tar.gz ./artifacts/myapp-Windows/myapp-Windows.zip ./*.deb ./*.rpm

# Job 4: Update Homebrew Tap update-homebrew-tap: name: Update Homebrew Tap runs-on: ubuntu-latest needs: package-and-upload steps: - uses: MibexSoftware/homebrew-releaser@v1 with: token: ${{ secrets.HOMEBREW_TAP_TOKEN }} tap: ‘YourGitHubUsername/homebrew-tapname’ formula: ‘myapp.rb’ install: | bin.install “myapp”

# Job 5: Publish the release publish-release: name: Publish GitHub Release runs-on: ubuntu-latest needs: [package-and-upload, update-homebrew-tap] permissions: contents: write steps: - name: Publish Release uses: shogo82148/actions-publish-release@v1 with: tag_name: ${{ github.ref_name }}

Conclusions and Recommendations

This comprehensive analysis of compiling Python web frameworks with Nuitka and automating their distribution yields a clear set of actionable recommendations for developers and DevOps engineers.

  1. Framework Selection is Paramount: The choice of web framework has the most significant impact on the success and stability of a Nuitka compilation project.
    • Recommendation: For most general-purpose web applications intended for compilation, Flask is the recommended framework. Its primary challenges relate to static asset packaging, which are easily and reliably solved with compile-time flags. Its compatibility with multi-threaded WSGI servers like Waitress avoids the complex runtime issues associated with multiprocessing.
    • FastAPI should be chosen only when its high-performance, asynchronous capabilities are a strict requirement. Developers must accept the architectural constraint of running the compiled application in a single-worker process mode and handle CPU-bound concurrency using a ProcessPoolExecutor.
    • Django is not recommended for Nuitka compilation in any production scenario. Its heavy reliance on dynamic imports and convention-over-configuration principles creates fundamental incompatibilities with Nuitka’s static analysis engine.
  2. Adopt a “Compilation-First” Mindset: Treat Nuitka compilation not as a final deployment step but as an integral part of the development and testing cycle.
    • Recommendation: Structure applications from the outset with static analysis in mind. This includes using programmatic server startup, explicitly declaring dependencies, and being mindful of how and where data files are loaded. Compiling early and often will reveal hidden architectural dependencies and enforce a more robust and explicit coding style.
  3. Embrace Automation for Distribution: The complexity of building, packaging, and releasing software for multiple platforms necessitates a fully automated CI/CD pipeline.
    • Recommendation: Implement a comprehensive GitHub Actions workflow as detailed in Part 3. Leverage a matrix strategy for cross-platform builds, use FPM within Docker for creating diverse Linux packages, and employ dedicated actions for managing GitHub Releases and Homebrew taps. This investment in automation ensures that releases are consistent, repeatable, and less prone to human error.

By adhering to these guidelines, teams can successfully leverage Nuitka to transform their Python web applications into easily distributable, high-performance native executables, streamlining deployment and protecting intellectual property across a wide range of target platforms.

Works cited

  1. Nuitka/Nuitka: Nuitka is a Python compiler written in Python. It’s fully compatible with Python 2.6, 2.7, 3.4-3.13. You feed it your Python app, it does a lot of clever things, and spits out an executable or extension module. - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka
  2. Nuitka, accessed July 26, 2025, https://nuitka.net/
  3. Flask, Django, or FastAPI? : r/Python - Reddit, accessed July 26, 2025, https://www.reddit.com/r/Python/comments/1dxcdiy/flask_django_or_fastapi/
  4. agusmakmun/flask-django-quart-fastapi-performance-test … - GitHub, accessed July 26, 2025, https://github.com/agusmakmun/flask-django-quart-fastapi-performance-test-comparison
  5. Benchmarks - FastAPI, accessed July 26, 2025, https://fastapi.tiangolo.com/benchmarks/
  6. Nuitka is the best python compiler I’ve used. I have tried at least three others… | Hacker News, accessed July 26, 2025, https://news.ycombinator.com/item?id=27538051
  7. Nuitka Release 2.5 — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/posts/nuitka-release-25.html
  8. Nuitka Release 1.0 — Nuitka the Python Compiler : r/Python - Reddit, accessed July 26, 2025, https://www.reddit.com/r/Python/comments/wq3l8a/nuitka_release_10_nuitka_the_python_compiler/
  9. Fastapi and uvicorn build failed and mutiple workers failed · Issue #1063 · Nuitka/Nuitka - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/1063
  10. Uvicorn, accessed July 26, 2025, https://www.uvicorn.org/
  11. FastAPI child process gets killed even with enough unused RAM and CPU left in the machine - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/79311202/fastapi-child-process-gets-killed-even-with-enough-unused-ram-and-cpu-left-in-th
  12. Nuitka+fastapi+uvicorn=«multiprocessing/resource_tracker.py:96: UserWarning: resource_tracker: process died unexpectedly, relaunching» · Issue #1139 · Nuitka/Nuitka · GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/1139
  13. [Question] Does Nuitka support multiprocessing? (Yes) · Issue #3050 - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/3050
  14. Waitress — Flask Documentation (3.1.x), accessed July 26, 2025, https://flask.palletsprojects.com/en/stable/deploying/waitress/
  15. Waitress and multiprocess pooling - Google Groups, accessed July 26, 2025, https://groups.google.com/g/pylons-discuss/c/h5kDKI5KsXU
  16. Flask with Templates · Issue #2561 · Nuitka/Nuitka · GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/2561
  17. Use Cases — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/user-documentation/use-cases.html
  18. Is it possible to deploy/distribute Flask as an executable for desktop use? : r/Python - Reddit, accessed July 26, 2025, https://www.reddit.com/r/Python/comments/21evjn/is_it_possible_to_deploydistribute_flask_as_an/
  19. Nuitka User Manual — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/user-documentation/user-manual.html
  20. waitress-serve — waitress 3.0.2 documentation - The Pylons Project, accessed July 26, 2025, https://docs.pylonsproject.org/projects/waitress/en/stable/runner.html
  21. Serving Flask app with waitress on windows - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/51045911/serving-flask-app-with-waitress-on-windows
  22. How to run a Flask App Over HTTPS, using Waitress and NGINX. Updated for 2022., accessed July 26, 2025, https://dev.to/thetrebelcc/how-to-run-a-flask-app-over-https-using-waitress-and-nginx-2020-235c
  23. Flask Waitress Simple Webserver : r/flask - Reddit, accessed July 26, 2025, https://www.reddit.com/r/flask/comments/x7k2kq/flask_waitress_simple_webserver/
  24. Has anyone managed to compile Django with Nuitka?, accessed July 26, 2025, https://forum.djangoproject.com/t/has-anyone-managed-to-compile-django-with-nuitka/35637
  25. Nuitka + Django + Pipenv how to make it work? · Issue #1058 - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/1058
  26. How to Compile Django App using Nuitka · Issue #1266 - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/issues/1266
  27. Nuitka, Python3 and Docker scratch | DAZN Engineering - Medium, accessed July 26, 2025, https://medium.com/dazn-tech/nuitka-python3-and-scratch-4ce209ed6dd6
  28. How make nuitka compile faster? - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/71909575/how-make-nuitka-compile-faster
  29. Action to build with Nuitka on GitHub in your workflows, accessed July 26, 2025, https://github.com/Nuitka/Nuitka-Action
  30. standard.nuitka-package.config.yml - GitHub, accessed July 26, 2025, https://github.com/Nuitka/Nuitka/blob/develop/nuitka/plugins/standard/standard.nuitka-package.config.yml
  31. Can’t compile Python using Nuitka - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/34841288/cant-compile-python-using-nuitka
  32. multiprocessing — Process-based parallelism — Python 3.13.5 documentation, accessed July 26, 2025, https://docs.python.org/3/library/multiprocessing.html
  33. Tips — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/user-documentation/tips.html
  34. Performance — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/user-documentation/performance.html
  35. Converting Python Scripts to Optimized Executables with Nuitka and the UPX Plugin, accessed July 26, 2025, https://nyahmet.medium.com/converting-python-scripts-to-optimized-executables-with-nuitka-and-the-upx-plugin-8fb06af04a7b
  36. Solutions to the Common Issues — Nuitka the Python Compiler, accessed July 26, 2025, https://nuitka.net/user-documentation/common-issue-solutions.html
  37. shogo82148/actions-upload-release-asset: Yet Another … - GitHub, accessed July 26, 2025, https://github.com/shogo82148/actions-upload-release-asset
  38. Building installation packages with FPM | ePages Developer Portal, accessed July 26, 2025, https://developer.epages.com/blog/tech-stories/building-installation-packages-with-fpm/
  39. jordansissel/fpm: Effing package management! Build packages for multiple platforms (deb, rpm, etc) with great ease and sanity. - GitHub, accessed July 26, 2025, https://github.com/jordansissel/fpm
  40. What is FPM? — fpm - packaging made simple 1.7.0 documentation, accessed July 26, 2025, https://fpm.readthedocs.io/en/v1.7.0/intro.html
  41. Creating an Alpine package, accessed July 26, 2025, https://wiki.alpinelinux.org/wiki/Creating_an_Alpine_package
  42. the FPM package manager - Read the Docs, accessed July 26, 2025, https://fpm.readthedocs.io/
  43. apk - Alpine package format - fpm - Read the Docs, accessed July 26, 2025, https://fpm.readthedocs.io/en/latest/packages/apk.html
  44. deb package creation using fpm with build dependencies - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/30802265/deb-package-creation-using-fpm-with-build-dependencies
  45. Create rpm package using fpm with --after-install gives error - Stack Overflow, accessed July 26, 2025, https://stackoverflow.com/questions/42821215/create-rpm-package-using-fpm-with-after-install-gives-error
  46. brew/docs/How-to-Create-and-Maintain-a-Tap.md at master - GitHub, accessed July 26, 2025, https://github.com/Homebrew/brew/blob/master/docs/How-to-Create-and-Maintain-a-Tap.md
  47. Homebrew tap with bottles uploaded to GitHub Releases, accessed July 26, 2025, https://brew.sh/2020/11/18/homebrew-tap-with-bottles-uploaded-to-github-releases/
  48. Creating your own Homebrew tap and grabbing binaries from S3 - DEV Community, accessed July 26, 2025, https://dev.to/lucassha/creating-your-own-homebrew-tap-and-grabbing-binaries-from-s3-15ma
  49. Homebrew Releaser · Actions · GitHub Marketplace, accessed July 26, 2025, https://github.com/marketplace/actions/homebrew-releaser%