I built a small, templated GitLab CI/CD pipeline plus a one‑command project templater that takes MERN apps from a fresh repo to a live, Traefik‑routed staging URL in minutes. This is the story, the reasoning, and the exact shape.

TL;DR

  • Goal: ship quickly to staging where ~90% of releases happen.
  • Approach: a tiny GitLab deploy job that talks to a remote Docker host over SSH, Compose overlays merged via Make, and Traefik labels for routing.
  • Templater: a fast CLI that standardizes Node, React, and Next projects with drop‑in CI/Compose files.
  • Outcome: consistent repos, predictable deploys, and clickable links for stakeholders fast.

Why Staging Comes First

Most of our client work lives on staging; many clients run production themselves. That means our highest leverage is getting a live URL fast for reviews, demos, and feedback. Heavy pipelines would slow us down without helping the 90% path we actually walk.

So I optimized for three things: speed, consistency, and simplicity.


Design Principles

  1. Small surface area A short pipeline is easier to read, reason about, and copy.
  2. Composable by environment Base Compose + thin overlays; Compose does the merging, not custom logic.
  3. Agentless servers DOCKER_HOST=ssh://… keeps the target host clean; no extra agents or daemons.
  4. Reverse‑proxy native Traefik router rules live beside the service as labels.
  5. Staging‑first ritual Production is optional; staging is frictionless.

Architecture at a Glance

1) A tiny GitLab job that deploys over SSH

The job logs into the registry, materializes the staging env file from CI variables, then asks the remote Docker host to pull and recreate the service with Compose.

# .gitlab-ci.yml (staging excerpt)
deploy-staging:
  stage: deploy
  image: docker:27
  variables:
    ENV_NAME: "Staging"
  before_script:
    - mkdir -p ~/.ssh && ssh-keyscan "$SERVER_IP" >> ~/.ssh/known_hosts
    - export DOCKER_HOST="ssh://$SERVER_USER@$SERVER_IP"
    - echo "$CI_REGISTRY_PASSWORD" | docker login --username "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin
  script:
    - echo "$ENV_BASE64" | base64 -d > ".env-${ENV_NAME}"
    - make "${ENV_NAME}"                            # emits Staging.yml
    - docker compose -f "${ENV_NAME}.yml" --env-file ".env-${ENV_NAME}" pull
    - docker compose -f "${ENV_NAME}.yml" --env-file ".env-${ENV_NAME}" up -d --force-recreate
  rules:
    - if: '$CI_COMMIT_BRANCH == "Staging"'

Why this works:

  • Stateless runners, clean servers.
  • Readable YAML any engineer can follow it end‑to‑end.
  • Idempotent rollouts via compose pull && up -d.

2) Compose overlays via Make

A single Make target merges base and staging overlay into a concrete file for the deploy:

staging:
	docker compose -f ./docker-compose.yml -f ./docker-compose.stag.yml config > Staging.yml

Environment differences stay small and explicit.

3) Traefik‑aware overlay

Traefik router + service labels make the app reachable immediately at your chosen host.

# docker-compose.stag.yml (excerpt)
services:
  web:
    image: ${IMAGE_REPO}:${IMAGE_TAG}
    env_file:
      - .env-Staging
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp-stag.rule=Host(`staging.example.com`)"
      - "traefik.http.services.myapp-stag.loadbalancer.server.port=3000"
networks:
  traefik-reverse-proxy:
    external: true

Result: merge → pipeline → live URL without extra infra tickets.


The Project Templater

I wrapped the whole pattern in a fast templater so new repos start standardized:

  • Choose a stack: Node API, React SPA, or Next SSR.
  • Answer prompts: repo URL, staging host, optional CI toggle.
  • Scaffold: base Compose, staging overlay, GitLab CI snippet.
  • Initialize Git: create develop, Staging, and Production branches; first commit.

From there, pushing to Staging produces a live, Traefik‑routed URL. Every project has the same bones, which makes reviews and handoffs effortless.


Developer Experience Highlights

  • Environment‑as‑branch Deploy by merging to Staging; the trigger is obvious in history and reviews.
  • Registry‑first Treat images as artifacts; the server pulls exactly what CI built.
  • SSH to Docker Deploy from anywhere, no agents to maintain.
  • Readable Compose Base + overlay is human‑scale and greppable.
  • Fast feedback Stakeholders click a link minutes after a merge; iteration accelerates.

Impact on the Organization

  • Shorter cycle time New repos reach staging quickly; demos happen earlier.
  • Easier onboarding Every repo shares the same layout and deploy shape.
  • Fewer infra tickets Hostnames and paths live in labels next to the service.
  • Right‑sized ceremony We don’t pay a complexity tax for prod‑only features when clients run production.

Quick Start (What You Can Copy)

  1. Keep your pipeline small for the common path.
  2. Use Compose overlays to model environments.
  3. Deploy over SSH with DOCKER_HOST to stay agentless.
  4. Put Traefik labels beside the service so routing changes are code.
  5. Wrap it in a templater so every new repo starts standardized.

Current Limitations (Short by Design)

  • Staging‑first: blue/green and canary are add‑ons if needed.
  • No formal test gates yet: rely on smoke checks + stakeholder validation; add tests incrementally.
  • Single‑host expectation: assumes a Docker host and Traefik gateway; horizontal orchestration is out of scope for this pattern.

The trade‑off is deliberate: keep the baseline simple so teams ship consistently.


Closing

If your stakeholders live in staging, optimize for staging. Make the path from commit to clickable URL short, obvious, and repeatable. When the baseline is this simple, delivery feels effortless and teams spend more time building features than wrangling YAML.