Build images

Write the first Dockerfile

Describe the Notes API image with a base image, working directory, copied files, documented port, and startup command.

A Dockerfile is a recipe for building an image. It’s a plain text file with one instruction per line, run from top to bottom. Each instruction either adds a layer to the filesystem or sets some metadata the final image carries.

Here’s the one for the Notes API. Save it as Dockerfile (no extension) in the project folder:

FROM node:22-alpine
WORKDIR /app
COPY package.json server.js ./
ENV PORT=3000
EXPOSE 3000
CMD ["node", "server.js"]

Let’s go through it line by line.

The instructions

FROM node:22-alpine picks the starting filesystem. Every image starts from another image. This one is a small Alpine Linux with Node.js 22 already installed, so we don’t install Node ourselves.

WORKDIR /app creates the /app directory and makes it the current directory for everything that follows. Without it, files would land in /.

COPY package.json server.js ./ copies the two files from your project folder into /app inside the image. The ./ is relative to the WORKDIR.

ENV PORT=3000 sets a default environment variable. Our server reads it. You can override it when you start a container.

EXPOSE 3000 is documentation. It tells people and tools that the app listens on 3000. It does not publish the port. You still need -p when you run the container. This confuses everyone at first, so remember it: EXPOSE documents, -p publishes.

CMD ["node", "server.js"] is the default command Docker runs when the container starts. It becomes the main process.

Why the JSON form of CMD

You can also write CMD node server.js. Don’t.

With that shell form, Docker runs /bin/sh -c "node server.js". The shell becomes the main process, and Node is its child. When Docker stops the container it sends SIGTERM to the main process, the shell, which doesn’t forward it. Node never hears it, and after ten seconds Docker kills everything. Your app never gets the chance to close connections cleanly.

With the JSON form, Node is the main process and receives the signal directly.

Build-time and run-time

Notice the split in the file. FROM, COPY, and later RUN shape the image. They run at build time and their result is frozen. ENV and EXPOSE describe defaults you can change per container, with -e and -p. Keep environment-specific values out of the image and pass them at run time.

And node:22-alpine is a tag, so it moves. For a course that’s fine. For a release, record the digest too, as we saw earlier.

Build it

docker build -t notes-api:dev .

-t notes-api:dev names the image. The final . is the build context, which is the topic of the next lesson. Run docker image ls notes-api and you’ll see your first image.

Lesson completed