Skip to content

From a Thursday-evening deploy ritual to shipping 40+ times a week

A containerized build and a gated CI/CD pipeline replaced a manual, once-a-week deploy ritual, with a one-click rollback that turns a bad release into a non-event.

ShuntwellCorbel & Lane/client-portal
prod · run 1,481 liveDH
Run 1,482 Held at approval

main · "Statement list: sort by issue date" · P. Osei · started 2:40 PM

Re-runApprove production

Stages

Pipeline 7m 48s so far · production est. 1m 12s
Commit4be17c2
Imagesha-4be17c2
BuiltOnce · promoted unchanged
StagingDeployed in 36 s
  1. Build & push imagesha-4be17c2 pushed to registry1m 40sPassed
  2. Unit specs + lint412 specs passed · lint clean1m 10sPassed
  3. Integration testsAgainst a disposable database3m 36sPassed
  4. Security scan0 critical · 0 high46sPassed
  5. Deploy to stagingSame image · deployed in 36 s36sPassed
  6. Approval gateWaiting on a release owner—Waiting
  7. Promote to productionBlue-green: idle slot, then switch~1m 12sQueued
Where the 9 minutes go · approval wait excludedIntegration tests 40%

Gates before production

4 of 4 evaluated
TestsPassed412 unit specs green · integration suites greenevaluated on sha-4be17c2
CoverageAbove floorLine coverage checked against the 80% floor
VulnerabilitiesClearImage scan: 0 critical · 0 highevaluated on sha-4be17c2
Manual approvalHolding1 release owner required · requested 2:48 PMDHSAMFrelease-owners
Production stays on run 1,481 until approved, and 1,481 stays warm in the idle slot after the switch.

Who, what, and how long

Industry
Professional Services
Duration
7 weeks
Cooperation model
Fixed price, phased
Services
ContainerizationCI/CD pipelineRollback strategy
Integrations
HubSpotDocuSignXeroGoogle Workspace
Technologies
DockerGitHub ActionsTerraformContainer registryBlue-green deployment
Team
1 Project lead2 Frontend engineers1 Backend engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

What went wrong, and when

Deploys ran on undocumented tribal knowledge, environments drifted from each other, and the only rollback plan was redeploying an old build by hand and hoping it still worked.

Environments were built independently and had drifted, so staging didn't predict production: a class of failure the team had learned to expect and couldn't diagnose. Deploy steps lived in one engineer's head. And the rollback path had never been exercised, so nobody knew whether the old build would still run against the current database.

We containerized the app into a reproducible image, built a CI pipeline that tests and gates every commit, and wired automated delivery that promotes one artifact through environments with a one-click rollback if anything goes wrong.

Process

Phase by phase

  1. Phase 1: Containerise

    Reproducible builds

    Packaged the application into a reproducible image so a build stopped depending on the machine it ran on.

    • Dockerfile
    • Build pipeline
  2. Phase 2: Gate

    Nothing merges unchecked

    Built automated build, test, and lint stages gating every merge.

    • CI stages
    • Branch protection
  3. Phase 3: Promote

    One artifact, three environments

    Wired artifact promotion from staging to production so the tested thing is the shipped thing.

    • Promotion pipeline
    • Environment config
  4. Phase 4: Prove rollback

    Under a real failure

    Set up a one-click rollback and verified it by deliberately shipping a broken build.

    • Rollback path
    • Verification report
    • Release runbook
ShuntwellCorbel & Lane/client-portal
prod · run 1,481 liveDH
Releases

Production releases, newest first · last 7 days, 09/11–09/17/2026

Export log

Release log

Runs 1,476–1,482
RunChangeAuthorTimePipelineStatus
1,482Statement list: sort by issue dateP. Osei2:40 PM7m 48sAwaiting approval
1,481Invoice email copy for overdue remindersR. Tamura1:32 PM9m 00sLive
1,480Pin statement renderer to 4.8 while 5.0 is investigatedD. Hale11:05 AM8m 42sReleased
1,479Bump statement renderer 4.8 → 5.0D. Hale9:49 AM8m 54sRolled back
1,478Client inbox: unread count on tabM. Ferris9:20 AM9m 06sReleased
1,477Upload limit message for scanned receiptsP. Osei8:51 AM8m 48sReleased
1,476Terraform: raise portal task memoryS. Adeyemi8:12 AM9m 12sReleased

Production deploys per day

41 this week · none on the weekend
8Fri0Sat0Sun9Mon9Tue9Wed6Thu

Weekly digest

Sent Thu 8:00 AM
Production deploys41this week · was 1 a week before the pipeline
Commit to production9 minapproval wait excluded
Where the time goesBuild & push image1m 40sUnit specs + lint1m 10sIntegration tests3m 36sSecurity scan46sDeploy to staging36sPromote to production1m 12s
40%Integration tests (3m 36s) are the share of the pipeline that goes next.
Rollbacks1 · run 1,479 · 34 sWeekend on-call deploys0
On screen

The tail of the week's releases, runs 1,476 to 1,482, with the rolled-back renderer bump among six clean ones, beside the weekly digest: 41 deploys against one a week before, nine minutes from commit to production, and integration tests as the 40% that goes next.

The numbers, before and after

1 → 40+

Deploys per week

Hours → under a minute

Rollback time

0

Weekend on-call deploys

Deploys per week is the pipeline's own count in the quarter after cutover. Rollback time is measured from the decision to roll back to the previous version serving traffic, taken from the deliberate failure and from two real incidents since. Weekend on-call deploys is a count, and it's zero because the window no longer exists.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

Introduction

The engagement

Deploys happened manually once a week, after hours, with an engineer on standby in case something broke. The ritual made shipping feel risky and held every feature to the weekly window.

A team shipping once a week, on Thursday evening, with an engineer on standby. The ritual was rational: the only rollback was redeploying an old build by hand, so every deploy was a one-way door and the only defense was doing it rarely with someone watching. The engagement was commissioned to remove the door, not to deploy more often.

DevOps & Release Engineering

The solution

How it was handled

Packaged the app into a reproducible Docker image

One image is built per commit and promoted through environments unchanged, with configuration injected at run time instead of baked in per environment.

Built automated build, test, and lint stages gating every merge

Build, test and lint run as required checks that block the merge, and Terraform plans post the infrastructure diff into the same review.

Wired artifact promotion through staging to production

Promotion went in before automated deployment, so the team had a working manual path through the pipeline before anything ran without them.

Set up a one-click rollback path and verified it under a real failure

The rollback was verified by deliberately shipping a broken build to production in a quiet window, because an unexercised path is a hypothesis.

One reproducible artifact

A container image built once and promoted through environments unchanged.

One container image is built per commit, tagged with that commit, and promoted through staging and production unchanged. Nothing is rebuilt per environment, because a rebuild is a new artifact however identical the Dockerfile claims it is. Configuration arrives as environment variables at run time, so the thing tested is byte-for-byte the thing that ships.

What shipped
  • Built once per commit, promoted unchanged
  • No per-environment rebuilds. A rebuild is a new artifact
  • Configuration injected at run time, never baked in
ShuntwellCorbel & Lane/client-portal
prod · run 1,481 liveDH
Nightly drift compare

development, staging and production compared at 2:00 AM, 09/17/2026 · image, configuration and infrastructure

Compare now
developmentIn agreementImageregistry.corbel.internal/client-portal:sha-a91d03eRun 1,475 · Deployed on merge 3 flags ahead
stagingIn agreementImageregistry.corbel.internal/client-portal:sha-a91d03eRun 1,475 · Promoted 5:14 PM 3 flags ahead
productionLiveImageregistry.corbel.internal/client-portal:sha-a91d03eRun 1,475 · Promoted 5:31 PM flags at release

Compared items One image across all three

Only difference: 3 feature flags meant to be ahead during a build
ItemdevelopmentstagingproductionResult
Image digestsha256:a91d…03e7sha256:a91d…03e7sha256:a91d…03e7Identical
Image tagsha-a91d03esha-a91d03esha-a91d03eIdentical
Built byrun 1,475run 1,475run 1,475Built once
Config keys (run time)24 keys24 keys24 keysSame keys
Baked-in confignonenonenoneNone
Terraform stateno changesno changesno changesMatches code
statements.bulk_exportononoffAhead · intended
inbox.threaded_repliesononoffAhead · intended
billing.split_invoicesononoffAhead · intended
Configuration values are injected as environment variables at run time and aren't part of the image, so key sets are compared, never secrets.
On screen

The nightly drift compare: one image digest across development, staging and production, the same run-time configuration keys and no baked-in config, with the only difference three feature flags meant to be ahead during a build.

ShuntwellCorbel & Lane/client-portal
prod · run 1,481 liveDH
Change 318 · Bulk statement export Merge blocked

feature/bulk-export → main · M. Ferris · 4 commits · required checks run on every push

Re-run failed Merge
1 required check failing. This change can't merge into main.Build, unit specs, integration tests and lint are required on main. A failure stops the merge here, before main is broken.

Required checks

Branch protection · main
  • Build imagesha-7c02e9b · built once for this commit1m 38sPassed
  • Unit specsAll specs green1m 04sPassed
  • Lint0 problems22sPassed
  • Integration testsstatements/bulk_export.spec · 1 failing3m 41sFailed
  • Terraform planDiff posted to this review · 1 add, 1 change31sPosted
Rules on main Required checks must pass before merge. They block; they don't just report Infrastructure changes merge only with their plan reviewed One approving review from someone other than the author Merged commits build one image; nothing is rebuilt later

Terraform plan Posted to review

Same trigger as the checks
Terraform will perform the following actions: ~module.portal.container_service.web will be updated~ env_keys = 24 -> 25+ env_key = "STATEMENT_EXPORT_QUEUE" image = (set at deploy) +module.portal.queue.statement_exports will be created+ name = "statement-exports"+ retention_days = 4+ visibility_secs = 300+ dead_letter_after = 5 Plan: 1 to add, 1 to change, 0 to destroy.
SAS. Adeyemi · on the plan · 11:22 AMFour days of retention on the export queue — is that intended, or should failed exports live as long as statements do?
On screen

A change blocked before it reaches main: build, unit specs and lint green, a failing integration test stopping the merge, and the Terraform plan posted into the same review with a reviewer's question on it.

Gated merges

Build, test and lint stages block a merge up front, before anything reaches main.

Build, unit tests, integration tests and lint run as required status checks on the pull request, so a failure blocks the merge instead of arriving as a notification after main is already broken. Terraform plans run on the same trigger and post the infrastructure diff into the review, which stopped configuration changes landing without anyone reading what they'd actually do.

What shipped
  • Required checks block the merge; nothing reports after the fact
  • Terraform plan diff posted into the review
  • Infrastructure changes reviewed as part of the code change
ShuntwellCorbel & Lane/client-portal
prod · run 1,478 liveDH
Run 1,479 rolled back Reverted

"Bump statement renderer 4.8 → 5.0" · rolled back to run 1,478 · Thu, 09/17/2026

Open runbook
5xx spike9:57 AMStatement PDFs
Rollback clicked9:58:12 AMOne click · D. Hale on call
Previous run serving9:58:46 AMRun 1,478 · slot A
Decision to serving34 sRouting change, not a rebuild

5xx responses · GET /statements/:id/pdf

Shape only · 30 s buckets

Blue-green slots

after 9:58:46 AM
Slot A · run 1,478Live · 100%Kept warm and healthy the whole time
Slot B · run 1,479Drained · 0%Held for the renderer investigation
A rebuild of run 1,478 would have cost about ten minutes.

Timeline

From the pipeline's own event log
  1. 9:49 AMRun 1,479 promoted to slot B and switched live · slot A keeps run 1,478 warm
  2. 9:57 AM5xx spike on GET /statements/:id/pdf · health check alert pages on call
  3. 9:58:12 AMD. Hale (on call) clicks Roll back to run 1,478 · no rebuild, a routing change
  4. 9:58:46 AMSlot A serving all traffic · run 1,478 live, 34 s after the click
  5. 11:05 AMRun 1,480 pins the statement renderer to 4.8 while 5.0 is investigated

Rollback as routine

On screen

The bad release end to end: a 5xx spike on statement PDFs at 9:57 AM, one click by the engineer on call, and production back on the previous run 34 seconds later, served from the slot that was kept warm.

A one-click rollback path, proven under a real failure.

Blue-green means the previous version is still running and still healthy when the new one takes traffic, so rollback is a routing change: seconds, where a rebuild would cost ten minutes. It was tested by deliberately shipping a broken build to production during a quiet window, because a rollback path that's never been exercised is only a hypothesis.

What shipped
  • Blue-green: rollback is a routing change, not a rebuild
  • Previous version kept warm and healthy
  • Verified by shipping a deliberately broken build in a quiet window

Working inside their operation

A cross-functional team of 3 worked on a fixed price, phased basis over 7 weeks, covering Containerization, CI/CD pipeline, Rollback strategy. We held a standing mid-week checkpoint and wrote decisions down in place of status meetings. Nothing shipped that they hadn't seen working first.

Seven weeks, phased so the gate landed before the automation: nothing deployed automatically until build, test and lint were blocking merges, which meant the team spent two weeks with a pipeline that only refused things. That order was deliberate. A team that trusts the checks will accept automated deployment, and one that doesn't will route around it.

What changed in the runbook

  • The weekly deploy ritual was a symptom of having no way back. Deploying too often was never the problem.

    Deploy frequency was a consequence, never the goal: the team shipped weekly because that was the safe rate for a one-way door, and the rate rose on its own once there was a way back.

  • Promoting one artifact removed a whole class of works-in-staging failures.

    The works-in-staging failures were environment drift, and promoting one artifact removes drift by construction. Nobody has to keep two builds in agreement.

  • A rollback nobody has exercised isn't a rollback. The deliberate failure was the point.

    The deliberate failure is what turned the rollback from a documented procedure into a rehearsed one, and that's the difference at two in the morning.

One bad release, replayed

Why the way back takes seconds and never a rebuild

The renderer bump that broke statement PDFs, step by step: where each run was, which slot held the traffic, and what one click actually changed. Switch tabs, or use the arrow keys once one is focused.

Run 1,479 · Thu 09/17/2026 · blue-green slots A and B

9:58:12 AMOne click points the router back at slot A. Nothing is built, pulled or started, because run 1,478 never stopped. It is serving all traffic at 9:58:46 AM, 34 seconds after the decision; rebuilding it would have cost about 10 minutes.

Slot A · run 1,478 Live
100%of production traffic
Serving users
Slot B · run 1,479 Drained
0%of production traffic
Kept for the renderer investigation
Decision to serving34 smeasured: 34 s
Routing changeRebuild, ~10 min
Where the 34 seconds go
  • Click confirmed, decision logged~3 s
  • Router pointed at slot A~6 s
  • In-flight requests on slot B drain~21 s
  • Slot A verified serving~4 s
Phase split illustrative; the 34 s total is the recorded time.
Architecture

From a commit to production traffic, with the way back kept warm

The gate went in before the automation: for two weeks the pipeline only refused things, and nothing deployed on its own until build, test and lint were blocking merges.

  1. 01 · Trigger
    Pull request / mergeEvery commit runs the pipeline; Terraform plans run on the same trigger and post the infrastructure diff into the review.
  2. 02 · Gate
    Build, test, lintRequired status checks block the merge, before main can break.
  3. 03 · Artifact
    One image per commitTagged with its commit and never rebuilt per environment, because a rebuild is a new artifact.
  4. 04 · Promote
    Staging → productionThe same image moves through environments unchanged; configuration arrives as environment variables at run time.
  5. 05 · Serve
    Blue-green routingThe previous version stays running and healthy, so rollback is a routing change: seconds, where a rebuild takes ten minutes.

So a bad release stays a non-event

Blocked early, identical everywhere, reversible in seconds

Broken code stops at the merge

Build, unit tests, integration tests and lint are required checks on the pull request, so a failure blocks the merge instead of arriving as a notification after main is broken. Terraform plans post their diff into the same review.

Staging predicts production

One image is built per commit and promoted unchanged, with configuration injected at run time. The thing tested is byte-for-byte the thing that ships, which removes environment drift by construction.

A way back that has been used

The previous version stays running and healthy, so rollback is a routing change. It was proven by shipping a deliberately broken build in a quiet window, and has since been timed in two real incidents.

Is every deploy still a one-way door? Scope your build in three minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.