Back to cheat sheets

DevOps & CI

Docker

A to-the-point review of the Docker an SDET gets asked about: images vs containers, the Dockerfile & layer caching, the container lifecycle, volumes & networking, Docker Compose, and — the part that wins the interview — using containers for reproducible, ephemeral test environments and CI. Short explanations, short examples.

01What Docker Is — Containers vs VMs

A container packages an app with its dependencies so it runs the same everywhere. It shares the host OS kernel and isolates only the process, filesystem, and network — so it’s far lighter than a VM.

ContainerVirtual Machine
Isolatesprocess (shares host kernel)full OS (own kernel)
Size / startMBs, secondsGBs, minutes
Overheadnear-nativehypervisor

“Works on my machine” disappears: the image is the environment, so dev, CI, and prod run byte-identical dependencies.

For an SDET: the pitch is reproducibility — spin up the exact same browser/driver/DB stack locally and in CI, throw it away after, and never debug environment drift again.

02Images vs Containers & Layers

  • Image — a read-only template (the blueprint). Built from a Dockerfile, tagged like myapp:1.2.
  • Container — a running (or stopped) instance of an image, with a thin writable layer on top.
  • Registry — where images live (Docker Hub, ECR, GHCR); push/pull moves them.

An image is a stack of layers, one per build instruction. Layers are cached and shared between images — so ordering your Dockerfile well is the main build-speed lever.

Image vs container, in one line: the image is the class, the container is the object — one image, many containers.

03The Dockerfile & Build Caching

A Dockerfile is the recipe for an image. Each instruction adds a layer.

FROM node:20-alpine            # base image
WORKDIR /app
COPY package*.json ./          # copy deps manifest FIRST
RUN npm ci                     # cached unless package*.json changes
COPY . .                       # then the source
EXPOSE 3000
CMD ["node", "server.js"]      # default process
  • Cache order matters: copy the dependency manifest and install before copying source, so a code change doesn’t bust the (slow) dependency layer.
  • CMD vs ENTRYPOINTCMD sets the default command (easily overridden); ENTRYPOINT sets the fixed executable, with CMD as its default args.
  • Multi-stage builds — build in a heavy image, copy only the artifact into a tiny runtime image; smaller, safer final images.
  • Use a .dockerignore to keep node_modules/.git out of the build context.
Common trap: putting COPY . . before RUN npm ci means every source edit reinstalls all dependencies — the #1 slow-build mistake.

04Running Containers

The everyday commands an SDET should be fluent in:

CommandDoes
docker runcreate + start a container from an image
docker ps / ps -alist running / all containers
docker exec -itrun a command (e.g. a shell) inside a running container
docker logsview a container’s stdout/stderr
docker stop / rmstop / delete a container
docker build -tbuild an image from a Dockerfile
docker run -d --name web -p 8080:3000 -e NODE_ENV=test myapp:1.2
#        |    |            |             |
#   detached  name    host:container  env var

docker exec -it web sh        # shell into it
docker logs -f web            # follow the logs
docker rm -f web             # stop + remove
Ports: -p 8080:3000 maps host 8080 → container 3000. Without publishing a port the service isn’t reachable from the host.

05Volumes & Data Persistence

A container’s writable layer is ephemeral — delete the container and its data is gone. Volumes persist data outside the container lifecycle.

  • Named volume — Docker-managed storage (-v mydata:/var/lib/postgresql/data); the right choice for databases.
  • Bind mount — maps a host directory into the container (-v $(pwd):/app); great for live-reloading source in dev.
  • tmpfs — in-memory only; fast and wiped on stop.
For tests: bind-mount your test code in for fast iteration, but keep test data ephemeral so every run starts from a clean, known state.

06Networking

  • bridge (default) — containers on the same user-defined bridge network reach each other by name (built-in DNS).
  • host — share the host’s network stack (no port mapping, less isolation).
  • none — no networking.

Inside a Compose network a service connects to another by its service name as the hostname — e.g. the app reaches the DB at postgres:5432, not localhost.

Classic gotcha: a container’s localhost is the container itself, not the host or another container — use the service/container name (or, from inside a container to the host, host.docker.internal).

07Docker Compose

Compose defines a multi-container app in one YAML file and brings it all up with a single command — perfect for a test stack (app + DB + mock server).

services:
  app:
    build: .
    ports: ["8080:3000"]
    environment:
      DB_URL: postgres://db:5432/test
    depends_on: [db]
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: test
    tmpfs: ["/var/lib/postgresql/data"]   # ephemeral test DB
  • docker compose up -d / down — start / tear down the whole stack.
  • depends_on controls start order (not readiness — add a healthcheck for that).
  • One network is created automatically, so services resolve each other by name.

08Docker for Testing & CI

This is where containers earn their keep for an SDET — disposable, identical environments on every run.

  • Ephemeral dependencies — spin up a real Postgres/Redis/Kafka in a container for integration tests instead of mocking, then throw it away.
  • Testcontainers — a library (Java/.NET/Python/Node) that starts containers from your test code and tears them down after, with dynamic ports — no leftover state.
  • Selenium/Playwright in Docker — official browser images and Selenium Grid containers give consistent, headless browsers in CI.
  • CI — GitHub Actions / GitLab run jobs inside containers (services: / container:), so the build environment is version-pinned and reproducible.
// Testcontainers (Java) — a throwaway Postgres for one test class
@Container
static PostgreSQLContainer<?> db =
    new PostgreSQLContainer<>("postgres:16");
// db.getJdbcUrl() gives a dynamic URL; container is removed after tests
Say this: Testcontainers gives you real dependencies with the isolation of unit tests — no shared staging DB, no flaky cross-test contamination.

09Rapid-Fire Q&A

Reveal each answer to self-check, then test yourself with the quiz.

Container vs VM?

A container shares the host kernel and isolates just the process — MBs and seconds; a VM virtualizes a full OS with its own kernel — GBs and minutes.

Image vs container?

The image is a read-only template (the class); a container is a running instance of it (the object) with a writable layer on top.

Why order Dockerfile instructions carefully?

Each instruction is a cached layer; copy the dependency manifest and install before copying source so code changes don’t bust the dependency layer.

CMD vs ENTRYPOINT?

CMD sets the default command (easily overridden at run); ENTRYPOINT sets the fixed executable, with CMD supplying its default arguments.

How do you persist data?

Use a volume (named volume for DBs, bind mount for live source) — the container’s own writable layer is deleted with the container.

How do two containers talk?

On a shared user-defined/Compose network they resolve each other by service or container name via Docker’s built-in DNS — not localhost.

What does -p 8080:3000 do?

Publishes (maps) host port 8080 to container port 3000 so the service is reachable from the host.

What is a multi-stage build?

Build in a heavy image, then COPY only the artifact into a small runtime image — smaller, more secure final images.

What is Docker Compose for?

Defining and running a multi-container app (app + DB + mocks) from one YAML file with a single up/down.

How does Docker help testing?

Reproducible, disposable environments — real dependencies via Testcontainers and consistent browsers in CI, with clean state every run.

Image too big — what do you do?

Use a slim/alpine base, multi-stage builds, a .dockerignore, and combine RUN steps to cut layers.

depends_on guarantees readiness?

No — it only controls start order; add a healthcheck (or retry in the app) to wait until the dependency is actually ready.