Docker images are built from a Dockerfile, which is a blueprint for how to prepare a ready-to-run image. We can relate it to steps we perform in a computer to get something installed and running.

We can start with a base image that corresponds to an OS (like ubuntu, alpine, Debian etc…) or we can even start from essentials installed in an OS.

Basic steps are

  1. Start from the base image
  2. Run commands
  3. Specify default run command

Here’s a basic example from nodejs

FROM node:14-alpine

# Create app directory
WORKDIR /usr/src/app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm ci --only=production

# Bundle app source
COPY . .

CMD [ "node", "server.js" ]
  • FROM specifies the base image. Here, we’ll get an image of alpine Linux with nodejs installed in it. (Refer dockerhub)

for the commands, WORKDIR, RUN and CMD we must assume that we are inside a container running the os we specified in FROM

whereas, COPY and ADD will have access to the Directory that we’re in at the base OS outside docker.

  • WORKDIR will move the current working directory into the path and it will create the path if it doesn’t exist. It is a good idea to specify this since default dir might have conflicting folders/files in them.

  • COPY is used to copy files from our base OS outside docker to the File system inside of the image we’re creating.

  • RUN will simply execute a command that follows.

  • EXPOSE will expose a port in the container (not published to base os it is only available inside docker network)

  • CMD specifies the default starting command to be run.

Docker service builds an image from the following instructions. Each of the steps is cached and when we make changes at some point, it uses caches for the unchanged previous steps.

Therefore, it is important to write frequently changing parts to the later stages of the build file to utilize cache and speed up the build process.

Multistage docker build.

We can also break down the build into multiple steps like for example, the first step will compile and another step generates a lightweight runtime environment that simply runs the compiled file.

Here’s an example for go multistage build

# builder image
FROM golang:1.14-alpine as builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o program .

# runner image
FROM alpine
COPY --from=builder /build/program .

# executable
CMD [ "program" ]

we can specify a temporary build step with the “as” keyword, which will perform a build with the following steps we can add steps to perform actions like a compilation here.

we can easily access the files that are available in the builder by using the –from option inside copy. It is exactly like copying from the current directory but it will copy files from the temporary step’s file system.