CI/CD Pipelines with GitHub Actions: From Commit to Production
Manual deployments are one of the highest-risk activities in software engineering. A developer SSHes into a production server, runs commands from memory, makes a mistake, and causes an outage at 5pm on a Friday. Continuous Integration and Continuous Deployment (CI/CD) automates the path from a Git commit to a running system — running tests, building containers, and deploying to production without manual intervention.
This article covers how to build a real CI/CD pipeline with GitHub Actions: the anatomy of a workflow file, a build-and-test pipeline, Docker image builds and pushes to a container registry, deployment strategies (rolling, blue-green, canary), and secrets management. By the end you'll have a pipeline that deploys on every merge to main.
CI vs CD: What the Letters Actually Mean
Continuous Integration (CI) is the practice of merging code changes frequently and automatically validating them with a build and test suite. The goal is to catch integration bugs — where two developers' changes conflict — within minutes, not during a quarterly release crunch. CI runs on every pull request and merge.
Continuous Delivery (CD) extends CI by automatically deploying validated code to a staging environment. Deployment to production still requires a manual approval step. Continuous Deployment (also CD) removes that manual step — every green build on main goes to production automatically. Most teams target Continuous Delivery (auto-deploy to staging, manual gate to production) and move to Continuous Deployment as confidence in their test suite grows.
Quick reference
- CI: code is automatically built and tested on every push. Merge only when green.
- Continuous Delivery: CD deploys to staging automatically. Production deploy is manual but always ready.
- Continuous Deployment: every green main build goes directly to production — no human approval.
- Trunk-based development: everyone commits to main (or short-lived branches < 1 day). Required for true CD.
- Feature flags: use flags to deploy dark code to production without activating it — decouples deploy from release.
- Mean time to recovery (MTTR): small, frequent deploys reduce the blast radius of failures and speed recovery.
Remember this
CI ensures every commit is verified. CD ensures it's deployable. Start with Continuous Delivery (auto-staging, manual production) and graduate to Continuous Deployment.
GitHub Actions Anatomy
A GitHub Actions workflow is a YAML file in .github/workflows/. A workflow has triggers (when it runs), jobs (parallel or sequential units of work), and steps (sequential shell commands or actions within a job). Jobs run on runners — GitHub-hosted VMs (ubuntu-latest, windows-latest, macos-latest) or self-hosted runners in your infrastructure.
Actions are reusable steps published on the GitHub Marketplace. actions/checkout fetches your code. actions/setup-node installs Node. docker/login-action authenticates to a registry. Using published actions for common tasks reduces boilerplate and inherits community security fixes.
Quick reference
- on: defines triggers — push, pull_request, schedule (cron), workflow_dispatch (manual).
- jobs: each job runs on a fresh VM in parallel by default. Use needs: [job-name] for sequencing.
- steps: run sequentially within a job. Each step is a uses: (action) or run: (shell command).
- services: spin up Docker containers alongside the job (database, Redis, mock servers).
- cache: save node_modules/pip cache between runs — turns 3min installs into 10s.
- Concurrency: cancel in-flight runs on the same branch when a new commit is pushed.
Remember this
One YAML file per workflow. Separate jobs for test, build, and deploy. Use services for database integration tests. Cache dependencies.
Docker Build and Container Registry Push
After tests pass, the deploy artifact is a Docker image. Building and pushing it to a container registry (GitHub Container Registry, AWS ECR, Google Artifact Registry) is the bridge between CI and CD. The image is tagged with the Git SHA — a short commit hash — making every image traceable back to the exact code it was built from.
Multi-stage Dockerfiles separate the build environment (compiler, dev dependencies) from the runtime image (only what's needed to run the app). A Node.js app's final image should be ~100MB, not 1GB with all build tools included.
Quick reference
- Tag images with git SHA (github.sha) — every image traceable to exact code. Also tag latest for convenience.
- Multi-stage builds: builder stage has compiler/tools, runtime stage has only the binary. Smaller, more secure.
- GitHub Container Registry (ghcr.io): free for public, included with GitHub. No extra credentials needed.
- Layer caching (type=gha): caches Docker layers between runs. Turns 5min builds into 30s on cache hit.
- Scan images: add trivy or grype scanning step to catch CVEs before pushing.
- Never bake secrets into images. Use runtime environment variables or secret stores (AWS Secrets Manager, Vault).
Remember this
Build multi-stage Docker images tagged with the Git SHA. Push to a registry only after tests pass. Use layer caching to keep build times under 2 minutes.
Deployment Strategies: Rolling, Blue-Green, Canary
How you deploy matters as much as what you deploy. Rolling deployments replace instances one at a time — new pods come up, old pods come down, traffic gradually shifts. This means during deployment, old and new versions run simultaneously, which requires backward compatibility for any shared database changes.
Blue-green deployments run two identical environments (blue and green). You deploy to the idle environment, run smoke tests, then flip traffic. Rollback is instant — flip traffic back. Canary deployments shift a small percentage of traffic (1–5%) to the new version first, monitor error rates and latency, then gradually increase. Each strategy trades deployment speed for risk reduction.
Quick reference
- Rolling: gradual, requires API backward compatibility during rollout. Default in Kubernetes.
- Blue-green: instant rollback, doubles infrastructure cost during deploy. Good for risky migrations.
- Canary: partial traffic to new version, monitor metrics, increase gradually. Best for high-traffic services.
- Readiness probes: Kubernetes waits for /health to return 200 before routing traffic. Essential for zero-downtime.
- Rollback: kubectl rollout undo deployment/api — reverts to previous ReplicaSet in seconds.
- Feature flags instead of canary: deploy fully, activate for 1% of users via flags. Separates deploy from release.
Remember this
Use rolling deploys with readiness probes for zero-downtime. Add a smoke test step that auto-rolls back on failure. Blue-green for database migrations; canary for high-risk features.
Secrets and Environment Management
Secrets in code are one of the most common security vulnerabilities. API keys, database passwords, and JWT secrets hardcoded in source code get committed to version history, exposed in CI logs, and leaked by any developer who clones the repo. The rule is absolute: secrets never live in the repository.
GitHub Secrets (Settings → Secrets) are encrypted at rest and injected as environment variables into workflows. They're masked in logs — if a secret value appears in a log line, GitHub replaces it with ***. For production systems, a dedicated secret store (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) provides rotation, audit logging, and fine-grained access control.
Quick reference
- GitHub Secrets: encrypted, injected as env vars, masked in logs. Free, good for CI/CD secrets.
- AWS Secrets Manager / Azure Key Vault: rotation support, audit logging, IAM-based access. For production runtime.
- HashiCorp Vault: self-hosted, dynamic secrets (generates temporary DB credentials on demand).
- Commit .env.example with structure but no values. Add .env to .gitignore.
- Scan for secrets in CI: add gitleaks or truffleHog as a pre-merge check to catch accidental commits.
- Rotate secrets regularly. Use separate secrets per environment (dev/staging/prod).
Remember this
Never commit secrets to a repository. Use GitHub Secrets for CI/CD. Use a secret store (Secrets Manager, Vault) for runtime configuration. Scan commits for leaked secrets.
A well-built CI/CD pipeline does three things: it catches bugs before they reach users (CI), it makes deploys so boring that no one is scared to do them (CD), and it makes rollback fast enough that a bad deploy doesn't become an outage (deployment strategies).
Start simple: a GitHub Actions workflow that runs tests on every PR and deploys to staging on merge to main. Add Docker and container registry push when you're ready. Add canary or blue-green when traffic justifies it. The most important step is removing the human from the critical path — not because humans make mistakes, but because manual processes don't run at 2am when that urgent fix needs to ship.
Related Articles
Explore this topic