Guides/Server Setup
Server Setup9 min read

How to Set Up GitHub Actions for CI/CD

GitHub Actions automates your test, build, and deploy pipeline on every push. This guide covers writing your first workflow, running tests on PRs, building Docker images, and deploying to a server.

Create Your First Workflow File

Create .github/workflows/ci.yml: name: CI — on: push: branches: [main] — pull_request: branches: [main] — jobs: test: runs-on: ubuntu-latest — steps: — uses: actions/checkout@v4 — uses: actions/setup-node@v4 with: node-version: 20 — run: npm ci — run: npm test. This runs your tests on every push to main and every PR. GitHub provides 2,000 free minutes per month on public repos.

Cache Dependencies for Faster Runs

Add a cache step before npm ci: — uses: actions/cache@v4 — with: path: ~/.npm — key: ${{ runner.os }}-node-${{ hashFiles("**/package-lock.json") }}. This caches node_modules between runs when package-lock.json has not changed. Reduces install time from 60-90 seconds to 5-10 seconds. The cache key includes a hash of package-lock.json so it auto-invalidates when dependencies change.

Build and Push a Docker Image

Add a build job: build: needs: test — runs-on: ubuntu-latest — steps: checkout, then: — uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} — uses: docker/build-push-action@v5 with: push: true tags: yourusername/myapp:${{ github.sha }},yourusername/myapp:latest. Add secrets in your GitHub repo Settings → Secrets.

Deploy to a Server via SSH

Add a deploy job: deploy: needs: build — runs-on: ubuntu-latest — steps: — uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ubuntu key: ${{ secrets.SSH_PRIVATE_KEY }} script: docker pull yourusername/myapp:latest && docker stop myapp || true && docker run -d --rm -p 3000:3000 --name myapp yourusername/myapp:latest. Store your SSH private key as a GitHub secret.

Run Only Relevant Jobs on PRs

Use conditional steps to avoid deploying on PRs: if: github.ref == "refs/heads/main" && github.event_name == "push". This ensures the deploy job only runs when a PR is merged to main, not on every PR push. Use required status checks in GitHub branch protection rules to block merging if tests fail. This creates a solid gate: tests must pass before merging, and merging triggers automatic deployment.

Need Help?

Want this done for you?

Our engineering team handles implementations like this every week. Get a free scoping call — we will tell you exactly what it takes and what it costs.

Book a free call

© 2026 NexWorldTech — Built for Global Dominance.