We use cookies to enhance your experience on the site
CodeWorlds

Main project - containerisation and CI/CD pipeline

The whole module consisted of separate tools: the image, Compose, the pipeline, secrets, metrics, tracing, the infrastructure plan. Now we gather them into one configuration - and along the way recall why each decision looks the way it does. Those justifications are what separate a copied configuration from an understood one.

The image - two stages

1FROM node:20-alpine AS builder
2WORKDIR /app
3COPY package.json yarn.lock ./
4RUN yarn install --frozen-lockfile
5COPY . .
6RUN yarn build
7
8FROM node:20-alpine AS production
9WORKDIR /app
10RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
11COPY --from=builder /app/dist ./dist
12COPY --from=builder /app/node_modules ./node_modules
13USER nestjs
14EXPOSE 4000
15HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
16  CMD curl -f http://localhost:4000/health || exit 1
17CMD ["node", "dist/main"]

Three decisions in this file are worth being able to justify.

COPY --from=builder
carries the result from the first stage into the second. The order of its parts is fixed:
COPY
,
--from=builder
,
/app/dist
(from),
./dist
(to). Thanks to it the production image contains no compiler, no sources and no development dependencies - only the result.

USER nestjs
- we create a non-root user to limit the potential damage in case of a breach. Not to make the container start faster, not to make the image smaller, not to enable multithreading. A process running as root inside a container, having escaped the isolation, is root on the host as well.

HEALTHCHECK
tells Docker how to check whether the application is really alive, rather than merely whether the process exists. The order:
healthcheck:
,
test: curl -f http://localhost:4000/health
,
interval: 30s
,
retries: 3
.

Compose - three services

In the Compose file, check the two things most often confused.

The

api
container connects to the database by service name:
mongodb:27017
- not by the container's IP address, not through
localhost:27017
and not through external DNS.
localhost
inside a container is that same container, so it would point at the API rather than the database.

The

-v
flag in
docker compose down -v
removes the volumes along with their data.
It does not enable verbose logging, does not validate the file and does not print the version. That command deletes the database - use it deliberately.

Deployment - the order

A change's full road to production has four stages:

  1. Push the code to the main branch.
  2. CI: lint, test, build.
  3. Docker build & push image.
  4. Smoke tests and monitoring after deployment.

The fourth stage is often skipped, and it is the one that separates "deployed" from "working". Smoke tests are a handful of checks of the most important paths right after deployment - does the page open, does login work, does health respond. The pipeline may have gone green and the application still not come up, because an environment variable was missing that nobody needed on CI.

Kubernetes - two commands

Once the image is ready, the cluster accepts a description of resources from a file:

1kubectl apply -f deployment.yaml

The order of parts:

kubectl
,
apply
,
-f
,
deployment.yaml
. The
-f
flag names the file with the description;
apply
brings the cluster to the state written in it - creating what is missing and changing what differs.

Scaling is a separate command:

1kubectl scale deployment roman-api --replicas=5

Here the order reads:

kubectl scale
,
deployment roman-api
,
--replicas=5
. Five replicas means five parallel copies of the application, among which the cluster spreads the traffic.

Terraform - modules

One last thing from the infrastructure layer. Modules in Terraform are reusable blocks of infrastructure that can be parameterised - not plugins for talking to a cloud (those are providers), not unit tests and not log files.

The point is the same as with a function in code: you describe "an application server with a database and a network" once, then call it three times for the test, staging and production environments, changing only the parameters.

The project checklist

Gather the set and check every point:

  • Docker - a multi-stage image, a non-root user,
    HEALTHCHECK
    ,
  • Docker Compose - three services, connections by name, a volume for the database's data,
    depends_on
    with
    service_healthy
    ,
  • CI/CD - a workflow on
    push
    and
    pull_request
    , jobs in the order lint → build → test → docker, secrets in the repository settings,
  • Secrets - no password in the repository;
    .env
    locally only,
  • Observability - a
    /metrics
    endpoint, RED metrics, tracing with
    span.end()
    in
    finally
    ,
  • Infrastructure as Code - the plan in
    .tf
    files, the state in a remote backend, not in Git.

Summary

The campaign is ready to march:

  • COPY --from=builder
    carries the build result across:
    COPY
    ,
    --from=builder
    , source, destination,
  • we create a non-root user to limit damage in case of a breach - not for speed or image size,
  • HEALTHCHECK
    in order:
    healthcheck:
    ,
    test:
    ,
    interval:
    ,
    retries:
    ,
  • containers connect by service name (
    mongodb:27017
    ), not by IP and not through
    localhost
    ,
  • docker compose down -v
    removes the volumes along with their data
    ,
  • deployment order: push → CI (lint, test, build) → docker build & push → smoke tests and monitoring,
  • kubectl
    apply
    -f
    deployment.yaml
    feeds a description into the cluster,
  • kubectl scale
    deployment name
    --replicas=5
    changes the number of copies,
  • Terraform modules are reusable, parameterisable blocks of infrastructure - not providers, not tests, not logs.

This is the module's last lesson. You can now build an image, describe an environment, push a change through a pipeline, hide the secrets, see what is happening in production, and reproduce the whole infrastructure from a file. For now remember: every element of this configuration has a justification - and it is knowing them, not the file itself, that carries over to the next project.

Go to CodeWorlds