Perfect topic for dev.to — this kind of “build CI/CD from scratch”
Modern CI/CD tools like GitHub Actions, GitLab CI, and Jenkins often feel like magic boxes driven by YAML files. But what actually happens behind the scenes?
In this tutorial, we build a real, event-driven CI/CD platform from scratch, using Node.js, GitHub Webhooks, and Railway Cloud — no YAML, no plugins, just clean code and clear control.
By the end, you’ll understand how CI/CD systems really work internally, not just how to configure them.
🧠 What We’re Building
We design a custom CI/CD orchestrator that:
- Listens to GitHub webhook events
- Verifies webhook authenticity using HMAC signatures
- Extracts branch and commit context
- Triggers a deployment pipeline
- Redeploys a real production payment service
- Runs entirely on Railway Cloud
This is not a demo — it’s a real production-style pipeline.
🏗️ High-Level Architecture
Here’s the architecture we implement:
Developer Pushes Code
↓
GitHub Repository
↓
GitHub Webhook Event
↓
Webhook Server (Node.js)
↓
Signature Verification (HMAC)
↓
Deployment Orchestrator
↓
Pipeline Execution Logic
↓
Railway GraphQL API
↓
Payment Service Redeployed
Key Design Principle
The orchestrator controls deployments — services never self-deploy.
This mirrors how real CI/CD platforms work internally.
🔧 Technology Stack
Core Technologies
- Node.js (Express) – Webhook server & orchestrator
- GitHub Webhooks – Event-driven triggers
- Crypto (HMAC SHA-256) – Security & signature verification
- Railway Cloud – Infrastructure & deployments
- Railway GraphQL API – Programmatic redeployments
📦 System Components Breakdown
1️⃣ GitHub Repository
- Hosts the payment service
- Configured with a webhook pointing to our server
- Sends events on every push
2️⃣ Webhook Server (Node.js)
The webhook server is responsible for:
- Receiving GitHub events
- Verifying request authenticity
- Extracting branch & payload
- Forwarding events to the orchestrator
Signature Verification (Critical Security Layer)
GitHub signs each webhook request using a shared secret.
We validate it using HMAC:
function verifySignature(req) {
const sig = req.headers['x-hub-signature-256'];
const hmac = crypto.createHmac('sha256', GITHUB_SECRET);
const digest = "sha256=" + hmac.update(JSON.stringify(req.body)).digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(digest));
}
This ensures:
- Requests really came from GitHub
- Payloads were not modified
- Unauthorized triggers are blocked
3️⃣ Branch-Aware Event Processing
const branch = req.body.ref?.split("/").pop() || "main";
This allows:
- Branch-based pipelines
- Different deployment strategies per branch
- Easy future extension (staging, prod, hotfix)
🧠 The Orchestrator (Heart of the System)
The orchestrator is a pipeline controller, similar to GitHub Actions or Jenkins — but implemented in code.
Responsibilities
- Register pipelines
- Map pipelines to branches
- Execute pipeline steps sequentially
- Handle failures gracefully
Orchestrator Design
class Orchestrator {
constructor() {
this.pipelines = {};
}
registerPipeline(branch, steps) {
this.pipelines[branch] = steps;
}
async trigger(branch, payload) {
const steps = this.pipelines[branch] || this.pipelines["main"];
if (!steps) return;
for (const step of steps) {
await step(payload);
}
}
}
Pipeline Registration
orchestrator.registerPipeline("main", [runPaymentPipeline]);
This line means:
“Whenever main branch changes, run this deployment pipeline.”
🚀 The Deployment Pipeline
Instead of installing, testing, and building locally, we let Railway handle infrastructure, just like real cloud-native CI/CD systems.
Pipeline Responsibilities
- Authenticate with Railway
- Trigger redeployment
- Monitor API response
- Fail safely if deployment fails
Payment Service Pipeline
export async function runPaymentPipeline() {
const response = await fetch(
"https://backboard.railway.app/graphql/v2",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RAILWAY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation DeployService($serviceId: String!, $environmentId: String!) {
serviceInstanceRedeploy(
serviceId: $serviceId,
environmentId: $environmentId
)
}
`,
variables: {
serviceId: PAYMENT_SERVICE_ID,
environmentId: ENVIRONMENT_ID,
},
}),
}
);
}
This mirrors how:
- GitHub Actions
- GitLab CI
- CircleCI
trigger deployments internally via provider APIs.
🔐 Why This Architecture Matters
Advantages Over Traditional CI/CD
✅ Full control over pipeline logic
✅ No YAML complexity
✅ Easier debugging
✅ Clear separation of concerns
✅ Extendable to any cloud provider
🔮 What You Can Build Next
This foundation can easily be extended to:
- Multiple services
- Multiple environments (dev, staging, prod)
- Manual approvals
- Rollbacks
- Notifications (Slack, Email)
- Full internal platform tooling
🎯 Who Should Read This?
This guide is perfect for:
- Backend engineers curious about DevOps internals
- Developers tired of copy-pasting CI configs
- Platform engineers
- Anyone who wants to truly understand CI/CD
🧩 Final Thoughts
CI/CD isn’t magic — it’s just event handling, orchestration, and APIs.
By building your own orchestrator, you don’t just deploy code —
you gain architectural clarity and engineering confidence.
If you enjoyed this deep dive, consider exploring:
- Multi-stage pipelines
- Canary deployments
- Platform engineering concepts
Happy building 🚀
– CodingMavrick
For full video tutorial watch:- Build Your Own CI/CD Pipeline from Scratch | Node.js DevOps [https://youtu.be/7jlC-Svus9M]
Top comments (0)