DEV Community

Tirth T
Tirth T

Posted on

How I Tricked AWS Elastic Beanstalk Into Using pnpm

You've migrated your Node.js project to pnpm. Locally, everything works well — faster installs, cleaner node_modules, a strict lockfile.

Then you deploy to AWS Elastic Beanstalk.

And it fails.

Here's what happened when we pushed our first pnpm-based deployment:

[ERROR] An error occurred during execution of command [app-deploy] - 
[InstallDependencies]. Stop running the command.
Error: install dependencies fails: Command /bin/sh -c npm --omit=dev install 
failed with error exit status 1.
Stderr: npm warn old lockfile
npm warn old lockfile The package-lock.json file was created with an old version
npm error code EUSAGE
npm error
npm error `npm ci` can only install packages when your package.json and 
package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock 
file with `npm install` before continuing.
Enter fullscreen mode Exit fullscreen mode

The root cause: Elastic Beanstalk's Node.js platform hardcodes npm install during deployment. There's no config toggle, no environment variable, no checkbox in the console. It runs npm install. Period.

Our first fix attempt was to use corepack enable — the standard way to activate pnpm on a normal Node.js installation — inside a platform hook. That also failed:

.platform/hooks/prebuild/01_install_pnpm.sh: line 4: corepack: command not found
.platform/hooks/prebuild/01_install_pnpm.sh: line 22: pnpm: command not found
Enter fullscreen mode Exit fullscreen mode

It turns out Amazon Linux 2023's Node.js package does NOT include corepack, unlike standard nodejs.org distributions. So not only does Beanstalk force npm install, you can't even install pnpm the standard way.

You can't change it. But you can trick it.


The Strategy: An npm Wrapper

Instead of fighting Beanstalk's lifecycle, we work with it. The approach:

  1. Install pnpm on the instance (using Corepack or npm, depending on the platform)
  2. Replace the npm binary with a wrapper script that intercepts install and ci commands and silently redirects them to pnpm
  3. All other npm commands (--version, run, etc.) still use the original npm

Beanstalk thinks it's running npm. It's actually running pnpm. It never knows the difference.

Here's the architecture:

Beanstalk Lifecycle
    │
    ├─ prebuild hook ──► Install pnpm via Corepack
    │                    Replace /usr/bin/npm with wrapper
    │
    ├─ npm install ────► Wrapper intercepts → pnpm install
    │
    └─ npm start ──────► Wrapper passes through → original npm
Enter fullscreen mode Exit fullscreen mode

Project Structure

Elastic Beanstalk uses a .platform directory for lifecycle hooks. Here's what we need:

your-project/
├── .platform/
│   └── hooks/
│       └── prebuild/
│           └── 01_install_pnpm.sh    ← The core script
├── Procfile                           ← web: pnpm start
├── package.json
├── pnpm-lock.yaml                     ← Your pnpm lockfile
└── ...
Enter fullscreen mode Exit fullscreen mode

Why hooks/prebuild?

Beanstalk runs hooks in this order: prebuildnpm installpredeploypostdeploy. We need pnpm ready before Beanstalk attempts to install dependencies.


The Bug That Almost Broke Everything

Our first version of the script had a critical chicken-and-egg bug:

1. Script runs: npm install -g pnpm
2. Script replaces /usr/bin/npm with wrapper that calls pnpm
3. Deployment FAILS (some other reason)
4. Next deployment tries again...
5. npm is now broken! It tries to call pnpm, but pnpm doesn't exist!
6. "pnpm: command not found" — forever
Enter fullscreen mode Exit fullscreen mode

The wrapper was created before verifying pnpm was properly installed. If anything failed between installation and wrapper creation, npm would be permanently broken on the instance — it would try to call pnpm (which doesn't exist), and since npm is broken, you can't even npm install -g pnpm to fix it.

This is why the script must:

  1. Always restore the original npm first (safety net)
  2. Verify pnpm is installed before creating the wrapper
  3. Only then replace npm with the wrapper

The Platform Hook Script

Create .platform/hooks/prebuild/01_install_pnpm.sh:

#!/usr/bin/env bash
set -e

echo "=== Starting pnpm installation via Corepack ==="

# ─── SAFETY NET ─────────────────────────────────────
# If a previous deployment failed mid-script, the original
# npm binary might be stuck at /usr/bin/npm_original.
# Restore it before we do anything else.
if [ -f /usr/bin/npm_original ]; then
    echo "Restoring original npm from backup..."
    mv /usr/bin/npm_original /usr/bin/npm
fi

# ─── INSTALL COREPACK ───────────────────────────────
# Amazon Linux 2023 does NOT include corepack unlike
# standard nodejs.org Node.js distributions.
# We must bootstrap it using npm first.
echo "Installing corepack..."
npm install -g corepack

# ─── ENABLE PNPM VIA COREPACK ──────────────────────
# 'corepack enable' creates a shim (lightweight proxy) at /usr/bin/pnpm
# It doesn't download pnpm yet — just creates the pointer.
# 'corepack prepare' actually downloads the binary and caches it,
# preventing the "Do you want to download?" prompt during automated builds.
echo "Enabling corepack and installing pnpm..."
corepack enable
corepack prepare pnpm@latest --activate

# ─── VERIFY INSTALLATION ───────────────────────────
if ! command -v pnpm &> /dev/null; then
    echo "ERROR: pnpm installation failed!"
    exit 1
fi
echo "pnpm installed successfully: $(pnpm --version)"

# ─── THE NPM WRAPPER ───────────────────────────────
# Only AFTER pnpm is verified, it's safe to create the wrapper.
# We backup the real npm binary, then replace it with a script
# that intercepts install/ci commands and redirects to pnpm.
echo "Creating npm wrapper..."
mv /usr/bin/npm /usr/bin/npm_original

cat > /usr/bin/npm << 'EOF'
#!/bin/bash

# Loop through ALL arguments — not just $1.
# Beanstalk runs: npm --omit=dev install
# If you only check $1, you'd see --omit=dev and miss 'install' entirely.
for arg in "$@"; do
    case "$arg" in
        install|ci|add)
            echo "[npm-wrapper] Intercepted install → using pnpm"
            pnpm install --prod --frozen-lockfile --ignore-scripts
            exit $?
            ;;
        rebuild|update|prune|dedupe)
            echo "[npm-wrapper] Intercepted $arg → skipping (pnpm handles this)"
            exit 0
            ;;
    esac
done

# Everything else (--version, run, start, etc) → use original npm
/usr/bin/npm_original "$@"
EOF

chmod +x /usr/bin/npm

echo "=== pnpm installation complete ==="
Enter fullscreen mode Exit fullscreen mode

Important: The file MUST have Linux line endings (LF, not CRLF). If you're on Windows, configure your editor or use git config core.autocrlf input.


Make It Executable

This is the step everyone forgets. The hook script must be executable, or Beanstalk silently ignores it:

# If you're on macOS/Linux
chmod +x .platform/hooks/prebuild/01_install_pnpm.sh

# If you're on Windows, use Git to track the permission
git update-index --chmod=+x .platform/hooks/prebuild/01_install_pnpm.sh
Enter fullscreen mode Exit fullscreen mode

Understanding the Wrapper: The $1 Bug

Our original wrapper only checked $1:

# BROKEN — only checks the first argument
if [ "$1" = "install" ] || [ "$1" = "ci" ]; then
    pnpm install --prod --frozen-lockfile --ignore-scripts
else
    /usr/bin/npm_original "$@"
fi
Enter fullscreen mode Exit fullscreen mode

This broke because Beanstalk runs:

npm --omit=dev install
Enter fullscreen mode Exit fullscreen mode

Here, $1 is --omit=dev, not install. The wrapper missed it entirely and fell through to the original npm, which failed because there's no package-lock.json.

The fix was to loop through ALL arguments using for arg in "$@" and use a case statement:

for arg in "$@"; do
    case "$arg" in
        install|ci|add)
            pnpm install --prod --frozen-lockfile --ignore-scripts
            exit $?
            ;;
        rebuild|update|prune|dedupe)
            exit 0  # Skip — pnpm handles it
            ;;
    esac
done
Enter fullscreen mode Exit fullscreen mode

Why intercept rebuild, update, prune, and dedupe?

Beanstalk runs npm rebuild and sometimes npm prune after install. With pnpm, these are unnecessary — pnpm handles everything during install. We skip them with exit 0 so Beanstalk doesn't interpret it as a failure.

Why --prod --frozen-lockfile --ignore-scripts?

Flag Why
--prod Only install production dependencies (skip devDependencies)
--frozen-lockfile Fail if lockfile is out of sync — prevents "works on my machine" bugs
--ignore-scripts Skip postinstall scripts during deploy for security and speed

Understanding Corepack Shims

When you run corepack enable, it doesn't install the real pnpm binary. It creates a shim — a lightweight proxy script at /usr/bin/pnpm that intercepts your command.

Running corepack prepare pnpm@latest --activate actually pre-downloads the real binary into the system cache. Without this step, the shim would prompt:

! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-10.28.2.tgz
? Do you want to continue? [Y/n]
Enter fullscreen mode Exit fullscreen mode

This prompt would hang an automated deployment. The --activate flag ensures it downloads silently.

Note: If you SSH into the instance as a different user (e.g., ec2-user), you may still see this prompt because the user-specific cache is empty. The web application runs fine because the deployment user (root/webapp) already had pnpm "prepared" by the script.


Deploy and Verify

Push your code and watch the Beanstalk logs. Here's what a successful deployment looks like (from our actual eb-hooks.log):

=== Starting pnpm installation via Corepack ===
Restoring original npm from backup...
Installing corepack...
changed 1 package in 1s
Enabling corepack and installing pnpm...
Preparing pnpm@latest for immediate activation...
pnpm installed successfully: 10.28.2
Creating npm wrapper...
=== pnpm installation complete ===
Enter fullscreen mode Exit fullscreen mode

Performance Results (February 2, 2026)

These are actual numbers from our production Elastic Beanstalk environment after fixing the npm wrapper bug:

Metric Before (npm) After (pnpm) Improvement
Total Deploy Stage ~6 minutes 1 min 6 sec 5.5x faster
Package Install ~3 minutes 13 seconds 14x faster
npm rebuild ~5 seconds Skipped N/A

Troubleshooting

Key Log Files

Log File Purpose
/var/log/eb-hooks.log Prebuild script output (your script logs here)
/var/log/eb-engine.log Deployment steps, artifact downloads
/var/log/web.stdout.log Application runtime output/errors
# View last 100 lines of each log
sudo tail -100 /var/log/eb-hooks.log
sudo tail -100 /var/log/eb-engine.log
sudo tail -100 /var/log/web.stdout.log
Enter fullscreen mode Exit fullscreen mode

Diagnosing the npm Wrapper State

# Check if npm is original or wrapper
cat /usr/bin/npm

# If it shows the wrapper script, npm has been replaced
# If it shows the original npm content (long script), it's untouched

# Check if backup exists
ls -la /usr/bin/npm_original

# Check if pnpm exists
which pnpm
pnpm --version
Enter fullscreen mode Exit fullscreen mode

Fixing Broken State Manually

If npm is broken on the instance (wrapper exists but pnpm doesn't):

# Restore the original npm
sudo mv /usr/bin/npm_original /usr/bin/npm

# Then install pnpm manually
sudo npm install -g pnpm

# Verify
pnpm --version
Enter fullscreen mode Exit fullscreen mode

Running the App Manually via SSH

SSH sessions don't have Elastic Beanstalk environment variables. Load them first:

sudo su -
source /opt/elasticbeanstalk/deployment/env
cd /var/app/current
pnpm start
Enter fullscreen mode Exit fullscreen mode

Common Errors & Solutions

Error Cause Solution
corepack: command not found Amazon Linux doesn't include corepack Use npm install -g corepack first
pnpm: command not found (inside npm) npm wrapper created before pnpm installed Restore npm_original, then reinstall
App crashes with missing env var SSH session doesn't have EB env vars Use source /opt/elasticbeanstalk/deployment/env
Rollback loop Old broken version keeps deploying Disable auto-rollback in CodePipeline (set to "None")

TL;DR

  1. AWS Elastic Beanstalk hardcodes npm install — you can't change it
  2. Amazon Linux 2023 doesn't include corepack — you must bootstrap it with npm install -g corepack
  3. Create a .platform/hooks/prebuild/ hook that installs pnpm and replaces npm with a wrapper
  4. The wrapper must loop through ALL arguments (not just $1) because Beanstalk runs npm --omit=dev install
  5. Always restore the original npm first to avoid the chicken-and-egg bug
  6. Result: package install dropped from ~3 minutes to 13 seconds

Should AWS Just Support pnpm Natively?

Yes. But until that day comes, this wrapper approach is production-tested and running across multiple services without issues.

If you found this useful, feel free to share or leave a comment.


This approach has been running in production since December 2025 across multiple Node.js services on Elastic Beanstalk (Amazon Linux 2023, Node.js 22.x platform).

Top comments (2)

Collapse
 
trinhcuong-ast profile image
Kai Alder

The $1 bug is such a classic gotcha. I hit something almost identical with a Docker entrypoint wrapper once — spent way too long wondering why flags were eating my subcommand detection.

That chicken-and-egg problem with the npm backup is really well documented too. The safety net of restoring npm_original first is the kind of thing you only learn after a painful debugging session on a broken instance, haha.

One question — have you considered just containerizing the app and using ECS instead? I know it's a bigger lift, but it removes the whole "platform hardcodes npm" problem entirely. Or is EB working well enough now that it's not worth the migration?

Collapse
 
tirtht profile image
Tirth T

Firstly , thank you for your appreciation and definitely agree with you on painful debugging xD. Regarding your question , yes it is too big of a lift right now to migrate to ECS since we have 5 beanstalks running our entire production architecture all connected via shared load balancer , ACM Verified etc. Migrating our entire codebase from javascript to typescript itself was a very huge migration and required god knows how many permissions and approvals from upper management. So this is something we could look in future but definitely not now. And yes EB is working very well enough right now that we could probably delay this containerization until it becomes like a priority necessity.