How to Install a Frappe App on an Offline Ubuntu Server (Without GitHub or PyPI)

How to install a Frappe app on an offline Ubuntu server without internet access

Frappe install app without cloning from github

If you have ever built a custom Frappe Framework application on your local workstation — whether running WSL2 on Windows, native Ubuntu, or macOS — and then needed to deploy it onto a production server with zero internet connectivity, you have likely hit a brick wall. 🚧

Standard Frappe workflows rely heavily on online package indexes and remote repositories. Running bench get-app tries to clone from Git repositories via GitHub or GitLab, while pip attempts to query PyPI for Python dependencies, and yarn or npm fetch JavaScript packages for frontend assets. On an air-gapped, isolated enterprise network, every single one of these operations will time out and fail. ❌

The good news is that this is completely solvable. You do not need internet access on your target production server to deploy custom Frappe applications. You simply need to mirror offline what bench and pip do online: download dependencies ahead of time on a connected workstation, package them up correctly, transport the archive, and perform a fully offline installation.

This step-by-step guide covers the end-to-end workflow for deploying custom Frappe apps into air-gapped Ubuntu environments without losing developer velocity or compromising system integrity. 🛠️


⏱️ Time to Complete (~25 minutes)

🎯 What You'll Achieve / Learn

  • 🧠 Understand why standard bench get-app commands fail in air-gapped environments
  • ⚙️ Match OS architecture, Python versions, and C-extension binary wheels between your prep machine and target server
  • 📦 Pre-download platform-compatible Python wheels (.whl) using pip download
  • 🚚 Package source code, Python wheels, and pre-built node_modules into a single portable bundle
  • 🔌 Install Python packages locally using ./env/bin/pip install --no-index --find-links
  • 📝 Register custom apps manually in Frappe's sites/apps.txt and execute site migrations offline
  • 🔍 Troubleshoot binary wheel architecture mismatches, missing dependencies, and frontend asset compilation errors

🛠️ Prerequisites

  • 💻 A "Prep" Workstation with Internet Access: Your local development machine (Linux, WSL2, or macOS) with Python 3.10+ and Git installed.
  • 🖥️ An Air-Gapped Target Ubuntu Server: An installed Ubuntu Linux server running Frappe Bench without outbound internet access.
  • 💾 File Transfer Access: A method to move files to the server (USB drive, internal network scp / SFTP, or an enterprise artifact storage share).

Why Standard Frappe Installation Fails Offline 🧠

Architecture matching and pre-downloading Python wheel dependencies

Section visual: Matching prep workstation architecture with air-gapped target server before downloading wheels.

When you execute the standard command:

bench get-app https://github.com/your-org/my_custom_app.git

Frappe's CLI performs three network-dependent tasks behind the scenes:

  1. Source Code Fetch: Clones the app's git repository into ~/frappe-bench/apps/my_custom_app.
  2. Python Dependency Resolution: Invokes pip install -e apps/my_custom_app, which reads pyproject.toml or setup.py and connects to PyPI to download dependent Python wheels.
  3. Frontend Asset Bundling: Runs yarn install or npm install inside the app directory if node dependencies are defined in package.json.

In an air-gapped environment (financial institutions, defense systems, internal private clouds, or isolated VLANs), all network sockets to external domains are blocked by firewalls. 🔒

To bypass this without altering Frappe core logic, we split the deployment into two distinct phases:

  • Phase A (Connected Prep Machine): Download app code, pre-fetch matching binary wheels, install frontend node modules, and package everything into a .tar.gz archive.
  • Phase B (Air-Gapped Target Server): Transfer the archive, unpack it into apps/, run an offline pip install using --no-index, register the app in apps.txt, and execute bench migrate.

Crucial Step: Architecture & Python Version Matching ⚖️

The most common point of failure in offline deployments is binary incompatibility between Python wheels.

Python packages fall into two main categories:

  1. Pure Python Packages: Written entirely in .py code (e.g., requests, jinja2). These work across all platforms.
  2. C-Extension Packages: Packages containing compiled C/C++ or Rust code (e.g., cryptography, psycopg2, numpy, pillow). These are distributed as pre-compiled platform-specific wheels (.whl).

If you pre-download wheels on an Apple Silicon Mac (arm64) or macOS, those wheels will fail to install on an x86_64 Ubuntu Linux server. Similarly, if your prep machine runs Python 3.12 and your target server runs Python 3.10, version-specific wheel tags will cause pip to reject them offline.

Step 0: Audit Your Target Ubuntu Server

Auditing target server CPU architecture and Python version

Section visual: Auditing target CPU architecture (uname -m) and Python version (python3 --version) before pre-downloading wheels.

Before downloading dependencies on your connected workstation, access your target server terminal (or consult your infrastructure inventory) and check two key system variables:

# Check Python version installed inside your bench environment
~/frappe-bench/env/bin/python --version

# Check Linux CPU architecture
uname -m

You will typically receive:

  • Python Version: Python 3.10.x, Python 3.11.x, or Python 3.12.x
  • Architecture: x86_64 (standard 64-bit Intel/AMD) or aarch64 (ARM 64-bit)

Write these values down. You will feed them directly into pip on your prep machine. 📝


Step 1: Prepare Source Code on Your Connected Workstation 💻

On your internet-connected workstation, clone or locate your custom Frappe app repository:

git clone https://github.com/your-org/my_custom_app.git
cd my_custom_app

Clean out any existing build cache, compiled bytecode, or temporary files to ensure a lean bundle:

# Remove Python bytecode cache directories
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete

Step 2: Download Python Dependencies as Platform Wheels 📦

Frappe apps specify their Python dependencies inside pyproject.toml (standard for Frappe v14, v15, and v16).

On your prep workstation, navigate into your app directory and create a dedicated wheels cache folder:

cd my_custom_app
mkdir -p wheels-cache

Now execute pip download, passing explicit platform, Python version, and implementation flags matching your target server:

pip download -d ./wheels-cache . \
  --platform manylinux2014_x86_64 \
  --python-version 3.11 \
  --implementation cp \
  --only-binary=:all:

Explaining the Arguments:

  • -d ./wheels-cache: Tells pip to output all downloaded .whl files into this directory.
  • .: Tells pip to inspect pyproject.toml in the current working directory.
  • --platform manylinux2014_x86_64: Forces pip to download Linux x86_64 pre-compiled wheels, even if you run this command on macOS or Windows! (Use manylinux2014_aarch64 for ARM servers).
  • --python-version 3.11: Download wheels compiled for Python 3.11 (adjust to match your target server's Python version).
  • --implementation cp: Target CPython (standard Python runtime).
  • --only-binary=:all:: Rejects source packages (.tar.gz sdist) and requires pre-built binaries. This prevents target compilation errors on servers lacking C compilers (gcc, make, python3-dev).

[!TIP] If pip download fails for a specific obscure dependency with --only-binary=:all:, remove --only-binary=:all: for that package so its source tarball is downloaded, but ensure your target server has compilation build essentials installed.


Step 3: Bundle Frontend Assets & Node Modules (If Applicable) 🎨

If your custom Frappe app includes custom Desk pages, Vue/React components, custom bundles, or JS libraries defined in package.json, bench build will require Node dependencies.

On your prep workstation, run yarn/npm install inside the app directory before archiving:

# Install node packages locally so node_modules is pre-populated
yarn install

Make sure node_modules remains inside my_custom_app folder so it gets archived into your release bundle.


Step 4: Create the Offline Deployment Bundle 🗜️

Bundling app source code, Python wheels, and node_modules into a tarball archive

Section visual: Compressing app source code, wheels-cache, and node_modules into a single release archive.

Now compress the entire app folder — containing source code, wheels-cache/, and node_modules/ — into a single archive:

cd ..
tar -czvf my_custom_app_offline_v1.0.tar.gz my_custom_app/

Verify that the generated archive contains the required folders:

tar -tzvf my_custom_app_offline_v1.0.tar.gz | head -n 20

Step 5: Transfer the Archive to the Air-Gapped Server 🚚

Transferring deployment bundle to air-gapped server via SCP or USB storage

Section visual: Methods for moving the offline deployment bundle to the target Ubuntu server.

Transfer my_custom_app_offline_v1.0.tar.gz to your target Ubuntu server using your available offline medium:

Option A: via Secure Copy (SCP / SFTP over internal network)

scp my_custom_app_offline_v1.0.tar.gz [email protected]:/home/deploy_user/

Option B: via Mounted USB Drive

cp my_custom_app_offline_v1.0.tar.gz /media/usb-drive/
# On target server:
cp /media/usb-drive/my_custom_app_offline_v1.0.tar.gz ~/

Step 6: Extract and Perform Offline Installation 🔌

Offline bench pip installation and app registration workflow

Section visual: Terminal commands for offline pip wheel installation and bench registration.

Log into your target air-gapped Ubuntu server as the bench user (e.g., frappe or deploy_user).

Extract the archive into your user home directory:

tar -xzvf my_custom_app_offline_v1.0.tar.gz

Copy the app directory directly into your bench's apps/ directory:

cp -r my_custom_app ~/frappe-bench/apps/

Install Python Dependencies Without Internet

Navigate to the frappe-bench directory. Run pip using the bench's isolated virtual environment Python binary (./env/bin/pip).

Use --no-index to block PyPI connections and --find-links to point pip to your offline wheel cache:

cd ~/frappe-bench

./env/bin/pip install \
  --no-index \
  --find-links=apps/my_custom_app/wheels-cache \
  -e apps/my_custom_app
  • --no-index: Completely disables automatic network lookups to PyPI.
  • --find-links=apps/my_custom_app/wheels-cache: Forces pip to search only inside our pre-downloaded wheel directory.
  • -e apps/my_custom_app: Installs the app in editable mode, allowing Frappe to resolve module paths dynamically.

If all dependencies match, pip will install every package within seconds! ✨


Step 7: Register App in Bench Apps List 📝

Normally, bench get-app appends the new app's name to sites/apps.txt. Since we bypassed bench get-app, we must register the app manually.

Verify whether my_custom_app is present in sites/apps.txt:

cat sites/apps.txt

If it is not listed, append it:

echo "my_custom_app" >> sites/apps.txt

Step 8: Build Frontend Assets Offline ⚡

If your app contains frontend assets or JS bundles, build them now:

bench build --app my_custom_app

Because node_modules was bundled during Phase A, bench build will compile assets locally without attempting to query npm/yarn online registries.


Step 9: Install App on Site & Migrate Database 🚀

Now execute standard bench commands to link the app to your site and apply database migrations, doctype creations, and fixtures:

# Install app to site
bench --site your-site-name install-app my_custom_app

# Execute migrations
bench --site your-site-name migrate

Clear the Redis cache to ensure Desk loads fresh metadata:

bench --site your-site-name clear-cache

Step 10: Restart Services 🔄

Restart your production process manager to pick up Python module changes:

For Production Environments (Supervisor / Systemd):

sudo supervisorctl restart all
# Or if using systemd units:
# sudo systemctl restart frappe-bench-workers

For Development / Single-Process Environments:

bench restart

Your custom app is now fully installed, running, and accessible on your air-gapped Ubuntu server! 🎉


Comprehensive Troubleshooting & Diagnostic Matrix 🔍

Diagnostic checklist and troubleshooting matrix for offline Frappe deployments

Section visual: Diagnostic checklist and resolution paths for common offline deployment errors.

| Problem / Error Message | Root Cause | Solution |

| :--- | :--- | :--- | | ERROR: Could not find a version that satisfies the requirement | Missing wheel for target platform or Python version mismatch. | Re-run pip download on prep machine with exact --platform and --python-version matching python --version on server. | | App my_custom_app not found during install-app | App directory name mismatch or missing entry in sites/apps.txt. | Ensure apps/my_custom_app matches app package name and append name to sites/apps.txt. | | yarn install hangs or times out during bench build | Missing pre-bundled node_modules directory. | Run yarn install on prep workstation before creating .tar.gz bundle. | | ModuleNotFoundError: No module named 'xyz' | Secondary dependency omitted from wheels-cache. | Run pip download against pyproject.toml ensuring recursive resolution without cached wheel suppression. | | Permission denied on .whl files | Incorrect file ownership after extract. | Run chown -R frappe:frappe ~/frappe-bench/apps/my_custom_app. |


Key Takeaways & Best Practices 💡

  1. Treat Dependencies as Build Artifacts: In air-gapped environments, source code alone is incomplete. Code + matching compiled .whl binaries form the actual deployment unit.
  2. Automate Prep Scripts: Create a shell script on your prep machine (build_offline_bundle.sh) that automates pip download, yarn install, and tar compression to eliminate human error during releases.
  3. Verify Target Environment Early: Always check python --version and uname -m on the target server before fetching wheels.

Related Posts on Frappe & ERPNext Deployment


Related posts