← all postsamirmuz.com →
2026-07-17automationsreci-cdgitlabdeploymentdockersolo-devintermediate

My Trading Bot Deploys Itself

Three days before the morning this post describes, I could not have told you what a CI runner was. Then, one morning, I woke up to a green pipeline and a message from my own infrastructure: the new version of my trading bot had tested, built, and deployed itself to production ove...

11 min readseries: Building a Syariah-Compliant Trading Bot14 views

Three days from "what is a runner?" to a robot shipping to production

Three days before the morning this post describes, I could not have told you what a CI runner was. Then, one morning, I woke up to a green pipeline and a message from my own infrastructure: the new version of my trading bot had tested, built, and deployed itself to production overnight. Zero human hands.

This post is the honest map of how a solo project gets real CI/CD: a self-hosted GitLab, one runner, four stages, and a deploy script with ten gates that had already saved me from myself several times before automation ever touched it.


The shape: ship on tags, validate on pushes

The single most useful policy decision came first: pushing code never deploys. Only a version tag deploys.

git push                          # runs lint + test + build. nothing ships.
git tag bot-v2.5.9
git push --tags                   # runs the same 3 stages, THEN deploys

Every push gets the full validation for free, so I find breakage minutes after writing it. But shipping is a separate, deliberate act: a tag is me saying "this exact commit is a release." The pipeline enforces the difference, not my discipline at 1 AM.

diagram

The infrastructure is all self-hosted and boring on purpose: GitLab CE in one Linux container, a runner in another, production on its own VM, all on my own hardware. Building GitLab by hand instead of using gitlab.com taught me more about the product than a year of using it hosted would have.

The engine: one deploy script, ten gates

CI does not deploy by magic. The deploy stage calls the exact same deploy.sh I can run by hand, and that script is where the safety lives. Ten gates, in order, each one able to abort before production is touched:

diagram

Two design points carry all the weight:

Backup before touching anything (gate 3). If the migration or the new code is bad, the old data is already saved. The backup is not a nightly habit here, it is a precondition of every single deploy.

The switch is atomic (gate 8). current is a symlink. The new release is built and tested BESIDE the running one; only when the test gate passes does the symlink flip. A failed deploy leaves production exactly as it was, because production was never touched. This one pattern converts "deploy failed" from an incident into a log line.

Rollback lives in two places, and I wrote neither twice

  1. During a deploy: deploy.sh's own logic. Test gate fails, the switch never happens. Health check fails after the switch, the symlink flips back to the previous release automatically.
  2. After a deploy: GitLab's environment history. Every deployment to the production environment is recorded, and any past release has a re-deploy button. If a version deploys clean but BEHAVES wrong an hour later, rolling back is one click on a release that already proved it deploys.

No custom rollback framework. The script protects the transition; GitLab remembers every known-good state. Less code, more reliability.

The honest part: what the first deploys taught me

The pipeline's first attempt at production did not succeed. It fail-safed at gate 1: the runner could not even reach the production host, and the diagnosis went all the way down to my firewall's Layer 2 configuration (that story is its own post in the homelab series). The part worth repeating: the deploy failed without touching production. The gates did their job on their very first real test, just not the job I expected.

And one stage deserves its own confession: the test gate originally ran the suite against the production database, which worked right up until production data grew into something my tests did not expect. That failure, and the ephemeral-database fix, is the previous post in this series ("The Deploy Gate That Cried Wolf").

For a solo project, was any of this worth it? The pipeline answers a question that discipline cannot: not "can I deploy safely" but "does deploying safely depend on my mood, my caffeine, and the hour of night?" Now it does not. The robot follows the checklist every single time, and I get to be the person who wrote the checklist instead of the person executing it at midnight.

Takeaways

  • Separate validating (every push) from shipping (tags only). Git already has the ceremony built in.
  • Make the deploy script safe FIRST, automate it second. CI calling a fragile script is just faster fragility.
  • Backup as a deploy gate, not a schedule. Atomic switch via symlink. Health check with automatic revert.
  • Reuse rollback that already exists (your script's + your CI platform's) instead of writing a novel one.

Pictures to add later

  1. GitLab pipeline: the four green stages (lint / test / build / deploy)
  2. GitLab Environments page showing the production deploy history with re-deploy buttons
  3. The Telegram "deployed" notification
  4. The Tests tab showing the full suite listed by name
← back to blog