Better Coaching

Replacing Manual SFTP Releases with a Test-Gated CI/CD Pipeline

Stack

  • Docker
  • GitHub Actions
  • CapRover
  • Node.js
  • Express
  • Jest
  • Supertest
On this page

Key results

Deployment process
SFTP → CI/CD
Isolated environments
3
Production releases
Test-gated
Frontend + backend
Dockerized
Release cycle
~2 weeks

Problem

Better Coaching is a multi-tenant SaaS platform that football clubs and academies use to manage coaching, players and training content.

When the team inherited the platform, releases were manual. Changes were copied to the server over SFTP, and no pipeline validated or deployed them. Every release depended on the developer doing it knowing:

  • which files had changed;
  • which environment needed updating;
  • which configuration belonged where;
  • whether every required change had been transferred;
  • what to check after the deployment.

The process worked, but it was fragile. A forgotten file or a configuration difference could break a release, and there was no simple rollback. Because the environments had drifted apart, a change that worked in development wasn’t guaranteed to behave the same way in production.

The goal wasn’t just faster deployments. The release process needed to be:

  • repeatable;
  • less dependent on any one developer’s knowledge;
  • safe across environments;
  • validated before reaching production;
  • compatible with the ongoing modernization of the frontend and backend.

My role

I was the team lead on this work. I set the direction for how the platform should be built, tested and released, and led the team through containerizing the applications, splitting the environments, automating deployments and making integration tests a requirement for every release.

I also introduced the release rhythm the team settled into, so shipping became part of the normal engineering workflow instead of a separate manual operation.

Technical approach

Standardizing the applications with Docker

The first step was reducing the differences between environments. We containerized both the frontend and the backend with Docker, so the applications ran from a defined container configuration instead of manually configured servers.

The same build could now move through development, staging and production without depending on machine-specific setup, which gave the rest of the pipeline a reproducible foundation.

Separating development, staging and production

We split the platform into three environments. Development is where ongoing work is integrated and tested. Staging is a production-like environment for validating a release before customers see it. Production stays isolated from day-to-day development.

Branch-based deployment rules connect each branch to its environment:

  1. develop
  2. development
  1. staging
  2. staging
  1. main
  2. production

Automating deployment with GitHub Actions and CapRover

GitHub Actions runs the delivery workflow and CapRover handles application deployment:

  1. Pull request
  2. Automated checks
  3. Merge
  4. GitHub Actions
  5. CapRover
  6. Target environment

Deployments are driven by the repository and its branch state, not by someone copying files to a server.

Adding integration tests with Jest and Supertest

We added integration tests to the Node.js/Express backend with Jest and Supertest, in a dedicated structure alongside the application. Instead of testing functions in isolation, they exercise the API through realistic request flows. Coverage includes:

  • routes and middleware;
  • authentication and authorization;
  • validation;
  • controllers and services;
  • database operations;
  • HTTP responses and error cases.

Testing against a production-like database

The tests run against a separate staging/test database built on the production structure, never against production itself. They exercise real database interactions and behave more like the deployed application, while production data stays protected.

Making tests the release gate

The integration suite runs on every pull request, and code can’t be merged until the required checks pass. Tests stopped being something a developer might remember to run and became an enforced step. In effect, the pipeline asks one question before the other:

“Does the application still behave correctly?” comes before “Can we deploy it?”

If the answer to the first question is no, the second step never happens.

A predictable release rhythm

Changes now move through development and staging before production, on a roughly two-week cycle. Instead of coordinating an SFTP upload, release work means:

  • reviewing changes;
  • checking automated test results;
  • validating the release candidate on staging;
  • promoting the release when it’s ready.

Decisions & trade-offs

Automating deployment wasn’t enough

Early on, automating deployment looked like the biggest win. But it raised an obvious problem:

Automating an unsafe deployment process only lets you ship broken software faster.

A successful Docker build or deployment doesn’t prove the application still behaves correctly, and the platform already had complex authentication, permissions, data operations and business logic. So we treated the project as building a software-delivery pipeline, not just automating deployments, with verification required before anything could be released.

“Can be deployed” vs. “ready to be deployed”

A green deployment only tells you the software can be deployed. Passing integration tests tell you it’s ready to be deployed. Keeping those two questions separate, and enforcing both, is what made the pipeline a safety layer rather than just a faster way to release.

A staging copy instead of production

Testing against a separate database built on the production structure means maintaining a second database. In exchange, tests get realistic data behavior without any risk to customer data, which made it the clear choice.

Results

No more manual deployments

SFTP-based releases are gone. Deployments are triggered from version control and run through GitHub Actions and CapRover, the same way regardless of which developer is releasing.

Test-gated production releases

A change can no longer reach production just because the deployment itself succeeds. The integration tests have to pass first.

Controlled environment differences

Docker and separate environments removed most of the reliance on machine-specific configuration. Issues are easier to reproduce, and changes are easier to validate before users see them.

Staging as a real safety layer

Changes are validated under production-like conditions before they are promoted, so environment-specific problems surface on staging instead of after a production release.

Less dependence on individual developers

The release steps that used to live in someone’s head are now encoded in:

  • Docker configuration;
  • GitHub Actions workflows;
  • environment rules;
  • automated tests;
  • CapRover deployment configuration.

That makes delivery more consistent and easier for the whole team to maintain.

A safer foundation for continuous delivery

The biggest change wasn’t swapping SFTP for GitHub Actions. It was moving from a manual procedure to a structured delivery system where every change follows the same path:

  1. Developed
  2. Reviewed
  3. Tested
  4. Validated on staging
  5. Deployed

That gives Better Coaching a stronger base for shipping new features with less avoidable deployment risk.