Docker

Docker - Medium

Page 1 of 2

Dockerfile Instructions, Properly

The basic tier covered FROM, WORKDIR, COPY, RUN, and CMD - enough to get something running. A few more instructions come up in almost every real Dockerfile, and a couple of easy-to-confuse pairs are worth getting straight now.

ARG vs ENV: both set a variable, but ARG only exists during the build and is gone once the image exists - useful for things like choosing a version at build time. ENV is baked into the final image and is still there when a container runs.

ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim

ENV NODE_ENV=production

CMD vs ENTRYPOINT: both describe what runs when a container starts, but they interact differently with anything you add after docker run my-app .... CMD is a default that gets fully replaced if you specify your own command. ENTRYPOINT is closer to a fixed command that anything you add gets appended to as arguments. A common pattern uses both together - ENTRYPOINT sets the actual program, CMD sets its default arguments, so someone can override just the arguments without needing to know the whole command.

ENTRYPOINT ["node"]
CMD ["server.js"]

EXPOSE documents which port the app listens on - it's informational (it doesn't actually publish anything the way -p at docker run does), but it's genuinely useful documentation for the next person reading the Dockerfile.

LABEL attaches arbitrary metadata to an image - a maintainer name, a source repo URL, a version - readable later with docker inspect.


Image Layers, Introduced

Every single instruction in a Dockerfile - FROM, COPY, RUN, all of them - creates a layer, and an image is really just a stack of these layers on top of each other. This matters for one huge practical reason: Docker caches each layer, and if a layer hasn't changed since the last build, Docker reuses the cached version instead of redoing the work.

That's why build order matters, and it's worth a first look here even though the full depth of this belongs in the hard tier: if you put a slow step (like installing dependencies) after a step that changes on every single build (like copying your whole source code), Docker can never safely reuse the cached dependency-install step, because it has no way of knowing your source change didn't also change what dependencies you need. Order the stable, slow-changing steps first, and the steps that change on every commit last.

docker history my-app       # see every layer in an image, and roughly how big each one is

.dockerignore

When you run docker build ., Docker sends everything in that folder to the build process as the build context - including things you almost certainly didn't mean to include, like your .git folder or a giant node_modules directory. A .dockerignore file works exactly like .gitignore, listing what to leave out.

node_modules
.git
*.log
.env

Skipping this isn't just wasteful - a slow build context upload is one of the most common "why is my build so slow" complaints, and it's a one-line fix.


Multi-Container Apps with Compose

Beyond the basics, Compose files support a few things worth knowing:

services:
  web:
    build: .
    depends_on:
      - database
    env_file:
      - .env
  database:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

depends_on controls start order - it makes sure database starts before web - but it does not wait for the database to actually be ready to accept connections, only for its container to have started. That distinction causes a lot of confusing "connection refused" errors on a cold start; a proper fix uses a health check (covered below) combined with depends_on's condition: service_healthy option.

env_file loads environment variables from a file instead of listing them all inline - handy for keeping secrets out of the Compose file itself.


Docker Networking Modes

Every container gets attached to a network, and which kind changes what it can and can't reach:

  • bridge (the default) - containers get their own private network, can reach each other by name if they're on the same Compose setup, and reach the outside world through NAT. This is what you want almost all the time.
  • host - the container shares the machine's own network directly, with no isolation at all. Occasionally useful for performance-sensitive tools, but it gives up one of Docker's main safety benefits.
  • none - no networking at all. Rare, but used for tasks that genuinely never need to talk to anything over a network.
docker network ls                       # see all networks Docker knows about
docker run --network=host my-app         # opt into host networking

Volumes and Bind Mounts, Properly

Three different ways to attach storage to a container, and picking the wrong one is a common source of confusion:

  • A named volume is managed entirely by Docker - you don't need to know or care where it physically lives. This is the right choice for data that needs to genuinely persist, like a database.
  • A bind mount points at a specific, real path on your own machine. Great for local development (edit a file locally, see the change instantly inside the container), but it ties your setup to your machine's actual folder structure - not something you'd typically use in production.
  • A tmpfs mount lives only in memory, never touches disk at all, and disappears the moment the container stops. Useful for genuinely temporary, sensitive data you don't want written to disk even briefly.
docker run -v my-data:/data my-app                    # named volume
docker run -v $(pwd)/src:/app/src my-app               # bind mount
docker run --tmpfs /tmp my-app                          # tmpfs mount

    Welcome to OpsQuiz!

    Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.