The orchestration of modern web applications requires a seamless transition from a version-controlled repository to a live production environment. Achieving this "zero-touch" deployment is primarily accomplished by integrating PM2, a sophisticated production process manager, with GitHub Actions, a powerful CI/CD platform. PM2 serves as the bedrock for application stability, ensuring that Node.js or Bun applications remain operational indefinitely through a built-in load balancer and automatic restart capabilities. When paired with GitHub Actions, the manual overhead of SSHing into servers and running deployment scripts is eliminated, replaced by a trigger-based system where a code push to a specific branch initiates an automated sequence of code transfer, build execution, and process reloading.
The Architectural Foundation of PM2
PM2 is designed as a comprehensive production process manager specifically tailored for Node.js and Bun applications. Its primary utility lies in its ability to daemonize applications, meaning the process runs in the background independent of the terminal session, and monitors them to ensure they are kept alive forever. If an application crashes, PM2 automatically restarts it, minimizing downtime.
Beyond simple process management, PM2 incorporates a built-in load balancer via its cluster mode. This mode allows the application to spawn multiple processes to utilize multi-core CPUs, distributing HTTP, TCP, or UDP queries across these instances to maximize throughput and performance.
The software maintains broad compatibility across operating systems, functioning seamlessly on Linux, macOS, and Windows. It supports Node.js version 18 and above, as well as Bun version 1 and above. For environments where only Bun is installed, a specific compatibility step is required to ensure the PM2 shebang resolves correctly. This is achieved by symlinking the node command to the bun binary using the following command:
sudo ln -s $(which bun) /usr/local/bin/node
Installation of PM2 is performed globally via the following commands:
npm install pm2 -g
bun install pm2 -g
PM2 Application Management and Control
Once an application is initiated using the command pm2 start app.js, the operator can utilize a suite of management commands to control the application lifecycle.
The following table outlines the primary PM2 management operations:
| Command | Purpose | Target Parameter |
|---|---|---|
pm2 list |
Display all running applications and their status | N/A |
pm2 stop |
Stop a specific application instance | <app_name|namespace|id|'all'|json_conf> |
pm2 restart |
Perform a hard stop and immediate start | <app_name|namespace|id|'all'|json_conf> |
pm2 delete |
Remove an application from the PM2 list | <app_name|namespace|id|'all'|json_conf> |
pm2 describe |
View detailed information about a specific process | <id|app_name> |
pm2 monit |
Open a real-time dashboard for logs and metrics | N/A |
A critical distinction exists between the restart and reload commands. A restart is considered a "hard" operation that stops and starts the web server, which can lead to momentary downtime. In contrast, a reload is a "soft" operation that allows for zero-downtime updates by restarting processes one by one.
Configuring Ecosystem Files for Multi-Environment Deploys
The ecosystem.config.js file is the central configuration hub for PM2. It allows developers to define environment variables, interpreters, Node.js arguments, the number of instances for cluster mode, and other critical operational parameters.
For professional setups, it is recommended to separate configurations by environment. This prevents the accidental deployment of staging configurations to production servers. This is typically achieved by creating separate files such as:
ecosystem.staging.config.jsecosystem.production.config.js
These files map the application's behavior to specific server environments. When deploying manually from a development machine, the specific config file and environment must be targeted using:
pm2 deploy ecosystem.production.config.js production
Establishing Secure SSH Connectivity for GitHub Actions
For GitHub Actions to interact with a remote VPS, such as a DigitalOcean droplet, a secure and authenticated SSH tunnel must be established. This process involves the exchange of public and private keys to avoid the use of insecure passwords during the CI/CD pipeline.
The initial step is the generation of a specialized SSH key pair on the local development machine. It is recommended to use the Ed25519 algorithm for superior security and performance:
ssh-keygen -t ed25519 -C "github_deploy_action"
During this process, the key should be saved to a specific file (e.g., ./github_action_key) and created without a passphrase to allow the automated action to execute without manual intervention.
Once the key is generated, the public key (github_action_key.pub) must be appended to the server's authorized keys list to grant access:
echo "ssh-rsa AAAA....YOUR_PUBLIC_KEY..." >> ~/.ssh/authorized_keys
To ensure the GitHub Action can authenticate, the private key's contents must be stored as a GitHub Repository Secret named SSH_PRIVATE_KEY. This is accessed via Repo Settings > Secrets > Actions > New Repository Secret.
To prevent "Host key verification failed" errors, which occur when the remote server is not recognized by the GitHub runner, the server's known hosts entry must be captured. This is done using ssh-keyscan:
ssh-keyscan SERVER_IP > pbcopy
The output of this command should be stored in another GitHub Repository Secret named SSH_KNOWN_HOSTS.
Implementing the GitHub Actions Workflow
A typical deployment workflow is triggered by a push to a specific branch, such as main. The workflow is defined in a YAML file located at .github/workflows/main.yml.
The general logic flow of a PM2-based GitHub Action involves the following steps:
- Checkout the source code from the repository.
- Configure the SSH agent using the
SSH_PRIVATE_KEY. - Establish the known hosts using the
SSH_KNOWN_HOSTS. - Execute the PM2 deploy command.
For those utilizing a pre-built action such as victorargento/pm2-deployment@main, the configuration requires specific parameters to map the deployment to the remote server.
The parameters for the pm2-deployment action are as follows:
remote-path: The directory on the server where files will be copied (e.g.,/deployment/api).host: The public IP address of the server.username: The SSH login user, stored as${{ secrets.prod-user }}.port: The SSH port, which defaults to 22 but can be customized (e.g., 2080).password: The user password, stored as${{ secrets.prod-password }}.pm2-id: The specific ID or name of the PM2 application.
The automated sequence performed by this specific action includes copying repository contents to the remote folder, running npm ci for clean dependency installation, executing npm run build for TypeScript applications, and finally running pm2 reload or pm2 reset <id> to reset the restart counter to zero.
Manual Server Preparation and Initial Deployment
Before the automated pipeline can function, the server must be prepared to receive the application. This includes the initial setup of directories and environment variables.
The application directory must be created and must match the path defined in the ecosystem.config.js file:
mkdir /var/www
Environment variables should be configured on the server to ensure the application can connect to databases and external services. A common method is adding these exports to the .bashrc file:
export PGHOST=db.bit.io
export PGUSER=HIDDEN
export PGPASSWORD=HIDDEN
export PGPORT=5432
export PGDATABASE=HIDDEN
export PGSSL=yes
The initial deployment is performed from the local development environment to set up the application on the server:
pm2 deploy ecosystem.production.config.js production setup
pm2 deploy ecosystem.production.config.js production
Note for Windows users: If a spawn sh ENOENT error occurs during this process, it indicates that the shell (sh) is not in the system path and must be configured accordingly.
Implementing Reverse Proxy with Caddy
To expose the PM2-managed application to the internet via a domain name with automatic SSL, Caddy is used as a reverse proxy. Caddy simplifies the process by managing SSL certificates automatically.
First, DNS records must be configured to point the domain to the server's IP address. Then, the Caddyfile located at /etc/caddy/Caddyfile is configured to route traffic from the domain to the internal port where the PM2 application is running (e.g., port 3001).
Example Caddyfile configuration:
mpg2co2.com { reverse_proxy localhost:3001 }
www.mpg2co2.com { redir https://mpg2co2.com{uri} }
After updating the Caddyfile, the configuration must be reloaded to take effect:
sudo caddy reload
Detailed Analysis of Deployment Strategies
The integration of PM2 and GitHub Actions represents a shift from manual imperative deployments to declarative automated pipelines. By utilizing the ecosystem.config.js file, developers treat their process management as code, which ensures consistency across staging and production environments.
The use of npm ci instead of npm install within the GitHub Action is a critical best practice. npm ci ensures that the exact versions of dependencies specified in the lockfile are installed, preventing "it works on my machine" syndromes caused by drifting dependency versions.
Furthermore, the strategy of using pm2 reload over pm2 restart is essential for high-availability applications. In a cluster mode setup, reload allows the application to update its code without dropping active connections, as it replaces processes incrementally.
The security model implemented here—using Ed25519 keys and GitHub Secrets—ensures that sensitive credentials never enter the version control system. The use of SSH_KNOWN_HOSTS mitigates man-in-the-middle attacks by verifying the server's identity before the deployment begins.