CI/CD for Ruby applications: Bundler, RSpec, deployment to Kubernetes

Ruby service releases drag on, and failed deployments happen at night? We set up turnkey CI/CD for Ruby applications: build, checks, and deployment to the cluster. Releases speed up dramatically, and rollback takes minutes.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

CI/CD for Ruby: where the build and deploy break in production

The build is green on the developer's machine and fails in the shared build — the most common story in Ruby projects. The cause is almost always the same: environment divergence — different versions of Ruby and system libraries on the local machine and on the build agent. The same Gemfile produces a different result.

Bundler pins the dependency tree in Gemfile.lock, but the protection works only if the file is committed and not rebuilt on the fly. An update command inside the build step — and every Ruby application build pulls fresh versions along with transitive dependencies.

Native extensions add a second layer of instability: they compile against the system libraries of a specific machine, so the same set of dependencies turns into different artifacts, and a build in a neighboring environment behaves differently.

Flaky tests break trust in the build faster than real defects.

A suite that shares a common database between examples, depends on execution order or on the system clock, fails once every few runs without code changes — and the team gets used to re-running the job, no longer reading the red status. A real regression passes along with it.

A release on Kubernetes often relies on a chain of manual steps: apply migrations, warm the cache, restart background queue workers in the right order.

As long as this order lives in the on-call engineer's head, deploying a Ruby application is reproducible only by the person who invented it, and a migration without a reverse operation makes rollback a manual task. We will break down how each of these points is covered in a CI/CD setup for Ruby.

What a well-tuned pipeline gives you: speed, repeatability, rollback

The same build on any machine is the foundation of predictability.

Bundler pins dependencies in the lockfile, so the developer's laptop, the build environment, and the container deploy the same set of gems, and the "it works on my machine" discrepancy disappears as a class.

RSpec tests run on every commit, not before release: the fast unit-check loop is separated from the integration one with database access, and feedback arrives within minutes.

The path from a change to production shrinks from days to a single pass — the change goes to the main branch, passes checks, and lands in the cluster the same day.

Deployment is automated: the image is built once and promoted between environments without rebuilding, configuration and secrets are injected from the environment rather than hardcoded into the artifact.

Migrations are applied as a separate step before rolling out new replicas, readiness probes do not let traffic into unwarmed instances, and rollout is gradual — first a share of traffic to the new version, then the rest.

Rollback is returning to the previous image, not an emergency manual fix: if the error rate grows or response time drops, the version is reverted within minutes. The database schema remains compatible with both versions because migrations are written backward-compatible.

Pipeline rules and Kubernetes manifests describe these states in advance — release becomes a routine operation with known behavior.

CI/CD modes for Ruby: comparing approaches and constraints

The build mode determines not only release speed but also the class of failures that are even possible.

On a shared runner, the build stays green exactly until a neighboring project pulls a different gem version, while in a container reproducibility appears — but you pay for it with base image maintenance and a separate caching scheme.

Below are four options that teams actually choose between, and the limitations of each. The dependency cache key is calculated from the Gemfile.lock hash: as long as it has not changed, gems are not reinstalled.

Mode When it fits Limitations
Build on a shared runner one or two projects, infrequent releases, tests finish in a couple of minutes shared environment and global gems leak between projects: the build is reproducible by chance
Build in a container with a pinned Ruby version several projects and different Ruby versions, reproducibility required the base image must be updated and rebuilt; startup is slower, and the cache lives only inside the image unless moved outside
Dedicated runner per project heavy RSpec runs, private gems, access to internal registries idle time during low load; system libraries and the environment are updated by the team, not the platform
Dependency cache keyed by Gemfile.lock hash dozens of builds per day, monorepo, long gem installation an incorrect key yields a green build on outdated gems; the cache must be reset periodically

There is no universal mode. A container with cache keyed by the lockfile covers most cases, but it does not replace a dedicated runner where private gems and access to internal registries are needed.

You should choose based on the load profile and reproducibility requirements, not on the "right" way to build.

How implementation proceeds: stages of engineering work

We start by capturing the current state of the build setup: how the application is prepared, where dependencies live, how long a full test run takes, and what is still run manually. Based on the results, we fix the order of work and the readiness criteria for each step.

  1. Build setup audit. We examine the repository: how dependencies are pinned via Bundler, where RSpec runs, how long a full build takes, and which tests are flaky. The output is a map of bottlenecks and discrepancies between the local machine and the server.

  2. Environment scheme. We agree on the set of environments, parameters, and access rights for each. One built image passes through all environments without rebuilding — otherwise a check in the staging setup proves nothing.

  3. Secrets setup. Image registry credentials, rollout keys, and tokens are moved out of the repository into a secure store and bound to a specific environment. Rotation and access revocation do not require build changes.

  4. Pipeline construction. We arrange stages in increasing order of cost: style checks, tests, image build, publication, rollout to Kubernetes. Dependency and layer caching reduces waiting, and a failure in an early stage stops the rest.

  5. Pipeline verification. We run changes through all environments, separately test rollback to the previous image, and deliberately break the build to make sure the rules trigger. We measure the time from commit to a working version in staging.

  6. Handover to support. We provide the configuration description, environment scheme, rollback procedure, and instructions for local reproduction. We run a walkthrough with the team and support the first changes made independently by on-call engineers.

What remains in the repository and documentation after handover

We hand over not only a working pipeline but everything needed to operate it independently. The check is simple: a new engineer uses the documentation to bring up the environment, run tests, and ship a release without asking us a single question.

  • Pipeline configurations in the repository: stage descriptions, Ruby dependency caching, test execution order — with comments on why these parameters were chosen and what will break if they are changed.
  • Deployment documentation: step-by-step rollout procedure, rollback to the previous version, working with secrets and environment variables.
  • Environment scheme: which environments exist, how they differ in service composition and data, and how the build is promoted between them.
  • Access policies: roles and permissions in the repository, image registry, cluster, and pipeline panel — who can change what.
  • Release policy: how a release is coordinated, who approves it, and which checks are mandatory before production.
  • Maintenance instructions: where to view build and application logs, how to reproduce a failed test locally, and where to look during degradation.
  • Database schema migrations and their rollback counterparts — so the data structure changes together with the code, without manual edits on the server.
  • Acceptance checklist: a list of checks confirming that the infrastructure works and has been fully handed over.

All artifacts live next to the code and are updated with it — documentation does not lag behind configuration, and changes go through review just like regular edits.

Case study: reduced release time and eliminated night rollbacks

Starting point

The product is several Ruby services rolled out to Kubernetes. A full build took about forty minutes: Bundler brought up dependencies anew at every step, and RSpec ran in full even when a single model was edited.

A first-attempt release succeeded in six out of ten cases; the rest failed during rollout — the on-call engineer rolled back manually and spent twenty-five minutes doing so at night.

What changed

Bundler dependencies are cached between steps and rebuilt only when the lockfile changes.

Tests were split into a fast set for every commit and a full RSpec run before rollout, and selection by changed paths was added: an edit in one service does not drag the entire repository with it. The image is built layer by layer, and the base layer is reused.

Rollback to the previous version was automated: if liveness checks fail, the previous set of manifests is deployed without human involvement.

What became

  • Full build: from ~40 minutes to ~9.
  • First-attempt release: from 6 out of 10 to 23 out of 25.
  • Rollback to the previous version: from 25 minutes of manual work to automatic rollback in about a minute and a half.
  • The failure class "night rollout with a crash" has been eliminated: releases happen in the working window, and the on-call engineer is not woken by alerts.

The release cadence became predictable: a failed release is no longer an emergency event and does not knock the team off schedule.

How deploying to a managed cluster differs from owning your own servers

The difference is not that one option is "better," but where the responsibility boundary lies.

In a managed cluster, the provider maintains the control plane, nodes, updates, and replacement of failed hardware, while manifests, images, access policies, and the release pipeline remain with us.

On your own servers, the same boundary shifts entirely to you: Kubernetes versions, cluster state, backups, capacity, and on-call duties.

Regardless of the platform, what remains with the team is what is closest to the product: rollout logic, data schema migrations, the order of warming and switching traffic, secrets, and observability of business metrics. It is this layer that provides release speed, not who updates the kernel.

Option When it fits What remains on the team's side
Managed cluster A predictable release cycle is needed, and there is no in-house operations and none is planned Manifests, access, secrets, release policies, data migrations
Cluster on your own servers Data placement requirements or you already have your own hardware Everything listed above plus the control plane, updates, capacity, on-call duties
Hybrid: managed cluster and own nodes for special workloads Some services require data locality or specific hardware Unified access policies, network coordination, monitoring separation

Deployment migration almost always happens without downtime: the new setup is brought up alongside, the same release pipeline with checks is run, configurations and secrets are transferred, traffic is first switched to a share of requests, and the old setup is kept for rollback until metrics confirm the result.

Operations are most often underestimated: security updates, capacity control, response to node failures, and nighttime incident analysis. In the managed option, the provider covers this, and the team's time goes to the product rather than platform maintenance.

Technical doubts: secrets, tests, environments, dependencies

Some doubts arise before work starts, and the scope of the first stage depends on the answers. Below are short answers on pain points: secrets, flaky tests, monolith build, external service failures, and implementation order.

How do secrets get into the build?

Values live in a separate store and are injected at runtime. Build and deploy use different credentials with short-lived tokens, command output is filtered — the log keeps the fact of injection, not the value.

What should be done with tests that fail intermittently?

We mark flaky examples and count the failure rate and time for each. We move them to quarantine so they do not block the pipeline, and fix the causes: shared data, order dependence, real calls.

The monolith takes a long time to build — can it really be sped up?

Dependency and image layer caching, parallel test runs in parts, fast checks ahead of the build. Time to first result drops severalfold, and the developer sees the outcome without waiting for the full run.

What if an external service is unavailable?

In tests, we replace external calls with controlled stubs, leaving a couple of checks against a live connection. In deployment — timeouts, retry with increasing delay, and a partial result instead of an error.

Can it be implemented in parts?

Yes. First automated tests and builds in a shared pipeline, then automated deployment, then environments and rollback. Each stage is useful on its own and does not require stopping development.

Let's discuss the task and come back with an estimate and work plan

To come back with a work estimate and implementation plan rather than general considerations, a few inputs are enough for us. Below is what to send, what we will clarify on a call, and in what form we will deliver the result.

  • Repository or archive with the current build: Gemfile and Gemfile.lock, pipeline configuration — they show where Bundler dependency caching is not working and why a run takes minutes instead of seconds.
  • Kubernetes manifests: Deployment, Service, Ingress, Helm charts, or Kustomize overlays together with readiness and liveness probes.
  • How many clusters and namespaces there are, how the test and production environments differ, and how secrets and environment variables are currently passed.
  • How rollout is arranged today: manual steps, scripts, who rolls back a release and how, and whether there has been downtime in recent releases.
  • Requirements for the result: target build and deployment time, whether blue-green or gradual traffic switching is needed, and acceptable downtime.
  • Access restrictions: who grants rights in the cluster and image registry, and whether there is an air-gapped environment without external network access.

On the call, we will clarify RSpec run parallelism, test separation by type, the image cache strategy, and the order of database migrations during rollout.

In response, we will send the scope of work by stage, timelines for each stage, and a pipeline diagram with the points where your team gets involved. Describe the task in writing or give a link to the repository — we will discuss and come back with an estimate and work plan.