npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

portway-cpanel

v1.1.0

Published

Deploy local or GitHub projects to your own cPanel/SSH host — declarative config, incremental uploads, safe dry-runs.

Downloads

1

Readme

Portway

Deploy a local or GitHub-hosted project to your own cPanel / SSH host — as simply as:

portway deploy

Portway is not a hosting platform and not a Vercel-style PaaS. You bring your own cPanel account, server, domain, and credentials. Portway is only the deployment bridge between your code and your host:

Local Project ──────┐
                     ├────> portway CLI ────> Your cPanel host
GitHub Repository ───┘   (SFTP, FTP, or GitHub — server clones/builds itself)

Installation

npm install -g portway

The CLI is installed under three interchangeable command names — use whichever you like, they all run the exact same binary:

portway deploy
pway deploy             # short alias
portway-cpanel deploy   # explicit alias, if "portway" collides with
                        # something else already on your PATH

Quick start

cd your-project
portway init         # answer a few questions, creates Portwayfile + .portwayignore
portway dry-run       # see what WOULD be uploaded, with zero server writes
portway deploy        # actually deploy

portway init walks you through an interactive setup and writes a fully-commented Portwayfile explaining what each directive does, based on your answers (local vs. GitHub source, SFTP vs. FTP, SSH key vs. password, etc.).

Credentials: .env interpolation

Any value in the Portwayfile can reference an environment variable with ${VAR_NAME} — most commonly for PASSWORD, but it works for any field:

HOST example.com
USER my-ftp-user
PASSWORD ${CPANEL_FTP_PASSWORD}

Portway resolves ${CPANEL_FTP_PASSWORD} from, in order of precedence:

  1. A real environment variable (e.g. a CI secret) — wins if set.
  2. A .env file in the project root.

.env is never uploaded and should stay out of git via .gitignore. Inline secrets in the Portwayfile itself (a literal password or passphrase instead of ${VAR}) are rejected — this is enforced, not just a convention. If a referenced variable isn't defined anywhere, portway deploy/dry-run fails immediately with a clear error naming the missing variable, rather than silently sending a broken value.

portway init sets all of this up for you automatically if you choose password auth over an SSH key, or if your SSH key has a passphrase (PASSPHRASE) — it writes the placeholder to the Portwayfile and the real value to .env for you.

Example Portwayfile (SFTP)

NAME my-vite-app

SOURCE local

RUN npm run build
OUTPUT dist

HOST example.com
PORT 22
USER my-cpanel-user
KEY ~/.ssh/id_ed25519

TARGET public_html

CLEAN false

IGNORE *.map
IGNORE *.log

Example Portwayfile (FTP)

NAME my-vite-app

SOURCE local

RUN npm run build
OUTPUT dist

HOST ftp.example.com
PORT 21
USER my-ftp-user
PASSWORD ${CPANEL_FTP_PASSWORD}
TRANSPORT ftp
SECURE true

TARGET public_html

GitHub source mode

SOURCE github is fundamentally different from SOURCE local: your laptop never uploads the project. Instead, Portway SSHes into your server and tells it to clone the repo and build itself there:

GitHub ──clone/fetch──> Your cPanel server ──build──> TARGET
                              ▲
                   portway CLI only triggers this,
                     over SSH, from your machine

This is the recommended way to handle large repositories — nothing is ever zipped up and pushed from your laptop.

NAME my-app
SOURCE github
REPO your-username/your-repo
BRANCH main

RUN npm ci && npm run build
OUTPUT dist

HOST example.com
USER my-cpanel-user
KEY ~/.ssh/id_ed25519

TARGET public_html

Authentication is two separate things, and Portway only handles one of them:

  1. Portway → your server: the usual KEY/PASSWORD in the Portwayfile, same as local/FTP deployments.
  2. Your server → GitHub: an SSH deploy key registered directly on GitHub (repo Settings → Deploy keys) and present in the server's ~/.ssh, not your laptop's. This is entirely outside Portway's knowledge, so a private repo works exactly like a public one from Portway's point of view. If REPO is owner/repo, Portway builds the SSH clone URL ([email protected]:owner/repo.git) automatically; a full URL (https://... or git@...) is also accepted as-is.

How a deploy actually works, step by step:

  1. SSH into HOST as USER.
  2. If a previous clone exists on the server: git fetch + hard reset to origin/BRANCH (fast, incremental). Otherwise: fresh git clone --branch BRANCH.
  3. If RUN/BUILD steps are set, they run inside the clone, on the server.
  4. The deployable output (OUTPUT, or the repo root if no build step) is copied into a staging directory — excluding .git, node_modules, .env, and your IGNORE patterns, same as local mode.
  5. dry-run stops here and diffs the staged build against TARGET.
  6. deploy additionally does an atomic swap: moves the current TARGET aside, moves the new staged build into place, then removes the old one.

Because the whole thing is an atomic swap, TARGET always ends up an exact mirror of the latest build (except for anything matched by PRESERVE — see below).

RUN and COPY

Dockerfile-style build steps. RUN <command> is repeatable and runs in order — locally for SOURCE local, on the server (inside the freshly cloned repo) for SOURCE github:

RUN npm install
RUN npm run build
OUTPUT dist

The first command that fails stops the rest — a partially-succeeded build is never scanned or deployed.

COPY <from> <to> copies a file or directory from the project/repo root into the deploy output, after RUN steps finish and before anything gets scanned for deployment:

RUN npm run build
OUTPUT dist

COPY package.json package.json
COPY prisma prisma

The main reason you'd need this: a compiled dist/ typically doesn't include package.json (or a prisma/ schema directory, etc.) — so if you're running npm install or npx prisma migrate deploy on the server afterward via CMD (below), there's nothing there for it to work with unless you COPY it in explicitly. from is relative to the project/repo root; to is relative to the deploy output — both directories and single files work, and missing destination folders are created automatically.

Minification and concurrency

For local deployments, you can optionally minify every JavaScript file in OUTPUT after the build finishes and before Portway compares or uploads the files:

RUN npm run build
OUTPUT dist
MINIFY true

Minification reduces JavaScript size, which can make pages and applications load faster and reduces the amount of data sent to your server. It uses Terser to compress and mangle JavaScript. Only files inside OUTPUT are changed; your source files are left untouched.

Portway processes files concurrently to make deployments faster. By default, it uses up to 6 concurrent files, adjusted down to the number of CPU cores available on the computer running Portway. This setting applies to both optional minification and parallel uploads:

CONCURRENCY 4

CONCURRENCY is optional. Use a smaller value, such as 1, on a computer or server with limited resources or strict connection limits. Use a larger value when your host and computer can handle more parallel work; Portway accepts values up to 16. BUNDLE true remains a single archive upload and does not use per-file concurrency.

CMD

Run one or more commands on the server, right after a deploy that actually changed something — the classic case is running database migrations or npm install immediately after pushing new code, without a separate manual SSH step:

CMD npx prisma migrate deploy
CMD pm2 restart api

Multiple CMD lines run in order; the first one that fails stops the rest and marks the whole deploy as failed — but the files themselves are left deployed as-is. Commands run with the server's working directory set to TARGET, so a command like npm install acts on the app you just deployed.

Requires SSH exec, so it only works with TRANSPORT sftp or SOURCE github — plain FTP has no way to run a remote command, and CMD with TRANSPORT ftp is rejected at config-load time with a clear error.

  • dry-run lists what would run, without executing anything.
  • CMD only runs when the deploy actually changed something — a genuine no-op redeploy skips it entirely, same as it skips the file transfer.

Important gotcha for SOURCE github: anything a CMD command writes into TARGET — most commonly npm install's node_modules/ — isn't part of your git repo. Without PRESERVE, the next deploy's atomic swap will wipe it out again, forcing a full reinstall on every single deploy. Add it to PRESERVE:

CMD npm install --omit=dev
PRESERVE node_modules

Putting it all together — a compiled backend that needs its dependencies installed and migrations run on the server:

RUN npm install
RUN npm run build
OUTPUT dist

COPY package.json package.json
COPY prisma prisma

TARGET api

CMD npm install --omit=dev
CMD npx prisma migrate deploy
CMD pm2 restart api

INIT

If CMD npm install fails with something like:

✗ CMD command failed (exit code 127): npm install
  bash: npm: command not found

...that's a very common shared-hosting quirk, not a Portway bug. SSH command execution is non-interactive, and non-interactive shells often don't source the same profile scripts an interactive login shell does. Node/npm installed via cPanel's Node.js Selector typically needs its environment "activated" before node/npm land on PATH at all.

INIT fixes this by running setup commands before every CMD command and every RUN step (for SOURCE github):

INIT source /home/user/nodevenv/appname/18/bin/activate
CMD npm install
CMD npx prisma migrate deploy

Multiple INIT lines are joined with && and prepended fresh to each command — you never need to repeat the setup line yourself on every CMD/RUN line; Portway does that for you automatically.

Finding the right INIT command for your host: SSH into the server manually, do whatever you'd normally do to get node/npm working interactively, then run which npm to see the real path. If your host uses cPanel's Node.js Selector, the "Setup Node.js App" page shows an exact "Enter to the virtual environment" command you can use directly as your INIT line. If npm is just installed somewhere directly on disk without any activation script, a plain PATH export works too:

INIT export PATH="$PATH:/opt/cpanel/ea-nodejs18/bin"

BUNDLE mode

For SFTP deploys with many small files, uploading them one at a time means one round trip per file — slow, especially over a high-latency connection. BUNDLE true instead zips every new/changed file locally into a single archive, uploads just that one file, then extracts it on the server:

TRANSPORT sftp
BUNDLE true

A few things worth knowing:

  • SFTP only — extracting the zip requires running a command on the server, which plain FTP can't do. BUNDLE true with TRANSPORT ftp is rejected at config-load time.
  • Requires unzip on the server — virtually universal on cPanel/Linux hosting.
  • All-or-nothing — a bundle either extracts completely or the whole batch is treated as failed, and the next deploy retries everything cleanly.
  • Deletions (files removed locally) still happen individually.
  • Doesn't apply to SOURCE github, which already builds and deploys entirely on the server.

PRESERVE

Some files only make sense living on the server — a cPanel-managed .htaccess, an SSL certificate validation path under .well-known/, an uploads/ directory your app writes to at runtime. PRESERVE tells Portway to leave matching paths alone entirely:

PRESERVE .htaccess
PRESERVE .well-known

Patterns support glob wildcards (*.log, uploads/*), same as IGNORE.

What this does depends on the deployment mode:

  • Local/SFTP/FTP: matching paths are excluded from deletion. Portway only ever deletes files it previously uploaded itself, but PRESERVE makes the guarantee explicit.
  • SOURCE github: this is where PRESERVE matters most. Since a GitHub-source deploy atomically replaces the entire TARGET directory, anything server-only sitting in TARGET would otherwise be permanently lost the moment the swap happens. With PRESERVE set, Portway copies matching files out of the current TARGET into the new build before swapping, so they carry over intact.

Progress and concurrent uploads

By default, Portway uploads up to 4 files at once over separate connections instead of strictly one after another — a real speedup for projects with many small files. Works for both TRANSPORT sftp and TRANSPORT ftp. Tune it with:

CONCURRENCY 6

Set CONCURRENCY 1 to go back to fully sequential uploads — useful if your host caps how many simultaneous connections one account can open. This comes up more often with TRANSPORT ftp than sftp in practice: some FTP daemons on shared hosting cap concurrent connections per account quite low (2-4 isn't unusual), and you'll see connection-refused errors if you push past that. If you're not sure, start at the default and lower it only if you hit errors.

Progress display adapts to your terminal and the size of the deploy:

  • Interactive terminal, more than ~15 files: a single live-updating progress bar instead of one line per file.
  • Interactive terminal, fewer files: the classic per-file +/↑ lines.
  • Piped output / CI logs: always per-file lines.

dry-run similarly caps each category (added/modified/deleted/unchanged) at 25 listed files, summarizing the rest as ... and N more. This doesn't apply to BUNDLE mode (already a single transfer) or SOURCE github (no per-file upload step to begin with).

Ignoring files

.portwayignore (checked into your project) plus inline IGNORE rules in the Portwayfile control what never gets deployed:

IGNORE *.map
IGNORE *.log

node_modules, .git, and .env are excluded by default even without any rules of your own.

Incremental deployment

For SOURCE local deploys, Portway records a SHA-256 hash of every uploaded file. The next deploy only uploads new or changed files, and safely deletes files that were removed locally (as long as they were previously managed by Portway) — set CLEAN false if you'd rather it never delete anything on the server automatically.

Testing before you deploy for real

Always run portway dry-run before your first portway deploy against a new host — it shows exactly what would be uploaded, changed, and deleted with zero writes to your server, so you can catch a misconfigured TARGET or an overly broad IGNORE/PRESERVE pattern before it touches production.