VickyApp
Open navigation menu

Case studyInfrastructure & platform

A container platform that provisions client environments automatically

One shared platform replaced hand-built servers, so a new service line can grow without adding work to the operations team.

  • Infrastructure
  • Automation
  • Platform
Role
System Administrator & DevOps Engineer
Period
2024 — present
Status
Ongoing
Used
  • Docker Swarm
  • Traefik
  • Portainer
  • n8n
  • GitHub Actions
  • Linux

Context and problem

The company was opening a new service line: running client applications on our own infrastructure instead of handing over a server for the client to manage. Before this work, every request meant building one server, configuring it by hand, and handing over the result manually. That approach capped how many clients we could serve at once and made quality depend on who happened to be working that day.

What made it hard

  • Preparing an environment for a new client took hours per client and was done entirely by hand.
  • Environments drifted apart, so the same visible problem did not always trace back to the same cause.
  • There was no consistent release path. Changes were pushed by logging into servers one at a time, and the history of those changes was not recorded.
  • Client applications ran side by side, so one misbehaving environment could disturb the others.

Constraints that could not move

  • The operations team stayed small. Capacity could not grow with the client count.
  • Clients share one platform and are not allowed to interfere with each other.
  • The service line has to be maintained by people who did not build it, so decisions had to be written down rather than kept in one head.
  • Adding capacity had to happen without disrupting clients already running.

My role and responsibilities

  • Designed and built an eleven-node container cluster from scratch with clear role separation: one node for inbound traffic, three control nodes, and seven worker nodes.
  • Built automated client provisioning by connecting the platform management API to an automation workflow, so a new application request no longer involved manual setup.
  • Built a release path from the code repository to the servers, replacing changes pushed through direct server access.
  • Kept the platform running and handled production incidents alongside the operations team.

How I went about it

  1. 01

    Writing down the work that repeated most

    What happened for every new client was recorded first, then split into what genuinely differed per client and what could be made identical. The identical part is what was worth automating.

  2. 02

    Settling on one entry point and one release path

    All traffic was concentrated at a single point so adding a client would not mean editing configuration in many places. Releases were sent from the repository rather than from servers, which also left a record behind.

  3. 03

    Automating client environment provisioning

    The automation flow calls the platform API to create and register a new environment, then hands it over with no manual step in the middle.

  4. 04

    Testing with a real client before adding capacity

    The platform ran for a first client before more nodes were added, so design weaknesses surfaced while the blast radius was still small.

This service line started from a simple question: how do we run a client application on our own infrastructure without treating every client as a separate project. Before this work, the answer was to build a server, configure it by hand, and hand over the result. That worked for a handful of clients and stopped working as soon as the count grew.

Turning every client request into the same job

The first step was not adding machines. It was writing down what actually happened each time a client arrived. Most of it turned out to be identical: bring up an environment, make the application reachable, hand over access. Only a small part differed between clients. The identical part became one repeatable job, while the rest was left adjustable on purpose.

Drawing tenant boundaries in the topology

Clients share one platform, so the boundaries between them have to start at the machine layout. One machine carries all inbound traffic, three decide where workloads run, and the rest serve applications. That split means adding a client does not require edits in many places at once, and a misbehaving environment is easier to contain.

Closing the last manual step

Once environments were consistent, provisioning was still hands-on. That step was connected to an automation flow, so a new environment is created and registered by the system before it is handed to the client. Releases moved out of the servers and into the code repository, which also left a record of who changed what.

Results in short

The platform runs as an eleven-node cluster: one ingress node, three control nodes, seven workers, and around 300 active containers inside it. Provisioning a new client environment is automated with no manual step, and application releases ship through a pipeline instead of direct server access. What the team feels most is that the service line can keep growing without growing the team.

Outcomes

  • An eleven-node container cluster runs in production: one ingress node, three control nodes, and seven worker nodes.

    This is the topology that was actually installed and operated, not a design on paper.

    verified

  • Around 300 active containers run on that platform.

    A production figure, not an estimate.

    verified

  • Provisioning an application environment for a new client is automated once the platform management API is connected to the automation workflow.

    verified

  • Application releases ship through a pipeline, replacing manual deployment over server access.

    verified

  • Separating node roles made the boundary between clients clearer and cut down the repetitive work that still had to be done by hand.

Technical detail

This part only matters if you want to judge the technical side. Everything above is enough to understand the outcome.

Open the implementation detail3 decisions

An eleven-node Docker Swarm cluster with Traefik as ingress on a dedicated node, three managers, and seven workers. Portainer serves as the management API and UI, n8n orchestrates client provisioning, and GitHub Actions is the release path.

Giving ingress a dedicated node instead of running it on a node that also serves workloads.
This node is the single point of entry, so it must not compete for resources with client applications. The cost is that a full node never runs workloads, which reduces effective application capacity by one node.
Choosing Docker Swarm over Kubernetes.
The platform has to be maintainable by a smaller team than Kubernetes usually demands, and the early workload did not require orchestration that broad. What was given up is a richer scheduling model and ecosystem, so more demanding requirements later will force a revisit.
Automating client provisioning through the management API rather than per-client scripts.
Per-client scripts are quick to write but multiply with every new client. This approach ties the process to an interface the platform provides, so platform version changes have to be followed rather than ignored.

The client count grew without headcount growth, which turned documentation into part of the work rather than an extra. What I would do differently is set per-client resource limits from the start, because enforcing them on a cluster that is already populated is considerably harder.

What I learned

  • Useful automation starts from the work that repeats most often, not from the tool that looks most interesting.
  • A single entry point turns onboarding a client into a small change, which matters more than adding machine capacity.
  • A design only its author understands becomes a problem as soon as the team grows, so decision notes have to be written when the decision is made.