The Problem with Most CI/CD Tutorials Most tutorials show you a pipeline that deploys a "hello world" app to a free Heroku instance.
They skip the messy parts: secrets, rollbacks, and the moment your pipeline breaks because a dependency changed.
I've been there.
After years of fighting with over-engineered setups, I settled on a minimal pipeline that's easy to understand, debug, and extend.
It's not fancy, but it works.
The Core Idea A CI/CD pipeline is just three stages: Test - run automated checks Build - create an artifact Deploy - push the artifact to a server We'll use GitHub Actions because it's free for public repos and integrates with everything.
But the same concepts apply to GitLab CI, CircleCI, or Jenkins.
The Pipeline File Here's the complete : That's it.
Let's break it down.
Stage 1: Test The job runs on every push and pull request.
It checks out the code, installs dependencies with (which respects the lockfile), and runs your test suite.
If a PR fails tests, the job won't run because of the dependency.
Stage 2: Build The job only runs on pushes to (not on PRs).
It builds your app into a folder.
For a Node.js app, might be a bundler like Vite or webpack.
For a Python app, you'd replace with or similar.
Stage 3: Deploy The deployment step uses to copy the built files to your server.
It's dead simple and works for static sites, Node apps, or anything that runs behind a reverse proxy.
Secrets Management Never hardcode credentials.
In GitHub, go to Settings > Secrets and Variables > Actions, and add: - your server IP or domain - SSH username (usually or ) - the private key for a dedicated deploy user Create a separate user on your server with limited permissions.
Don't use root.
Rolling Back Every deployment overwrites the previous folder.
That's fine for small apps, but if you break something, you need a quick rollback.
I keep the last three releases on the server: Then to rollback, just extract the backup.
You can automate this with a script, but even manual is better than nothing.
Handling Dependencies installs exact versions from the lockfile.
This prevents the "works on my machine" problem.
For Python, use with pinned versions.
Testing the Pipeline Before you commit, test locally: If both pass, commit and push to .
Watch the Actions tab to see the pipeline run.
Common Pitfalls SSH key permissions: Make sure the private key is in OpenSSH format, not PuTTY.
If you generate with , it works.
Path issues: The assumes your build outputs to .
Adjust for your project.
Server path permissions: The deploy user needs write access to .
Set it up once with .
Extending the Pipeline Once this works, you can add: Linting - add a job before tests Database migrations - run a separate job after deploy Notifications - send a Slack message on failure using But don't add them until you need them.
The beauty of this pipeline is its simplicity.
When something breaks, you can trace the entire flow in five minutes.
Final Thoughts This pipeline won't handle Kubernetes or blue-green deployments.
But for most side projects, small business apps, and even some production systems, it's enough.
Start simple.
Get it working.
Then iterate.
That's the way to build something that actually works.
Have you built a similar pipeline?
What's your minimal setup?
Let me know in the comments.