Skip to content

The Docker Bay Six

Let’s look at the key instructions from our recipe, piece by piece.

These six instructions are not the only ones available, but they are the most common and fundamental instructions used in a Dockerfile.

FROM node:20-alpine

This sets the base image.

A Base Image is a pre-packaged environment that already contains the basic OS tools and runtime we need.

Instead of manually installing Linux and Node.js from scratch every time, we start with an image that has them ready to go.

Most base images are pulled from Docker Hub, the central registry for the Docker world.

When we specify FROM node:20-alpine, we are saying:

  • node: The official Node.js repository.
  • 20: The major version of the runtime.
  • alpine: A specific variant based on Alpine Linux, which is incredibly tiny (around 5MB) compared to standard distributions. This keeps our final image fast and secure by removing “bloat.”

Official or Bust

In a professional environment, always prioritize “Official” images (verified by Docker) over random community ones. It’s a matter of security. We don’t want to build our infrastructure on a foundation that we don’t trust.

WORKDIR /app

This sets the working directory inside the image.

After this line runs, any later COPY, RUN, or CMD instructions will execute relative to /app inside the container. If the folder doesn’t exist, Docker creates it.

WORKDIR

The WORKDIR instruction is not strictly necessary, but it is a good practice to use it. It makes the Dockerfile easier to read and understand.

Also, WORKDIR does not define the root directory of the image. It only defines the working directory for subsequent instructions.

COPY package*.json ./

This copies the package manifest files from our host machine into the working directory of the image. The ./ points directly to the WORKDIR we just established.

Later, the command:

COPY . .

copies the rest of our application code across to the image.

Dot Space Dot

The two dots in COPY . . refers to the build context, which is the set of files and directories that are copied from the host machine to the image.

This is not the same as .. which refers to the parent directory. It’s important to understand the difference between the two.

If you forget the space between the two dots, Docker will try to copy a file named .. to the current directory, which will likely fail.

RUN npm install

This executes a shell command during the image build phase.

In this case, it installs the NPM dependencies and burns the resulting node_modules directory permanently into the image.

RUN

We can use as many RUN instructions as we need in order to build the image. Most of the time, we will only need one or two, such as installing dependencies (npm install) and building the application (npm run build).

EXPOSE 3000

This records the container’s intended listening port in the image metadata.

It does not publish the port to your computer by itself.

Documentation Only

EXPOSE does not make the app reachable from your browser on its own.

Think of it as a note attached to the image that says, “This container expects to listen on port 3000.”

To actually reach the app from outside the container, we will still need to:

  1. map a host port to the container port when we run the container
  2. make sure the application inside the container is actually listening on that port

We’ll publish ports when we run the container a little later.

CMD ["npm", "run", "dev"]

This sets the default command Docker runs when the container starts.

In this lesson, we want our container to start the Vite development server, so this is the command we use.

Only One CMD

A Dockerfile should have only one active CMD.

If you write more than one, Docker uses only the last one.

If our package.json contains this script:

"scripts": {
"dev": "vite --host 0.0.0.0"
}

then this Dockerfile instruction:

CMD ["npm", "run", "dev"]

tells Docker to run that script when the container starts.

Why does `start` look different?

If package.json has a script named “start”, we can use CMD ["npm", "start"].

start is a special npm script name.

Both npm start and npm run start do the same thing.

Inside a container, localhost means “inside the container itself.”

So if Vite listens only on localhost, the dev server may be running, but our browser outside the container still won’t be able to connect to it.

That’s why our script uses:

"dev": "vite --host 0.0.0.0"

This tells Vite to listen on all network interfaces inside the container, making it reachable through the mapped port.

RUN vs CMD

RUN happens when the image is built.

CMD happens when a container is started from that image.


Dockerfile Best Practices

Layers are cached and only rebuilt when the instructions they depend on change.

This means that the order of instructions in a Dockerfile matters.