← Back to zeit/pkg

How to Deploy & Use zeit/pkg

PKG Deployment & Usage Guide

Package your Node.js project into a standalone executable for Linux, macOS, and Windows without requiring Node.js installation on the target machine.

Deprecation Notice: pkg is deprecated as of version 5.8.1 (the final release). Consider Node.js 21+ single executable applications for new projects.

1. Prerequisites

  • Node.js: Version 8.x or higher (matching or exceeding your target Node version)
  • npm or yarn: For package installation
  • Build tools (if using native addons): Python 3.x, C++ compiler (Visual Studio Build Tools on Windows, Xcode Command Line Tools on macOS, or build-essential on Linux)
  • For cross-compilation:
    • Linux: QEMU user emulation configured (binfmt_misc support) to run foreign architecture binaries
    • macOS: Rosetta 2 (for building x64 on ARM64, but not reverse)
    • Windows: x64 emulation (for building x64 on ARM64, but not reverse)
  • For macOS signing on Linux: ldid utility installed
  • For macOS deployment: codesign utility (Xcode Command Line Tools)

2. Installation

Install globally for CLI access:

npm install -g pkg@5.8.1

Or install locally per project:

npm install --save-dev pkg@5.8.1

Verify installation:

pkg --version
pkg --help

3. Configuration

Package.json Configuration

Add a pkg field to your package.json to define build parameters:

{
  "name": "my-app",
  "bin": "index.js",
  "pkg": {
    "scripts": [
      "lib/**/*.js",
      "routes/**/*.js"
    ],
    "assets": [
      "views/**/*",
      "config/*.json",
      "node_modules/some-package/assets/**/*"
    ],
    "targets": [
      "node18-linux-x64",
      "node18-win-x64",
      "node18-macos-x64"
    ],
    "outputPath": "dist",
    "compress": "GZip"
  }
}

Key Configuration Options:

FieldDescription
scriptsGlob patterns for JS files to include as bytecode
assetsNon-JS files to embed in the executable (accessible via virtual filesystem)
targetsDefault target platforms when none specified via CLI
patchesAdvanced: inline replacements for problematic packages (see dictionary examples)

Environment Variables

  • DEBUG_PKG=1: Enable basic diagnostic output (shows virtual filesystem structure)
  • DEBUG_PKG=2: Enable verbose diagnostics (includes DICT contents)
  • PKG_CACHE_PATH: Override default cache location for downloaded base binaries

4. Build & Run

Basic Usage

Package current directory (follows bin entry in package.json):

pkg .

Package specific entry file:

pkg index.js

Target Specification

Format: nodeRange-platform-arch (e.g., node18-linux-x64, node16-win-arm64)

Build for current platform:

pkg index.js

Build for specific targets:

pkg -t node18-linux-x64,node18-win-x64,node18-macos-x64 index.js

Build for latest Node on specific platforms:

pkg -t linux,macos,win index.js

Use host alias for current platform/Node version:

pkg -t host index.js

Output Control

Specify output filename:

pkg -o myapp-linux index.js

Specify output directory:

pkg --out-path ./dist index.js

Advanced Build Options

Include V8 options (baked into executable):

pkg --options "expose-gc,max-heap-size=34" index.js

Compression (reduces executable size):

pkg --compress GZip index.js
# or
pkg --compress Brotli index.js

Disable bytecode (source files included as plain JS - useful for cross-compilation when emulation unavailable):

pkg --no-bytecode --public-packages "*" --public index.js

Skip native addon builds (if no native dependencies):

pkg --no-native-build index.js

Skip macOS signature (if handling signing manually):

pkg --no-signature index.js

Runtime Behavior

Inside the packaged executable:

  • Virtual filesystem root: /snapshot (Linux/macOS) or C:\snapshot (Windows)
  • Use __dirname and require.resolve() to access bundled assets
  • Native addons (.node files) are extracted to temporary directories at runtime

Example asset access:

const path = require('path');
const fs = require('fs');

// Access bundled asset
const assetPath = path.join(__dirname, 'config.json');
const config = JSON.parse(fs.readFileSync(assetPath, 'utf8'));

5. Deployment

Distributing Executables

The output binaries are standalone and can be distributed via:

  • GitHub Releases: Upload platform-specific binaries as release assets
  • Package Managers: Submit to Homebrew (macOS), Chocolatey (Windows), or APT repositories (Linux)
  • Direct Download: Host on CDN or company file server
  • Docker: Copy binary into minimal images (scratch, alpine, or distroless)

CI/CD Pipeline Example (GitHub Actions)

name: Build Executables

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install pkg
        run: npm install -g pkg@5.8.1
      
      - name: Setup QEMU
        uses: docker/setup-qemu-action@v2
      
      - name: Build for all platforms
        run: |
          pkg -t node18-linux-x64,node18-linux-arm64,node18-win-x64,node18-macos-x64 --out-path dist .
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: executables
          path: dist/

macOS Code Signing

If pkg fails to ad-hoc sign automatically, or you need a trusted signature:

# Ad-hoc signing (sufficient for local distribution)
codesign --sign - --force dist/myapp-macos

# Developer ID signing (for distribution outside App Store)
codesign --sign "Developer ID Application: Your Name" --force dist/myapp-macos

Handling Native Dependencies

For packages requiring external binaries (like node-notifier or drivelist), ensure:

  1. Assets are included in pkg.assets configuration
  2. Paths are patched to use path.dirname(process.execPath) instead of __dirname (see dictionary examples in repo)
  3. Deploy files are placed alongside the executable in expected relative paths

6. Troubleshooting

"Killed: 9" on macOS

Cause: macOS Gatekeeper/Kernel killing unsigned binaries Solution:

codesign --sign - --force ./executable
# Or disable signing requirement during build:
pkg --no-signature index.js

Bytecode Generation Errors on Cross-Compile

Cause: Cannot run target architecture binary to generate bytecode Solution: Use --no-bytecode --public-packages "*" --public flags (includes source instead of bytecode)

Missing Files at Runtime

Symptom: Error: Cannot find module or missing assets Solution:

  • Add missing files to pkg.assets in package.json
  • Use require.resolve() to verify paths resolve within /snapshot virtual filesystem
  • Enable debugging: DEBUG_PKG=1 ./executable to see virtual filesystem contents

Native Module Loading Failures

Symptom: Error: The specified module could not be found Solution:

  • Ensure .node files are included in assets
  • Verify the native module is compiled for the target Node version
  • Use --no-native-build only if pre-built binaries are included

Large File Size

Optimization:

  • Use --compress GZip or --compress Brotli
  • Exclude unnecessary files from scripts and assets globs
  • Use --no-dict * to disable built-in package dictionaries if not needed

Debug Mode

Enable verbose logging to inspect the virtual filesystem:

DEBUG_PKG=1 ./myapp        # Basic filesystem tree
DEBUG_PKG=2 ./myapp        # Verbose including DICT contents

Windows Defender / Antivirus Flags

Cause: Executable packing resembles malware signatures Solution: Sign the executable with a valid code signing certificate (not just ad-hoc)

Architecture-Specific Issues

  • macOS ARM64: Experimental; ensure ad-hoc signing is present
  • Alpine Linux: Use alpine target (musl libc) instead of linux (glibc)
  • Static linking: Use linuxstatic target for maximum compatibility across Linux distributions