CI/CD for Bare Metal: GitHub Actions, GitLab CI, and RAW CLI
The biggest misconception about bare metal servers is that you lose the “git push to deploy” workflow. You don’t. With a CI/CD pipeline and SSH deploy keys, pushing to main triggers a build, deploys to your server, runs health checks, and rolls back automatically if something breaks — all in under 60 seconds.
Prerequisites
- A RAW bare metal server — any plan works. Deploy with
npx rawhq deploy - SSH key pair — for passwordless deployment from CI to your server
- Your app — Node.js, Python, Go, or any stack that runs on Linux
Step 1: SSH Deploy Keys
Generate a dedicated key pair for CI deployments. Never reuse your personal SSH key.
# Generate a deploy key (no passphrase)
ssh-keygen -t ed25519 -C "ci-deploy" -f deploy_key -N ""
# Copy public key to your RAW server
ssh-copy-id -i deploy_key.pub root@your-server-ip
# Test the connection
ssh -i deploy_key root@your-server-ip "echo connected"Add the private key as a secret in your CI system. In GitHub: Settings → Secrets → DEPLOY_SSH_KEY. In GitLab: Settings → CI/CD → Variables → DEPLOY_SSH_KEY (type: File).
Step 2: Server-Side Deploy Script
Create a deploy script on your server that handles the full deployment cycle:
# /opt/app/deploy.sh
#!/bin/bash
set -euo pipefail
APP_DIR="/opt/app"
REPO="https://github.com/you/your-app.git"
BRANCH="main"
cd "$APP_DIR"
# Pull latest code
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
# Install dependencies and build
npm ci --production
npm run build 2>/dev/null || true
# Restart with zero downtime
pm2 reload all --update-env
# Health check
sleep 3
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/health)
if [ "$HTTP_STATUS" != "200" ]; then
echo "Health check failed with status $HTTP_STATUS"
exit 1
fi
echo "Deploy successful"chmod +x /opt/app/deploy.shGitHub Actions Workflow
Create .github/workflows/deploy.yml in your repository:
# .github/workflows/deploy.yml
name: Deploy to Bare Metal
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: your-server-ip
username: root
key: $SECRETS_DEPLOY_SSH_KEY
script: /opt/app/deploy.shThat is the entire workflow. Push to main, GitHub Actions SSHs into your server and runs the deploy script. Typical execution time: 15–30 seconds.
Adding Tests Before Deploy
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: your-server-ip
username: root
key: $SECRETS_DEPLOY_SSH_KEY
script: /opt/app/deploy.shGitLab CI Workflow
Create .gitlab-ci.yml in your repository root:
# .gitlab-ci.yml
stages:
- test
- deploy
test:
stage: test
image: node:22
script:
- npm ci
- npm test
deploy:
stage: deploy
only:
- main
before_script:
- apt-get update -qq && apt-get install -y -qq openssh-client
- eval $(ssh-agent -s)
- cat "$DEPLOY_SSH_KEY" | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan your-server-ip >> ~/.ssh/known_hosts
script:
- ssh root@your-server-ip "/opt/app/deploy.sh"RAW CLI Deployments
The RAW CLI integrates directly into any CI system. Use it to provision servers, manage infrastructure, and trigger deploys:
# Install RAW CLI in your CI pipeline
npm install -g rawhq
# Deploy to an existing server
raw deploy --server my-app
# Or use in GitHub Actions
- name: Deploy with RAW CLI
env:
RAW_API_KEY: $SECRETS_RAW_API_KEY
run: |
npx rawhq deploy --server my-appZero-Downtime Deploys
The pm2 reload command in the deploy script performs a rolling restart — new workers start before old ones stop. No dropped connections. For even more safety, keep the previous release available for instant rollback:
# /opt/app/rollback.sh
#!/bin/bash
set -euo pipefail
cd /opt/app
git reset --hard HEAD~1
npm ci --production
npm run build 2>/dev/null || true
pm2 reload all --update-env
sleep 3
curl -sf http://127.0.0.1:3000/health || { echo "Rollback failed"; exit 1; }
echo "Rolled back successfully"Trigger with ssh root@server /opt/app/rollback.sh or add a GitHub Actions workflow dispatch trigger for one-click rollbacks from the UI.
Health Checks
Your app needs a health endpoint. This is non-negotiable for automated deploys:
// Express / Fastify example
app.get('/health', (req, res) => {
// Check DB connection, Redis, etc.
res.status(200).json({ status: 'ok' })
})The deploy script checks this endpoint after restart. If it returns anything other than 200, the deploy fails and triggers a rollback. No manual intervention needed.
CI/CD Comparison
Security Best Practices
- Dedicated deploy keys — never reuse personal SSH keys in CI
- Restrict deploy key permissions — use a non-root user with sudo only for service restarts
- Rotate keys quarterly — update CI secrets and server authorized_keys
- Pin action versions — use
appleboy/ssh-action@v1not@master - Audit deploy logs — store CI output and server deploy logs for post-mortems
Deploy to RAW
npx rawhq deploy7-day free trial. 13 seconds to deploy. Set up CI/CD once and never think about deployments again.