Back to Blog

The Moment I Stopped Worrying About Staging Servers and Embraced GitLab Review Apps

The Moment I Stopped Worrying About Staging Servers and Embraced GitLab Review Apps

The Moment I Stopped Worrying About Staging Servers and Embraced GitLab Review Apps

If you’ve ever had to ask "Is anyone using staging?" in a crowded Slack channel, you already know the quiet desperation of a blocked release cycle. Static staging environments are the traffic jams of the software world—expensive, crowded, and almost always broken the moment you need to demo a critical feature.

For a long time, my team lived in this bottleneck. We had a single "staging" server that was supposed to be our source of truth. In reality, it was a mess of half-baked features, weird database states, and conflicting configurations. When two developers needed to test different things, one had to wait. It wasn't just slow; it was demoralizing.

The Myth of the "Permanent" Staging Server

The problem with a permanent staging environment is that it becomes a "pet." We nurture it, we manually tweak its config, and we pray nobody runs a destructive migration on it. But modern CI/CD shouldn't rely on pets.

I realized we needed to move toward ephemeral environments—disposable instances of our app that live and die with a Merge Request (MR). In GitLab, these are called Review Apps. The goal is simple: every branch gets its own URL. When the branch is merged or closed, the environment vanishes.

Making it Work in the .gitlab-ci.yml

Setting this up feels like magic the first time you see it work. You don't need a massive infrastructure team; you just need a way to deploy your code programmatically (like a Kubernetes cluster, a cheap VPS with Docker, or even S3 for frontend apps).

Here’s a simplified version of how we configured our review apps using Docker and a basic naming convention.

deploy_review:
  stage: deploy
  script:
    - echo "Deploying to review server"
    - docker stack deploy -c docker-compose.yml myapp-${CI_COMMIT_REF_SLUG}
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_COMMIT_REF_SLUG.dev.example.com
    on_stop: stop_review
  only:
    - merge_requests

stop_review:
  stage: deploy
  script:
    - echo "Removing review app"
    - docker stack rm myapp-${CI_COMMIT_REF_SLUG}
  when: manual
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  only:
    - merge_requests

The key here is the environment block. GitLab tracks these deployments. When a developer pushes code, GitLab creates a "View Deployment" button directly on the MR.

Why the on_stop Action is Critical

You’ll notice the stop_review job. This is the part people usually forget, and it’s why cloud bills skyrocket.

The on_stop attribute tells GitLab: "When this MR is merged or closed, run this specific job." Without this, you’ll end up with dozens of zombie containers or VMs eating up resources for features that were merged three months ago. By setting when: manual on the stop job but linking it to on_stop, you give yourself the power to kill the environment manually *or* let GitLab handle it automatically when the branch dies.

The "Database Problem" (and how we cheated)

The biggest hurdle for Review Apps is almost always the database. You can't just spin up a 500GB production clone for every branch.

We solved this by using a "Sanitized Seed." Every time our Review App starts, it spins up a fresh, empty Postgres container and runs a script to seed it with just enough realistic-looking data to make the app usable.

# inside our entrypoint script
bundle exec rails db:setup
bundle exec rails db:seed:minimal_set

If you really need production-like data, look into tools like CloudNativePG for Kubernetes or simply use an anonymized daily dump that is small enough to pull quickly. It won't be perfect, but 95% of your QA doesn't need the full production dataset.

The Cultural Shift

Once we moved to Review Apps, the "Staging is down" messages stopped. But something else happened: Product Managers started actually looking at the code.

Before, a PM had to wait for a feature to hit the main staging server. Now, I send them a link to a specific Review App while the code is still in review. They can click around, leave a comment on the MR saying "the button color is off," and I can fix it before the code ever touches the main branch.

It shifted QA from a "final gate" to a "continuous conversation."

Some Gotchas to Watch For

1. SSL Certificates: If you’re using dynamic subdomains (e.g., feature-xyz.example.com), make sure you have a wildcard SSL certificate. Dealing with cert errors on every new review app is a great way to make people hate the new system.
2. External APIs: If your app talks to third-party services (like Stripe or Twilio), you’ll need a strategy for managing API keys and webhooks for 15 different versions of your app running simultaneously.
3. Resource Limits: Don't let a single Review App hog all the CPU. Set strict memory and CPU limits in your orchestrator (Docker/K8s).

Review Apps take some effort to configure initially, but the ROI is immediate. You’re not just automating a deployment; you’re removing the human friction of waiting in line. Stop babysitting your staging server and let it die. Your team will thank you.