Home
Backend from First Principles / Module 23 — DevOps for Backend Engineers

DevOps for Backend Engineers

Docker basics, Kubernetes essentials, the CI/CD pipeline that catches bugs early.


What happens after the code is written

Most of this series has been about writing backend code. This module is about everything that happens to that code after you write it — the journey from a commit on your laptop to a process serving real users.

That journey is the domain of DevOps, and the word causes confusion, so let us be precise. DevOps is not a job title and not a tool. It is the practice of treating building software and running software as one continuous responsibility instead of two departments that throw work over a wall. The "throw it over the wall" model — developers write code, a separate operations team runs it, neither fully understands the other's world — produced slow, fragile, blame-prone releases. DevOps is the correction: the people who build the software are also accountable for getting it deployed and keeping it healthy.

You do not need to become a DevOps specialist. There is a deep field here — the dedicated DevOps & Cloud Engineering series on this site covers it properly. But a backend engineer who cannot deploy their own service, cannot read why a container will not start, and cannot follow a CI pipeline is dependent on someone else for the last and most important mile.

This module is the backend engineer's slice of that field — the parts you will touch personally — organised as the three stages your code passes through: it gets packaged (containers), it gets run and scaled (orchestration), and it gets delivered automatically (CI/CD). Each stage gets a section. Each is also a full series of its own in the DevOps track; the goal here is the working mental model, not mastery.


Packaging — containers

The oldest deployment problem has a name: "works on my machine." Code runs perfectly on your laptop and fails on the server, because the two environments differ — a different language runtime version, a missing system library, a different operating system, a config file that exists in one place and not the other.

Containers solve this by changing what you ship. Instead of shipping just your code and hoping the destination has the right environment, you package your code together with its entire environment — the runtime, the libraries, the system dependencies, the lot — into one sealed, portable unit called a container image. That image then runs identically everywhere, because it carries its own world with it. Your laptop, the test server, production — all run the exact same image, so "works on my machine" becomes "works on every machine," because they are now genuinely the same machine.

A container is not a virtual machine. A VM virtualises an entire operating system and is heavy — gigabytes, slow to boot. A container shares the host's operating-system kernel and isolates only what is above it. The result is light: megabytes, and it starts in a fraction of a second. That lightness is what makes containers practical to start and stop constantly.

You describe the image in a Dockerfile — a recipe, read top to bottom:

Dockerfile
FROM node:20-slim                # start from a base image with Node 20
WORKDIR /app
COPY package*.json ./            # copy dependency manifests first
RUN npm ci --omit=dev            # install dependencies (cached layer)
COPY . .                         # copy the application source
EXPOSE 3000
CMD ["node", "server.js"]         # the command that runs on start

Notice the dependency manifests are copied and installed before the application source. Docker caches each step as a layer; because dependencies change far less often than source code, putting them first means an ordinary code change reuses the cached dependency layer and rebuilds in seconds. That ordering is the most useful container habit a backend engineer can have.

Two terms to keep straight: an image is the built, static package (the recipe baked); a container is a running instance of an image (the dish being served). One image, many containers — which is exactly what makes scaling possible. (DevOps Lesson 10 goes deep on Docker.)


Running and scaling — orchestration

One container is easy: docker run and it is up. Production is not one container. It is dozens or hundreds of containers, across several machines, and that raises a pile of questions one docker run cannot answer:

These questions are the job of an orchestrator, and the dominant one is Kubernetes. An orchestrator's core idea is declarative desired state. You do not script the steps — "start a container, check it, start another." You declare the end state you want: "I want 5 copies of this image running, each healthy, reachable at this address." Kubernetes then works continuously to make reality match that declaration and keep it matched.

Text
   you declare:  "5 healthy copies of image v2"
        │
        ▼
   ┌──────────────────────────────────────────┐
   │  Kubernetes — reconciliation loop         │
   │  observe reality  →  compare to desired   │
   │  →  act to close the gap  →  repeat       │
   └──────────────────────────────────────────┘
        │
        ▼
   a container died?    → start a replacement
   only 4 running?      → start a 5th
   a machine vanished?  → reschedule its work elsewhere

That reconciliation loop is the whole concept. A crash is not an incident requiring a human — Kubernetes observes "4 running, 5 wanted" and starts one. This is self-healing, and it is why orchestrated systems can be operated by a small team.

This is also exactly where the twelve-factor discipline from Module 22 pays off concretely. Kubernetes assumes your app is stateless, so it can freely create and destroy copies. It assumes your app reads config from the environment, so it can inject per-deploy settings. It assumes your app shuts down gracefully on SIGTERM (Module 17), so it can move and upgrade containers without breaking requests. A twelve-factor app drops into an orchestrator naturally; an app that hoards local state fights it at every turn. The backend code and the deployment platform are designed for each other. (DevOps Lesson 13 covers Kubernetes properly.)


Delivering automatically — CI/CD

The final stage is the path from a commit to running in production — and the goal is for that path to be automated, because a path that depends on a human remembering steps is a path that eventually breaks.

The automation has two halves, CI and CD.

Continuous Integration (CI) runs every time code is pushed. On each commit, an automated pipeline checks the work before it can be merged: it runs the test suite, runs the linter, builds the container image, maybe runs security scans. If anything fails, the merge is blocked. CI's purpose is to catch broken code at the door — at the moment of the commit, where it is cheap to fix and the author still has full context — rather than days later in production.

Continuous Delivery / Deployment (CD) takes the code that passed CI and moves it toward production: deploy to a staging environment, run further checks, then release to production. The distinction in the two D's: continuous Delivery keeps the code always ready to release but a human presses the final button; continuous Deployment releases automatically with no human gate. Which you want depends on how much you trust your test coverage and how high the stakes are.

Text
   commit ─► CI: test · lint · build image · scan
                       │
                  pass │ fail ─► merge blocked, author fixes it
                       ▼
   CD: deploy to staging ─► verify ─► deploy to production

A pipeline is defined as a file in your repository, version-controlled alongside the code:

YAML
# .github/workflows/ci.yml  — runs on every push
name: CI
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: npm run lint

Why this matters to you, a backend engineer, specifically: CI/CD is what makes graceful shutdown worth implementing and statelessness worth maintaining. It is the machine that exercises all of it. A deploy stops being a tense, manual, Friday-afternoon-avoided event and becomes a routine, boring, many-times-a-day non-event — which is the entire point. (DevOps Lesson 7 covers pipelines in depth.)


How far a backend engineer should go

The honest scope question: how much of this should a backend engineer actually own, and where does it stop being your job?

What you genuinely should be able to do yourself, without depending on anyone:

That competence is not optional anymore. A backend engineer who writes code and then hands it to "someone who does deployment" has given away ownership of whether their work actually runs — and will be slow and dependent every time something breaks in the last mile.

Where it legitimately stops: designing the cluster's networking, capacity-planning the node pool, building the organisation's deployment platform, writing the Terraform that provisions the cloud account, setting up the service mesh — that is genuine platform / DevOps engineering, a specialist discipline with real depth. You should understand it well enough to be a competent user of what those specialists build, and to talk to them precisely. You do not need to be one.

The mental model to keep: your code gets packaged (a container image), run and scaled (an orchestrator), and delivered (a CI/CD pipeline). Know that path well enough to walk it for your own service and to debug it when it breaks. For everything past that boundary — the deep version of each stage — the DevOps & Cloud Engineering series on this site is where to go next. This module is the bridge; that series is the territory.


Source & Credit

The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.

⁂ Back to all modules