Skip to content
Backend & systems

35 Docker, CI/CD, and Linux Interview Questions and Answers

This focused guide turns RecallDeck’s curated Docker, CI/CD, and Linux material into 35 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

38 min read35 detailed answersUpdated
What to remember

Organize the answer around ownership, limits, failure, and recovery. Definitions become interview-ready when they survive a concrete production scenario.

Question set

35 detailed answers

A recorded source review is available for 35 of 35 answers. Each reviewed answer lists its date and scope.

01

What is Docker and what problem does it solve?

Short answer: Docker packages an application and its user-space dependencies in an image, then runs an isolated process from that image. This reduces differences between development, CI and production.

For a Go service, the image can contain the compiled binary, CA certificates and any needed shared libraries. The host still supplies a compatible kernel and CPU architecture; configuration, credentials, resource limits and external services are supplied separately.

docker build -t myapp:1.0 .
docker run -d -p 127.0.0.1:8000:8000 --name myapp myapp:1.0
docker logs -f myapp

The application must listen on the container interface, such as :8000. This example publishes only to the host loopback address for local use.

Limit: the same image does not guarantee identical behavior on every machine. Linux containers on macOS/Windows normally run inside a Linux VM; CPU architecture, kernel features, networking and external configuration still matter. Build the required platform variants and test the actual deployment environment.

Sources: Reference 1, Reference 2.

02

Container vs virtual machine

Short answer: A VM virtualizes hardware and drags along a full guest OS with its own kernel. A container virtualizes the OS and shares the host's kernel, isolating only at the process level. Sharing a kernel often lowers startup and memory overhead; size, speed and isolation depend on the chosen configuration.

In depth:

Virtual machine:                     Container:
┌───────────────────────┐            ┌───────────────────────┐
│   App A   │   App B    │            │  App A   │   App B     │
│  Bins/Libs│ Bins/Libs  │            │ Bins/Libs│  Bins/Libs  │
│  Guest OS │  Guest OS  │            ├──────────┴─────────────┤
│  (kernel) │  (kernel)  │            │   Docker Engine        │
├───────────┴────────────┤           ├────────────────────────┤
│      Hypervisor        │            │   Host OS (shared kernel) │
├────────────────────────┤           ├────────────────────────┤
│      Host OS / hardware │           │      Hardware          │
└────────────────────────┘           └────────────────────────┘
Criterion VM Container
Kernel own, guest shared with host
Size includes a guest OS depends on image contents
Startup includes guest boot usually starts a process directly
Isolation strong (at the hardware level) weaker (at the process/kernel level)
Overhead high low

Container isolation is built on Linux kernel mechanisms:

  • namespaces — isolating "what the container sees": processes (PID), network, mount points, users, hostname.
  • cgroups (control groups) — limiting "how much the container can consume": CPU, memory, IO.
# Limit a container's resources via cgroups
docker run --memory=512m --cpus=1.5 myapp:1.0

⚠️ Gotcha: "weaker isolation" really matters for security. A vulnerability in the kernel can affect all containers on the host (escape). That's why you don't run trusted and untrusted code in containers on the same kernel without extra measures (gVisor, separate VMs). Without user-namespace remapping, container UID 0 maps to host UID 0, but namespaces and capabilities still restrict access. Host bind mounts and privileged mode can weaken that boundary; run as an unprivileged user and consider rootless mode or user namespaces.

Sources: Reference 1.

03

Image vs container

Short answer: An image is an immutable template, a "snapshot" of a filesystem with an application. A container is an instance of an image that can be running or stopped — a process with a thin writable layer on top of the image. Analogy: the image is the class, the container is the object.

In depth:

  • Image is read-only, made of layers. One image — many containers.
  • Container is the image + a thin writable layer on top. Writes to the container filesystem outside mounted paths go to this layer; volumes, bind mounts and tmpfs have their own storage. When the container is removed, the layer disappears (unless the data is moved out to a volume).
docker images          # list images
docker ps -a           # all containers (including stopped ones)

docker run myapp       # create and run a container
docker stop <id>       # stop
docker start <id>      # start again (same container, same writable layer)
docker rm <id>         # remove the container (the writable layer is deleted)
docker rmi myapp       # remove the image

⚠️ Gotcha: data written inside the container (not into a volume) disappears on docker rm. A common junior mistake is to put a database in the container's filesystem and be surprised that everything is gone after recreating it. Containers are ephemeral.

Sources: Reference 1, Reference 2.

04

Image layers and caching

Short answer: An image is built from layers — filesystem-changing instructions such as RUN and COPY create filesystem changes, while instructions such as CMD and ENV primarily set image metadata. Docker caches layers: on rebuild, unchanged layers are taken from the cache. That's why the order of instructions is critical — put rarely changing layers above frequently changing ones.

In depth:

A layer's cache is invalidated as soon as the instruction or its input changes (for example, a file that's copied via COPY). All subsequent layers are rebuilt too.

Bad — any code change breaks the dependency-install cache:

FROM python:3.12-slim
WORKDIR /app
COPY . .                          # code changes often → layer invalidated
RUN pip install -r requirements.txt   # dependencies reinstalled every time 😱

Good — dependencies first, then code:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .            # changes rarely
RUN pip install -r requirements.txt   # cached until requirements.txt changes ✅
COPY . .                           # code changes often — but this is the last layer

The same trick for Node:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

⚠️ Gotcha: combine related commands into a single RUN with &&, otherwise apt-get update in one layer and apt-get install in another can drift apart (a cached update + a fresh install → stale package indexes). Measure the bytes added by layers; layer count alone does not determine image size. Changes to remote package repositories also do not automatically invalidate a cached RUN.

Sources: Reference 1, Reference 2.

05

Main Dockerfile instructions

Short answer: FROM — base image, WORKDIR — working directory, COPY/ADD — copying files, RUN — execute a command at build time, ENV — environment variables, ARG — build-time arguments (not a secret store), EXPOSE — document a port, CMD/ENTRYPOINT — the container's startup command.

In depth:

# Base image (required, usually the first instruction)
FROM python:3.12-slim

# Metadata
LABEL maintainer="team@example.com"

# Build argument (available only at build time)
ARG APP_VERSION=1.0.0

# Environment variable (available both at build time and in the container's runtime)
ENV PYTHONUNBUFFERED=1 \
    APP_HOME=/app

# Working directory (created if missing; sets the default directory for subsequent RUN, CMD, ENTRYPOINT and relative COPY/ADD destinations)
WORKDIR $APP_HOME

# Copy files from the build context into the image
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

# Create an unprivileged user and switch to it
RUN useradd --create-home appuser
USER appuser

# Document the port (does NOT publish the port by itself!)
EXPOSE 8000

# Startup command
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]

Best practices:

  • Choose a supported base image version; tags are mutable. Pin a digest when exact image identity is required, with a process for security updates.
  • A .dockerignore so you don't drag in .git, node_modules, __pycache__.
  • Keep cache-friendly steps and remove temporary package files in the same RUN that created them. Fewer instructions alone do not guarantee a smaller image.
  • Don't run as root (USER).
  • Copy and install dependency manifests in a separate layer before copying application source. Then a source-only change reuses the cached dependency layer instead of running an expensive install again.

⚠️ Gotcha: EXPOSE 8000 doesn't publish anything to the outside — it's only documentation/metadata. To make the port reachable from the host, you need docker run -p 8000:8000. Also, use WORKDIR instead of RUN cd /app — cd only takes effect within a single RUN.

06

COPY vs ADD

Short answer: Prefer COPY for ordinary local files. ADD has extra behavior: it can unpack local tar archives and fetch remote sources. Choose it deliberately when that behavior is useful.

COPY ./src/ /app/src/
ADD app.tar.gz /app/

COPY --from=builder can also copy from a build stage or image. An ADD of a local archive may extract it; COPY preserves the archive as a file. Remote downloads are not inherently an anti-pattern: modern Dockerfile syntax supports ADD --checksum=sha256:<digest> for HTTP sources, allowing explicit integrity verification.

Decision: for a simple local copy, use COPY; for verified remote input, pin and validate the input; for more complex authenticated downloads, use a build secret mount with a suitable download command. A plain RUN curl ... does not automatically verify integrity either. Cache behavior and security depend on the instruction and its inputs, not just whether it is named COPY or ADD.

Sources: Reference 1, Reference 2.

07

CMD vs ENTRYPOINT

Short answer: ENTRYPOINT defines the default executable/command. CMD defines the default arguments (or the whole command), which are easy to override in docker run. They're often used together: ENTRYPOINT — the program, CMD — the arguments.

In depth:

If only CMD is set, it can be fully replaced in docker run:

CMD ["python", "app.py"]
docker run myapp                  # → python app.py
docker run myapp python other.py  # → python other.py (CMD replaced entirely)

If ENTRYPOINT is set, the arguments from docker run are appended to it:

ENTRYPOINT ["python"]
CMD ["app.py"]
docker run myapp            # → python app.py
docker run myapp other.py   # → python other.py (only the CMD part is replaced)

Exec form vs shell form (important!):

CMD ["python", "app.py"]    # exec form: PID 1 = python, signals reach it ✅
CMD python app.py            # shell form: PID 1 = /bin/sh -c, python is a child ⚠️

The exec form (a JSON array) is preferred because the application becomes PID 1 and correctly receives signals (SIGTERM on docker stop). In the shell form, the shell receives the signals, and the application may not stop gracefully.

⚠️ Gotcha: a shell wrapper that neither uses exec nor forwards signals can prevent graceful shutdown. Exec form avoids that wrapper, but the application still needs correct signal handling. docker stop uses the configured stop signal and grace period before SIGKILL. ENTRYPOINT can also be overridden with --entrypoint.

Sources: Reference 1.

08

ARG vs ENV

Short answer: ARG is a build-time variable, available only in the Dockerfile during docker build and not automatically added to the runtime environment; its value may still appear in image history, provenance or generated files. ENV is an environment variable, available both at build time and at runtime inside the container, and it's persisted in the image.

In depth:

ARG NODE_VERSION=20          # default value, overridden at build time
FROM node:${NODE_VERSION}

ARG BUILD_ENV                # no value → passed via --build-arg
ENV APP_ENV=production       # stays in the image and is visible to the process
docker build --build-arg BUILD_ENV=staging -t myapp .
docker run -e APP_ENV=dev myapp   # ENV can be overridden at run time

⚠️ Gotcha: don't pass secrets via ARG — even if the value is never stored in ENV, it can remain in the layer history and in docker history. For secrets at build time use --secret (BuildKit), and at runtime use environment variables via -e/--env-file or a secrets manager.

Sources: Reference 1, Reference 2.

09

Multi-stage builds

Short answer: A multi-stage build is several FROM stages in a single Dockerfile, where only the build result is copied into the final image, while heavy build tools (compilers, dev dependencies) stay in the intermediate stages. The main goal is to dramatically reduce the size and attack surface of the resulting image.

In depth:

If you ship the build stage, its compiler and build files remain in the image. A separate runtime stage can keep just the binary and runtime dependencies; measure actual image size.

# --- Build stage ---
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# --- Final stage ---
FROM alpine:3.20
COPY --from=builder /app/server /usr/local/bin/server
ENTRYPOINT ["server"]

For a frontend (build the static assets, serve via nginx):

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html

⚠️ Gotcha: the final image contains its own base image, changes made in its stage, and anything copied with COPY --from=...; it does not automatically inherit the builder filesystem. If you forget to copy a runtime dependency (for example, the CA certificates needed for HTTPS requests in scratch/alpine) — HTTPS verification or another dependent operation can fail at runtime even though the image built successfully.

The version tags shown illustrate the staging pattern, not a supported production baseline. Select maintained base versions matching the module and pin reviewed image digests for reproducibility; test runtime certificates, cgo and architecture requirements.

Sources: Reference 1.

10

How to reduce image size

Short answer: Use lightweight base images (alpine, slim, distroless), multi-stage builds, a .dockerignore, combine RUNs and clean the package manager cache in the same layer, and don't install dev dependencies in production.

In depth:

FROM python:3.12-slim       # example base; measure the selected image
WORKDIR /app
COPY requirements.txt .

RUN apt-get update \
 && apt-get install -y --no-install-recommends gcc \
 && pip install --no-cache-dir -r requirements.txt \
 && apt-get purge -y gcc \
 && rm -rf /var/lib/apt/lists/*   # clean up in the SAME layer, otherwise the weight stays

.dockerignore — don't drag unnecessary things into the build context:

.git
node_modules
__pycache__
*.log
.env
dist
.venv

Comparison of bases:

  • alpine — ~5 MB, musl libc (sometimes incompatible with libraries built against glibc).
  • slim — a stripped-down Debian, a good balance of compatibility and size.
  • distroless — only the runtime, no shell and no package manager (smaller runtime attack surface, not a security guarantee).

⚠️ Gotcha: deleting files in a later layer doesn't reduce the image — the preceding layer has already committed their weight. You must clean up in the same RUN. Also, alpine uses musl instead of glibc: binary Python wheels (manylinux) may not work, and you'll have to compile → which can turn out slower and more painful than slim.

Sources: Reference 1.

11

Volumes vs bind mounts

Short answer: Both are ways to persist data outside the container's ephemeral layer. A volume is managed by Docker and stored through the configured volume driver (the host path depends on the Docker installation) — for production data (a DB). A bind mount mounts a specific folder from the host into the container — handy for development (live code).

In depth:

# Named volume — Docker manages the storage itself
docker volume create pgdata
docker run --env-file ./postgres.env -v pgdata:/var/lib/postgresql/data postgres:16

# Bind mount — a specific host path → a path in the container
docker run -v $(pwd)/src:/app/src myapp        # host code is visible inside (hot reload)

# tmpfs — temporary memory-backed storage; pages may reach disk through swap
docker run --tmpfs /tmp myapp

In compose:

services:
  db:
    image: postgres:16
    env_file: ./postgres.env
    volumes:
      - pgdata:/var/lib/postgresql/data   # named volume
  web:
    build: .
    volumes:
      - ./src:/app/src                     # bind mount for development
volumes:
  pgdata:
Volume Bind mount
Management Docker manual (host path)
Portability stable volume name; data migration is explicit tied to the host's structure
Use case production, DB data development, configs

⚠️ Gotcha: a bind mount "covers" the contents of the target folder in the container. If you mount an empty host folder onto /app/node_modules, the modules installed in the image disappear. The fix is an anonymous volume for node_modules on top of the code bind mount. Also, a bind mount ties you to host paths and can break portability and permissions (UID/GID).

Create postgres.env with a local POSTGRES_PASSWORD before running these examples; keep it out of version control. A local Docker volume survives container removal, but does not automatically move to another host or replace backups.

Sources: Reference 1, Reference 2.

12

Docker networking: bridge, host, ports

Short answer: By default, containers are attached to the bridge network — each has its own internal IP, and ports are published to the outside via -p host:container. On a user-defined bridge network, containers see each other by service name (built-in DNS). The host network removes isolation — the container uses the host's network directly.

In depth:

# Port publishing: host:8080 → container:80
docker run -p 8080:80 nginx

# User-defined network → containers resolve each other by name
docker network create appnet
docker run -d --name db --network appnet --env-file ./postgres.env postgres:16
docker run -d --name api --network appnet myapi   # inside api: host "db" resolves to db's IP

# host network: Linux Engine, or opt-in Docker Desktop 4.34+; no -p
docker run --network host nginx

Driver types:

  • bridge (default) — an isolated virtual network, NAT to the outside.
  • host — shares the host network, no port publishing; removes that network-namespace boundary and may reduce networking overhead.
  • none — only the loopback interface, no external network connectivity.
  • overlay — Docker multi-host networking that requires Swarm mode; Kubernetes networking is configured separately through its network implementation.

⚠️ Gotcha: on the default bridge network, DNS by container names doesn't work — name resolution exists only on a user-defined network (docker network create). That's why compose automatically creates its own network and services see each other by name. Also: inside a container, localhost is the container itself, not the host; to reach the host on Mac/Windows you use host.docker.internal.

For the local DB example, create an untracked postgres.env containing POSTGRES_PASSWORD. Publish ports only when needed; -p 127.0.0.1:8080:80 restricts exposure to the host loopback interface.

Docker overlay networks require the participating Docker hosts to join a swarm, even for standalone containers. Documentation.

Sources: Reference 1.

13

What is docker-compose?

Short answer: Docker Compose (current command: docker compose; docker-compose is the legacy command) is a tool for describing and running multi-container applications from a single YAML file. Instead of long docker run commands for each service, you describe all services, networks, volumes, and dependencies declaratively, and bring everything up with one command, docker compose up.

In depth:

services:
  web:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    networks:
      - appnet

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      retries: 5
    networks:
      - appnet

volumes:
  pgdata:
networks:
  appnet:
docker compose up -d        # bring everything up in the background
docker compose ps           # service status
docker compose logs -f web  # logs of a service
docker compose down         # stop and remove (volumes are kept)
docker compose down -v      # + remove volumes

⚠️ Gotcha: depends_on without condition: service_healthy only guarantees startup order, not the service's readiness. The DB container is "started", but Postgres inside is still initializing — the initial connection can fail. Healthchecks handle initial readiness; the application still needs appropriate retry/reconnection behavior for later outages. The credentials shown are disposable local examples, not production secrets. Compose can also be used for some production deployments; choose orchestration by reliability, scaling and operational needs, not by a rule that production requires Kubernetes.

14

Environment variables and secrets in Docker

Short answer: Configuration is passed via environment variables (-e, --env-file, environment in compose). Secrets (passwords, tokens) must not be baked into the image — use env at run time, Docker secrets, or external managers (Vault, AWS Secrets Manager, k8s Secrets).

In depth:

# One at a time
docker run -e DATABASE_URL=postgres://... myapp

# From a file
docker run --env-file .env myapp
# compose: reference to a .env file, values not in the repository
services:
  web:
    env_file: .env
    environment:
      LOG_LEVEL: info

Compose can mount a secret file under /run/secrets/. This local file example is not Swarm encrypted secret storage:

services:
  web:
    secrets:
      - db_password
secrets:
  db_password:
    file: ./db_password.txt

⚠️ Gotcha: a secret passed via ENV in a Dockerfile or --build-arg remains in the image layers and is visible in docker history / docker inspect — anyone with access to the image can read it. Build-time credentials can be supplied with BuildKit secret mounts; never copy their contents into output layers. Supply application secrets at run time, and add .env to .gitignore and .dockerignore. Environment variables are visible to processes and in /proc/<pid>/environ, so for highly sensitive data, file-based secrets or an external manager are better.

Sources: Reference 1, Reference 2.

15

Registry, tags, and the `latest` anti-pattern

Short answer: A registry stores images. Docker Hub, GitHub/GitLab Container Registry and ECR support visibility controls; public/private availability depends on the service and repository configuration. Images are versioned with tags (myapp:1.4.2). Using latest in production is an anti-pattern: it's a floating, unpredictable tag that breaks reproducibility and rollbacks.

In depth:

docker login registry.example.com
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0
docker pull registry.example.com/team/myapp:1.0

A good tagging strategy:

  • myapp:1.4.2 — semantic version (for rollbacks).
  • myapp:git-a1b2c3d — tag by commit SHA (exact link to the code).
  • myapp:1.4 / myapp:1 — floating tags for convenience.
  • For maximum strictness — pin by digest: myapp@sha256:....

⚠️ Gotcha: latest is just the default tag, not "the most recent version." It doesn't update automatically and points to whatever image was last tagged with it. In production, image: myapp:latest means "who knows what's deployed" — the tag alone cannot identify the deployed bytes. Record and deploy the image digest, or enforce immutable release tags, so reproduction and rollback remain possible.

Even version and commit tags can be moved unless the registry enforces immutability. Record the deployed digest for reliable reproduction and rollback; a tag name alone is not proof of identical bytes.

Sources: Reference 1.

16

Stateless containers and one process per container

Short answer: Prefer containers organized around one service or responsibility, with durable state stored intentionally outside the container's replaceable writable layer. This is a design guideline, not a ban on stateful services or multiple processes.

A web service can keep requests transient while PostgreSQL stores persistent data on a volume. The database container is stateful and needs backups, recovery tests, and a deliberate storage lifecycle. A volume surviving container replacement is not itself a backup or a promise it can be attached on every host.

“One process” means one cohesive responsibility: a process manager with workers, or a service with necessary helper processes, can be reasonable. The important properties are clear signal handling, child-process reaping, logs, health checks, and independent deployment/scaling boundaries.

Example: Run web, background worker, and database as separate services when their lifecycles differ. Do not assume adding web replicas removes bottlenecks in their shared database or queue.

Watch out: Standard output/error is a convenient log-collection convention. File logging can also work with a correctly configured persistent/collected destination. In either case, define rotation, retention, and collection instead of assuming logging happens automatically.

17

What are CI and CD?

Short answer: Continuous integration means integrating small changes frequently and using automated checks to detect problems early. Continuous delivery keeps changes in a deployable state so a release can be chosen on demand. Continuous deployment automatically releases changes that pass the required gates.

  • CI: Build and test proposed changes and merged code. CI does not itself merge every change or guarantee that main is always correct; triggers and required checks must be configured.
  • Delivery: Produce a tested, deployable artifact. Production release timing remains a decision; a manual approval is one common implementation.
  • Deployment: Automate the production step for eligible changes. Tests, rollout checks, monitoring and recovery still matter.
CI:                    change → build/tests → feedback
Continuous delivery:   validated artifact → release when chosen
Continuous deployment: validated artifact → automatic release

Watch out: Ask what “CD” means in the team. Continuous delivery is the ability to release reliably on demand, not a requirement that every team uses a particular approval button. Automation does not prove a release is safe.

18

Pipeline stages: lint → test → build → deploy

Short answer: A typical pipeline: lint (static analysis/style) → test (unit/integration tests) → build (build an artifact/Docker image) → deploy (roll out). Configure dependencies so deployment requires the relevant checks; independent checks can run in parallel. Some tests require a build first.

In depth:

  1. Lint / static analysis — the cheapest and fastest stage; catches style errors and some bugs before the code even runs (ruff, eslint, gofmt, type checking with mypy/tsc).
  2. Test — unit tests, then integration tests. Often with coverage measurement. A test failure blocks a merge only when the repository requires that check.
  3. Build — build the artifact: Docker image, jar, binary, frontend bundle. Tag by version/commit.
  4. Deploy — publish the image to the registry and roll out to an environment (staging → prod). Often with DB migrations and smoke tests afterward.

Prefer fast feedback, but choose order by actual dependencies and runtime. gofmt formats Go code; CI must explicitly fail on unexpected formatting changes. Build once and promote the same tested artifact when the deployment process permits it.

⚠️ Gotcha: DB migrations are a separate pain point. They need to be compatible with the previous version of the code (expand/contract), otherwise during a rolling deploy the old pods will crash on the new schema. Don't "drop a column and deploy the code at the same time."

19

GitHub Actions / GitLab CI in brief

Short answer: Both systems describe automation in repository YAML. GitHub Actions has workflows, jobs, steps, and runners; GitLab CI/CD has pipelines containing jobs, often grouped into stages.

Concern GitHub Actions GitLab CI/CD
Configuration .github/workflows/*.yml .gitlab-ci.yml
Dependencies needs connects jobs Stage order, or a DAG with needs
Steps Ordered steps inside a job Job script and related hooks
Build outputs Explicit artifact upload/download or registry Declare artifacts; later stages download prior-stage artifacts by default, configurable with dependencies or needs:artifacts

Example workflow: Test a Go module with go test ./..., build the binary or image after tests pass, publish an identified artifact, then deploy it through the configured environment gate. A container build job needs access to a suitable builder; pushing an image also needs registry authentication.

Watch out: Job dependencies do not automatically share a filesystem. GitHub-hosted jobs generally get fresh environments; self-hosted runners can retain state, and GitLab behavior depends on the executor. GitHub jobs run independently unless connected with needs; GitLab jobs in the same stage are eligible to run concurrently. Available runners and limits determine actual parallelism. Keep secrets in the platform's protected mechanisms and restrict untrusted pull-request access.

20

Deployment strategies: rolling, blue-green, canary

Short answer: Rolling — gradually replace instances with the new version one by one. Blue-green — bring up a full new environment (green) alongside the old one (blue) and switch all traffic over at once. Canary — route the new version to a small fraction of traffic, observe, and gradually increase it.

In depth:

Strategy How Pros Cons
Rolling replace pods incrementally can avoid downtime with readiness/draining and sufficient surge capacity two versions run at once, slow rollback
Blue-green two full environments, switch traffic fast traffic switchback if state remains compatible; old connections may still drain needs 2x resources
Canary 5% → 25% → 100% of traffic to the new version early detection of problems on a small audience harder to set up (routing, metrics)
Rolling:    [v1][v1][v1] → [v2][v1][v1] → [v2][v2][v1] → [v2][v2][v2]
Blue-green: blue(v1) active | green(v2) warmed up → switch LB to green
Canary:     95% of traffic → v1, 5% → v2; grow the share when metrics look good

⚠️ Gotcha: with rolling and canary, two versions of the application run at the same time — the API and DB schema must be backward compatible. Blue-green with a shared DB also requires schema compatibility. Canary is pointless without good monitoring: someone has to decide whether the rollout is "healthy."

Sources: Reference 1.

21

Artifacts and dependency caching in CI

Short answer: Artifacts are named outputs you need from a run, such as a compiled binary or test report. Caches store reusable data to avoid repeating work, such as downloaded dependencies or compiler intermediates. Correct builds should still work when the cache is absent.

For a Go pipeline, cache module downloads/build intermediates using the Go version, platform, and dependency state where relevant. Always run dependency resolution and validation; a cache hit is not proof the dependencies match the current source. Publish the tested binary or image under an immutable identity such as a commit or digest.

Transfer differs by platform: GitHub Actions uses explicit artifact upload/download steps. GitLab jobs declare artifacts, and later stages fetch earlier-stage artifacts by default unless configured otherwise. Job ordering alone is not a portable output-transfer contract.

Watch out: A lockfile hash is a useful cache-key input, not a universal requirement for every kind of download cache. A plain requirements.txt is not necessarily a fully pinned lockfile. Cache contents may be evicted and platforms impose quotas and retention policies. Never rely on a cache as the only release copy, and do not put secrets in caches or public build outputs.

22

Linux: file system and permissions

Short answer: In Linux there's a single tree rooted at / (no C:/D: drives). Every file has an owner (user), a group, and permissions for three categories — owner/group/others — each with three rwx bits (read/write/execute). Permissions are changed with chmod, ownership with chown.

In depth:

Key directories: /etc (configs), /var (logs, variable data), /home (home directories), /usr (programs), /tmp (temporary), /proc and /sys (virtual kernel file systems), /bin, /opt.

Permissions in ls -l output:

-rwxr-xr--  1 alice devs  4096 Jun 24 10:00 script.sh
│└┬┘└┬┘└┬┘
│ │  │  └── others: r-- (read)
│ │  └───── group:  r-x (read + execute)
│ └──────── owner:  rwx (everything)
└────────── type: - file, d directory, l link

Numeric notation (r=4, w=2, x=1):

chmod 755 script.sh   # rwx r-x r-x — executable file/script
chmod 644 file.txt    # rw- r-- r-- — a regular file
chmod 600 secret.key  # rw- --- --- — owner only (private key)
chmod +x deploy.sh    # add execute permission

chown alice file.txt          # change the owner
chown alice:devs file.txt     # owner and group
chown -R www-data:www-data /var/www   # recursively for the whole directory

⚠️ Gotcha: chmod 777 ("everything for everyone") is almost always bad advice from the internet, a security hole; the real cause of the problem is usually the wrong owner (chown). For a directory, the x bit means "you can enter / access files inside," not "execute." OpenSSH rejects private keys accessible to other users; 600 or read-only 400 are common settings. Directory traversal and ACLs also matter.

Sources: Reference 1.

23

Useful command-line commands

Short answer: Navigation (ls/cd/cp/mv/rm), viewing processes (ps/top/htop), searching (find/grep), text processing (awk/sed), logs (tail -f), disk (df/du), network (curl/wget/ssh), archives (tar), process management (kill).

In depth:

# Navigation and files
ls -lah                  # detailed, hidden files, human-readable sizes
cp -r src/ dst/          # recursive copy
mv old new               # move/rename
rm -rf dir/              # delete recursively (CAREFUL)

# Processes
ps aux | grep nginx      # all processes, filter by name
top                      # real-time monitoring (htop is nicer)

# Search
find /var/log -name "*.log" -mtime -1     # .log files changed in the last 24h
find . -type f -size +100M                # files larger than 100 MB
grep -rni "error" /var/log/app/           # recursive case-insensitive search with line numbers

# Text processing
awk '{print $1, $7}' access.log           # print the 1st and 7th columns
sed 's/foo/bar/g' file.txt                # replace foo with bar
cut -d: -f1 /etc/passwd                    # first column split by the ":" delimiter

# Logs
tail -f /var/log/app.log                   # watch for new lines
tail -n 100 app.log | grep ERROR
journalctl -u myapp -f                      # logs of a systemd service in real time

# Disk
df -h                    # free space by partition
du -sh ./*               # size of each item in the current folder
du -sh /var/log          # total size of a directory

# Network
curl -s https://api.example.com/health      # request (quiet mode)
curl -i -X POST -d '{"a":1}' -H "Content-Type: application/json" URL  # POST with a header
wget https://example.com/file.tar.gz
ssh -i ~/.ssh/key.pem user@host              # connect using a key

# Archives
tar -czf backup.tar.gz dir/    # create a gzip archive (gzip-compressed tar, not ZIP)
tar -xzf backup.tar.gz         # extract a gzip-compressed tar archive
tar -tzf backup.tar.gz         # view the contents

⚠️ Gotcha: rm -rf doesn't ask for confirmation and has no trash bin — a wrong path can delete unintended data. GNU rm normally refuses recursive removal of /, but that protection does not protect other directories. Mnemonic for tar: create / extract, z = gzip, f = file. And kill <pid> sends SIGTERM, it doesn't "kill instantly."

Sources: Reference 1, Reference 2.

24

Processes, signals, daemons, systemd

Short answer: Signals notify or control a process. SIGTERM requests termination and can be handled; SIGINT is commonly sent by Ctrl+C; SIGKILL cannot be caught, blocked, or ignored. SIGHUP means reload only for applications that implement that behavior.

kill -TERM 1234  # replace 1234 with the verified target PID
kill -HUP 1234   # only if this service documents reload on HUP
systemctl status myapp
journalctl -u myapp --since "1 hour ago"

Graceful shutdown is application behavior: A handler for SIGTERM may stop accepting work, wait for requests with a bound, flush required data, and exit. Without a handler, the default action normally terminates the process; SIGTERM alone does not guarantee cleanup.

SIGKILL gives application code no cleanup opportunity. Delivery/exit need not appear instantaneous, for example while a task is in uninterruptible kernel sleep. The kernel releases process-owned resources on exit, but unfinished application-level effects may need recovery; not every lock or database transaction is permanently left behind.

Many Linux systems use systemd as service manager, but not all do. docker stop sends the configured stop signal (normally SIGTERM), waits a configured timeout, then uses SIGKILL if needed. Defaults vary by platform; Linux containers commonly use ten seconds.

Watch out: Prefer the service's documented stop mechanism and verify completion. Force termination only when its effects and recovery path are understood.

25

Pipes and redirection (stdin, stdout, stderr)

Short answer: A process has three standard streams: stdin (0, input), stdout (1, normal output), stderr (2, errors). The pipe | directs one command's stdout into another's stdin. Redirections: > (overwrite a file), >> (append), 2>&1 (merge stderr into stdout).

In depth:

# Pipe: stdout on the left → stdin on the right
cat access.log | grep ERROR | wc -l       # count lines with ERROR

# Redirect stdout to a file
echo "hello" > out.txt        # overwrite
echo "more" >> out.txt        # append to the end

# Streams by number: 0=stdin, 1=stdout, 2=stderr
command > out.log 2> err.log   # output and errors to separate files
command > all.log 2>&1         # both stdout and stderr to one file
command 2>/dev/null            # throw errors into the void
command &> all.log             # bash: everything to one file (shorthand for >file 2>&1)

# stdin from a file
sort < names.txt

# Combinations
printf '%s\n' banana apple | sort | head -n 1      # compose read-only filters
journalctl -u myapp 2>&1 | tail -50

⚠️ Gotcha: the order in > file 2>&1 matters! It means "stdout → file, then stderr → wherever stdout currently points (file)." But 2>&1 > file first points stderr at the current stdout (often the terminal), and only then sends stdout to the file — stderr retains that earlier destination. By default, Bash uses the last command’s exit status for a pipeline, so a successful final command can hide an earlier failure. set -o pipefail makes the pipeline report its rightmost nonzero status, or zero if every command succeeds; ${PIPESTATUS[@]} exposes the individual statuses immediately afterward.

26

Environment variables (export, .bashrc, PATH)

Short answer: Environment variables are key=value pairs available to processes. export VAR=value makes a variable available to child processes. PATH is the list of directories where the shell looks for executable commands. Persistent settings go into .bashrc/.profile/.zshrc.

In depth:

# Create a previously unset shell variable (not exported yet)
MY_VAR=hello
# Export it (visible to child processes)
export MY_VAR=hello
export DATABASE_URL="postgres://localhost/app"

echo $MY_VAR        # read it
env                 # all environment variables
printenv PATH       # a specific one

# For a single command only
DEBUG=1 python app.py

# PATH — where to look for commands
echo $PATH                       # /usr/local/bin:/usr/bin:/bin:...
export PATH="$HOME/bin:$PATH"    # add your own folder to the front
command -v python                 # shell command resolution

Persistent variables go in the shell's init files:

# ~/.bashrc (bash, interactive) or ~/.zshrc (zsh)
export PATH="$HOME/.local/bin:$PATH"
export EDITOR=vim

source ~/.bashrc    # Bash only; use the correct file for your shell

⚠️ Gotcha: a newly created, unexported shell variable is not inherited by child processes. A variable already exported keeps its export attribute when reassigned. Export changes future child environments; it does not update already-running processes or the parent shell. Changes in .bashrc don't apply to already-open sessions — you need source or a new terminal. Adding "." (the current folder) to PATH is a security risk — you could accidentally run a malicious ls from the current directory.

27

Which process is listening on a port (lsof, netstat, ss)

Short answer: You can find out which process is using a port with ss, lsof, or netstat. The modern recommendation is ss (faster, replaces the deprecated netstat).

In depth:

# ss — the modern tool
ss -tulpn                 # TCP+UDP, listening, with PID and ports (no name resolution)
ss -tlpn | grep :8000     # who's listening on port 8000

# lsof — list open files/sockets
lsof -i :8000             # the process on port 8000
lsof -i -P -n | grep LISTEN

# netstat (deprecated, but still common)
netstat -tulpn | grep :80

# ss/netstat flags: t=tcp, u=udp, l=listening, p=process(pid), n=numeric, a=all

A typical "Address already in use" scenario:

ss -tlpn | grep :8000          # find the PID holding the port
# users:(("python",pid=12345,fd=3))
kill 12345                     # free the port

⚠️ Gotcha: to see another user's process (PID) on a port you often need root (sudo ss -tulpn), otherwise the process column will be empty. On macOS the flags differ — there lsof -i :PORT is more reliable. On Linux, binding below the configured unprivileged-port threshold requires suitable privilege, such as CAP_NET_BIND_SERVICE; running the whole application as root is not required.

Sources: Reference 1.

28

What is nginx?

Short answer: Nginx is an HTTP server and reverse proxy. It can serve files, forward requests, terminate TLS, distribute traffic, cache responses, and apply configured limits. It is one possible deployment component, not a prerequisite for a Go HTTP service.

# Inside the http context; backends must already be running.
upstream backend {
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
}
server {
    listen 127.0.0.1:8080;
    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

This local HTTP example preserves the /api/ prefix. Adding a URI / to proxy_pass http://backend/ replaces the matching location prefix. Public HTTPS needs a TLS listener and valid certificate configuration; proxying to a backend can use HTTP or HTTPS according to the trust boundary.

Watch out: Forwarded headers carry claims about the original connection. Configure the application to trust them only from known proxies, and configure the edge's handling of client-supplied headers. A proxy does not hide a backend whose port is independently reachable. Validate configuration with nginx -t before reloading and verify the actual routes afterward.

29

nginx vs WSGI/ASGI server (gunicorn/uvicorn)

Short answer: nginx is an optional web server and reverse proxy; Gunicorn/Uvicorn are Python application servers. They solve different problems. A Go service normally uses its own net/http server and does not need WSGI or ASGI.

  • A proxy can centralize TLS, routing, buffering and limits in front of many application instances.
  • Gunicorn runs Python WSGI applications with a selected worker model. WSGI is synchronous at the interface, but concurrency depends on workers and threads.
  • Uvicorn serves ASGI applications, including async HTTP and WebSockets. It is a production server; --reload is a development option.
client → optional proxy/load balancer → Go net/http application
client → optional proxy/load balancer → Uvicorn → ASGI application

Decision: choose a proxy or managed edge for concrete requirements, not because every application needs nginx. Configure trusted forwarding headers, request limits, timeouts and graceful shutdown. Size workers from memory and workload measurements; a CPU-based rule of thumb is not universal. Do not copy an old Gunicorn/Uvicorn worker command without checking the installed versions and current deployment documentation.

Sources: Reference 1, Reference 2.

30

Kubernetes in brief

Short answer: Kubernetes orchestrates container workloads toward a declared desired state. You configure controllers, networking, storage, and policies; automatic recovery or scaling works within those settings and the available infrastructure.

  • Pod: The smallest deployable unit, containing one or more closely related containers that share networking and any explicitly configured shared volumes.
  • Deployment: Maintains a desired number of replaceable Pod replicas and manages changes to their template, commonly through rolling updates.
  • Service: Provides a stable way to discover and reach a changing set of endpoints. A common ClusterIP Service has a virtual IP and distributes connections to ready endpoints; headless Services work differently.
  • Ingress: Declares HTTP(S) routing and requires an installed controller to implement it. Gateway API is another routing option.
# Minimal Deployment example; the image name is a placeholder.
apiVersion: apps/v1
kind: Deployment
metadata: { name: myapp }
spec:
  replicas: 3
  selector: { matchLabels: { app: myapp } }
  template:
    metadata: { labels: { app: myapp } }
    spec:
      containers:
        - name: myapp
          image: registry.example.com/myapp:1.4.2
          ports: [{ containerPort: 8000 }]

This creates a desired count of three replicas; it does not automatically add replicas when load rises or expose a public endpoint. Autoscaling requires configuration such as a HorizontalPodAutoscaler and suitable metrics. Production availability also needs readiness checks, resource capacity, graceful shutdown, and tested rollout behavior.

When to use it: Compare the needed orchestration capabilities with the team's operational capacity and existing platform. A small service can run on Kubernetes when a platform already exists; a standalone small project may be simpler on Compose or a managed application service. Container count alone is not the decision rule.

31

Cloud in brief (compute, storage, managed db)

Short answer: Cloud platforms provide compute, storage, networking, and managed services on demand. “Managed” describes which responsibilities the provider takes; it does not mean every availability, backup, scaling, or security decision is automatic.

Need AWS example You still choose/check
Compute EC2 VMs, Lambda functions, ECS/EKS containers Capacity, permissions, deployment, supported runtime and resource limits
Storage S3 objects, EBS block volumes, EFS files Access, retention, encryption, durability/availability needs and costs
Database RDS relational DBs, DynamoDB Schema, workload, access, backup/restore policy and service configuration

Managed versus self-hosted: Running a database on a VM gives you more control and maintenance work. A managed database automates selected operations. The total cost depends on workload, staffing, availability requirements, and service choices; neither option is always cheaper.

For example, ordinary RDS Single-AZ and Multi-AZ deployments have different recovery behavior. Multi-AZ configuration provides a standby and automated failover for covered failures; that is not a property of every managed database instance. Restore tests and application reconnection remain important.

Watch out: Check the specific product, deployment mode, and shared-responsibility model. Track data transfer, storage, I/O and backup charges, and keep an exit/recovery plan. Provider infrastructure security does not replace your identity, access, configuration, and data responsibilities.

32

Monitoring and logs conceptually

Short answer: Three common observability signals are: metrics (numeric measurements over time — CPU, RPS, latency), logs (text events), and traces (a request's path through services). A typical stack: Prometheus (metrics collection) + Grafana (dashboards/visualization), and ELK (Elasticsearch + Logstash + Kibana) or Loki for logs.

In depth:

  • Metrics (Prometheus + Grafana) — Prometheus periodically scrapes (pull) application metrics from an HTTP /metrics endpoint and stores them as time series. Grafana builds graphs and alerts on top. Metrics are for trends and alerting ("latency went up," "the disk is filling up").
  • Logs (ELK / Loki) — centralized collection of logs from all services into one place, with search and filtering (Kibana/Grafana). Containers write to stdout, and a collector (Fluentd/Filebeat/Grafana Alloy) ships it to storage.
  • Traces — record operations and their relationships across a request. OpenTelemetry provides instrumentation/collection APIs and tooling; a backend such as Jaeger stores and visualizes traces. Sampling and instrumentation determine what is visible.
  • Alerting (Alertmanager) — notifications (Slack/PagerDuty) when rules fire.

Useful concepts: SLI/SLO/SLA (indicators/targets/commitments on reliability), and the "four golden signals" — latency, traffic, errors, saturation.

⚠️ Gotcha: unstructured or local logs can still help, but consistent fields, timestamps and request/trace IDs make cross-service diagnosis easier. Redact secrets, minimize sensitive data, and configure access and retention. Observability is about answering useful questions, not installing a fixed tool list. Promtail reached end of life in March 2026; use a supported collector for new setups.

33

Why Docker if there's venv?

Short answer: A Python venv selects an interpreter and isolates Python packages. A container packages user-space files and dependencies and runs a process with operating-system isolation. These are different boundaries.

For a Go developer, the analogous distinction is between go.mod/go.sum dependency management and the environment in which the compiled service runs. A Go binary may still depend on shared libraries through cgo, CA certificates, time-zone data or external services. Containerization can package those files consistently.

A container does not package a new kernel, supply secrets automatically or make an image run on every CPU architecture. Linux images require a compatible Linux kernel or a Linux VM, plus a suitable platform build. Compose can start related database/cache containers, while application configuration and readiness still need attention.

Example: dependencies resolve correctly on a laptop, but HTTPS requests fail in a minimal image because the CA bundle is absent. Language dependency isolation alone does not address that runtime requirement.

Sources: Reference 1, Reference 2.

34

Why bother with CI/CD at all?

Short answer: To turn releases from a rare, manual, scary event into frequent, automatic, and safe ones. CI/CD catches bugs early (automated tests on every commit), reduces repeated manual deploy mistakes, speeds up feature delivery, and makes release artifacts and recovery steps easier to reproduce. Rollback still depends on compatible data and schema changes.

In depth:

Without CI/CD: code is merged in big chunks, tests are run "when someone remembers," the deploy is a manual script done "from memory" that one senior engineer runs on Fridays, and if something breaks — panic. Releases are rare and risky → every release is huge → which makes them even riskier (a vicious cycle).

With CI/CD:

  • Early detection — tests/lint on every PR; required checks reduce the chance of merging failures those checks can detect.
  • Repeatability — the deploy is described as code, done the same way every time, not dependent on a person.
  • Speed — features reach users faster, feedback comes faster.
  • Less risk per release — small frequent changes are easier to roll back and easier to debug.
  • Documentation — pipeline as code = it's clear what is built/deployed and how.

⚠️ Gotcha: CI/CD without good tests and monitoring is just "fast delivery of bugs to production." Automation amplifies both good and bad practices. First reliable tests and observability, then automatic deployment to production.

35

What does nginx do in front of the application?

Short answer: Nginx can centralize reverse proxying, TLS, buffering, limits, and traffic distribution before an application. Whether to add it depends on the deployment: a Go server or a managed load balancer may already provide the required functions.

# Minimal local example; the application listens on 127.0.0.1:8000.
events {}
http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    upstream app_backend { server 127.0.0.1:8000; }
    server {
        listen 127.0.0.1:8080;
        location / {
            limit_req zone=api burst=20;
            limit_req_status 429;
            client_max_body_size 10M;
            proxy_pass http://app_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

The limit_req_zone declaration allocates the named tracking zone; using limit_req zone=api without defining it is incomplete. In this example, burst requests can be delayed and excess requests receive 429. The limit is keyed by the direct peer IP; behind another proxy you must deliberately configure trusted client-IP handling.

Limits: Buffering and rate limits help only for the traffic and configuration they cover. They do not guarantee protection from all abuse, speed up Go computation, or repair a slow database. HTTPS, streaming/WebSocket behavior, and upstream failure handling require their own configuration and tests. Avoid duplicating a platform edge without a concrete benefit.

Source notes

References and review policy

Source links provide further reading. Where a recorded technical review is available, the answer shows its date and scope. Tool selections use direct provider links and contain no affiliate placements. Sources, features, and hiring formats can change.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going