GitHub Actions pipeline that builds, tests, and deploys a Node.js app to AWS EC2 with PM2 reload, rollback support, and scheduled health checks.
Push to main / feature branch
│
└── ci.yml
└── build-and-test: Node 22/24 matrix → npm ci → Jest tests → ESLint gate
│
└── release.yml (manual dispatch or pushed v* tag)
├── deploy: tar.gz → SCP to EC2 → deploy.sh (PM2 reload, atomic symlink swap)
└── create-release: GitHub Release for tagged deploys
| Decision | Implementation | Why |
|---|---|---|
| Low-interruption deploy | pm2 reload + atomic symlink swap (current -> release-timestamp) |
Keeps deploy behavior predictable and rollback-friendly |
| Auto-rollback | deploy.sh keeps the previous current target until the new release passes health checks |
Restores the last known-good symlink if deploy breaks the app |
| Controlled release trigger | release.yml runs from a manual dispatch or pushed v* tag |
Avoids accidental EC2 deploys from routine CI commits |
| Health check monitoring | Scheduled workflow runs hourly and reuses one open health-check issue while an outage is active | Avoids duplicate alert noise and keeps incident state readable |
| Separate CI / CD workflows | ci.yml validates code; release.yml deploys from manual or tag triggers |
Keeps routine validation separate from EC2 deployment |
Triggers: push to main, development, feature/* branches + all PRs
build-and-test: Node 22/24 matrix →npm ci→ Jest test suite → ESLint gate
Triggers: manual dispatch or new tag matching v*
deploy: packages build → SCP to EC2 → runs/var/www/app/scripts/deploy.sh- Installs dependencies in release dir → atomic symlink
current→pm2 reload - On failure: restores previous symlink →
pm2 reload(auto-rollback)
- Installs dependencies in release dir → atomic symlink
create-release: publishes GitHub Release with tag name
Runs hourly. Hits /api/health for configured environments. If a check fails, the workflow creates one health-check issue or comments on the existing open issue instead of creating duplicates.
| Secret | Purpose |
|---|---|
PROD_EC2_HOST |
Production EC2 hostname or IP |
PROD_EC2_USER |
SSH username |
PROD_EC2_KEY |
Private SSH key (PEM format) |
Also set: Actions → General → Workflow permissions → Read and write (allows built-in token to create releases and issues).
Node.js 22/24 · Express · Jest · PM2 · GitHub Actions · AWS EC2
The EC2 host should run Node.js 22 or 24 so the deployed runtime matches CI.
git clone https://github.com/darestack/github-actions-ec2-pipeline.git
cd github-actions-ec2-pipeline
npm install
npm test
npm start
# → http://localhost:3000
# → http://localhost:3000/api/healthTo add Nginx as a reverse proxy on the EC2 instance (port 80 → 3000):
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}