Docker for Web Development - Dockerfile

In the previous post, we covered Docker Volumes and how they can help you persist data and streamline your development process. In this post, we will dive deeper into Dockerfiles and how they can help you build and manage custom Docker images for your web development projects.

This is a series of posts about Docker for Web Development:

  1. Basic Concepts
  2. Volumes
  3. Custom Images with Dockerfile
  4. Container Networks
  5. Docker-Compose

What is a Dockerfile?

A Dockerfile is a text file that contains a set of instructions to build a Docker image. It defines the base image, the application code, dependencies, and any other configurations needed to create a custom image for your application. By using a Dockerfile, you can automate the process of building images and ensure consistency across different environments.

It is like a recipe that tells Docker how to create an image step by step. Then we build the image using the docker build command, and Docker will follow the instructions in the Dockerfile to create a new image, that can be used later to run containers.

Basic structure of a Dockerfile

A Dockerfile consists of a series of instructions, each starting with a specific keyword. Some of the most commonly used instructions include:

  • FROM: Specifies the base image to use for the new image.
  • LABEL: Adds metadata to the image, such as author information or version number.
  • COPY: Copies files or directories from the host machine into the image.
  • WORKDIR: Sets the working directory for subsequent instructions.
  • RUN: Executes commands in the image during the build process.
  • ENTRYPOINT: Defines the command that will be executed when a container is started from the image.
  • EXPOSE: Informs Docker that the container will listen on the specified network ports at runtime.
  • ENV: Sets environment variables in the image.
  • VOLUME: Creates a mount point for a volume in the image.

Example Dockerfile for a simple web application

Here is an example of a Dockerfile for a simple Node.js web application:

Copy
# Use the official Node.js base image
FROM node:alpine
# Add metadata to the image
LABEL author="Your Name" \
      version="1.0" \
      description="A simple Node.js web application"
# Set the working directory
WORKDIR /app
# Copy the application code (first dot is the source, second dot is the destination)
COPY . .
# Install dependencies
RUN npm install
# Expose the application port
EXPOSE 3000
# Define the command to run the application
ENTRYPOINT ["npm", "start"]

Cache improvement in Dockerfile

When building Docker images, it's important to consider the order of instructions in the Dockerfile to take advantage of Docker's caching mechanism. Docker caches the results of each instruction, so if an instruction hasn't changed, Docker can reuse the cached layer instead of rebuilding it. This can significantly speed up the build process.

A common practice is to copy only the dependency files (package.json and package-lock.json) first, install the dependencies, and then copy the rest of the application code. This way, if you change your application code but not your dependencies, Docker can reuse the cached layer for the dependencies, speeding up the build process.

Copy
# Use the official Node.js base image
FROM node:alpine
# Add metadata to the image
LABEL author="Your Name" \
      version="1.0" \
      description="A simple Node.js web application"
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Set the working directory
WORKDIR /app
# Copy only the dependency files first
COPY package*.json ./
# Install dependencies
RUN npm install
# Up to here Docker will cache the dependencies layer, so if 
# you change your application code but not your dependencies, 
# Docker can reuse the cached layer for the dependencies, 
# speeding up the build process.

# Copy the rest of the application code
COPY . .
# Expose the application port. Use the environment variable defined above to make it more flexible.
EXPOSE ${PORT}
# Define the command to run the application
ENTRYPOINT ["npm", "start"]

Building and running the Docker image

To build the Docker image from the Dockerfile, navigate to the directory containing the Dockerfile and run the following command:

Copy
docker build -t my-node-app:1.0.0 .

This command will build the image and tag it as my-node-app:1.0.0. The . at the end specifies the build context, which is the current directory where the Dockerfile is located.

If we now list the images in our local machine, we will see the new image we just built:

Copy
docker images

To run a container from the newly built image, use the following command:

Copy
docker run -d -p 3000:3000 my-node-app:1.0.0

Since we run it in detached mode, we can check the running containers using the command:

Copy
docker ps

And now we can access the application by navigating to http://localhost:3000 in our web browser.

Now that we have built and run our custom Docker image, we can easily share it with others or deploy it to different environments (SAP BTP, Azure, AWS, ...), ensuring that our application runs consistently across all platforms.

Multi-stage builds

Multi-stage builds are a powerful feature of Docker that allows you to create smaller, more efficient images by using multiple FROM statements in a single Dockerfile. This is particularly useful for applications that require a build process, such as compiling code or bundling assets, before running the application.

As an example, we can use a multi-stage build for our Node.js application. In the first stage, we will build the application, and in the second stage, we will create a smaller image that only contains the necessary files to run the application.

Copy
# Stage 1: Build the application
FROM node:alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Stage 2: Create the final image
FROM node:alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
ENTRYPOINT ["node", "dist/index.js"]

Pushing the image to a registry

Once you have built your custom Docker image, you can push it to a Docker registry, such as Docker Hub or a private registry, to share it with others or deploy it to different environments.

To push the image to a registry, you first need to tag it with the registry's URL. For example, if you are using Docker Hub:

Copy
docker tag my-node-app:1.0.0 your-dockerhub-username/my-node-app:1.0.0

Then, log in to the registry:

Copy
docker login

Finally, push the image to the registry:

Copy
docker push your-dockerhub-username/my-node-app:1.0.0