Why CI/CD?
Continuous Integration and Continuous Deployment (CI/CD) fundamentally alters how teams ship code. Manual deployments (e.g., using FileZilla, Cyberduck, or manual scp/rsync scripts directly from a developer's machine) introduce immense risk.
By utilizing GitHub Actions, you remove the human element from deploying code. You ensure that every piece of software pushed to your main branch is predictably built, tested, and shipped exactly the same way, every single time.
Understanding GitHub Actions
GitHub Actions relies on Workflows. A workflow is an automated procedure added to your repository defined via a YAML file inside the .github/workflows/ directory.
A typical workflow consists of:
- Events (
on): Triggers that start the workflow (e.g., pushing code, opening a PR, scheduled runs). - Jobs: A set of steps executed on the same runner (a virtual machine).
- Steps: Individual tasks that either run a shell command or an Action (pre-built executable scripts provided by the community).
Basic Production Deployment Pipeline
We'll start by building a pipeline that compiles a static site (using Nuxt, Next.js, Eleventy, or Astro) and ships it securely via Rsync to your Linux server (Ubuntu/Debian).
Create a file named .github/workflows/deploy.yml:
name: Deploy Production Site
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# Step 1: Clone the repository to the GitHub Actions runner
- name: Checkout Code
uses: actions/checkout@v4
# Step 2: Setup Node Environment
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Implicitly caches node_modules
# Step 3: Install dependencies exactly as dictated by package-lock.json
- name: Install Dependencies
run: npm ci
# Step 4: Run the build script
- name: Build Site
run: npm run build
# Step 5: Transfer the built files to your secure server
- name: Deploy to Server via Rsync
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
ARGS: "-rlgoDzvc -i --delete"
SOURCE: "dist/"
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
REMOTE_USER: ${{ secrets.SERVER_USER }}
TARGET: "/var/www/html/mysite/"
Required GitHub Secrets
Before this workflow succeeds, securely store the following in your repository (Settings > Secrets and variables > Actions):
SERVER_HOST: The IP or DNS of your target server (e.g.,203.0.113.50).SERVER_USER: The SSH username (usuallyubuntu,debian, orroot).SERVER_SSH_KEY: The private key corresponding to the public key installed in~/.ssh/authorized_keyson your target server.
Advanced: Caching and Environments
For enterprise architectures, compiling code and transferring files is just the beginning.
Multi-Environment Pipelines (Staging vs Prod)
You shouldn't push directly to production. Instead, create pipelines that deploy to a Staging server when code hits the develop branch, and to Production when code is merged into main.
name: Continuous Deployment
on:
push:
branches:
- main
- develop
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build Application
run: |
npm ci
npm run build
env:
# This injects environment-specific variables defined in GitHub Environments
API_ENDPOINT: ${{ vars.API_ENDPOINT }}
- name: Execute Remote SSH Commands
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /var/www/${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
git pull origin ${{ github.ref_name }}
npm ci
pm2 restart app
Implementing Dependency Caching
If your application takes 10 minutes to build and download packages, you are wasting GitHub Actions minutes and greatly reducing iteration speed. Utilize actions/cache:
- name: Cache Node Modules
id: cache-node-modules
uses: actions/cache@v3
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
node-modules-
- name: Install dependencies (if not cached)
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: npm ci
Note: In newer versions of setup-node, providing cache: 'npm' does this automatically, but explicitly using actions/cache gives you granular control over exactly what folders get cached (like .next cache folders).
Troubleshooting Tips
- Permission Denied (publickey): Ensure the
SERVER_SSH_KEYincludes the-----BEGIN OPENSSH PRIVATE KEY-----wrapper. Additionally, log into your server and runcat ~/.ssh/authorized_keysto ensure the public counterpart is correctly saved on a single line. - Rsync Pathing Issues: Notice that
SOURCEparameter (SOURCE: "dist/") in the rsync action. A trailing slash indicates you want the contents of thedistfolder. Without the trailing slash, you will transfer thedistfolder itself into the target directory, resulting in/var/www/html/mysite/dist/index.htmlwhich breaks your web server configuration. - npm install vs npm ci: Always use
npm ciin CI/CD pipelines. It is strictly faster and installs exactly what is in yourpackage-lock.json, throwing an error if the package file and log file are out of sync.npm installsilently resolves versions and edits the lockfile.
Final Thoughts
Adopting GitHub Actions requires minimal setup, but the return on investment regarding application stability is huge. By ensuring tests are passed and processes are repeatable, your team can merge pull requests with confidence.
OceanSoft Solutions creates tailored DevOps and automation architectures for web platforms. Need help configuring a robust enterprise pipeline? Reach out to our consulting team.