Docker for Dummies (Part-2 Http Server)

Docker for Dummies (Part-2 Http Server)

Learn how to containerise an HTTP server

Alright! We've got a bit of Docker residue lingering around, especially in areas like handling environment variables, managing those marathon HTTP servers, and wrapping our heads around concepts like Docker layers and port mapping. So, let's roll up our sleeves and tackle these Docker intricacies head-on!

The second project, a directory named http-express-server (in starter branch), contains three files: package.json ,.env (although env(s) should never be pushed to github) and main.js .

// Import required modules
const express = require('express');
require('dotenv').config()

// Create an Express application
const app = express();

const apiKey = process.env.API_KEY;

// Define a route
app.get('/', (req, res) => {
    res.send(`Hello, world! ${apiKey}`);
});

// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

This is boilerplate code required to run an http server in express.js. Now, we need to run this on docker. You can start with starter branch and see how you go.

Steps to Containerization:

  1. Make Dockerfile

  2. Write Boilerplate code for now. This config is our Dockerfile. Only thing that got changed is that we exposed port 5000

     # Use the official Node.js image from the Docker Hub, based on Alpine Linux for a smaller image size
     FROM node:22-alpine
    
     # Set the working directory inside the container to /usr/src/app
     WORKDIR /usr/src/app
    
     # Copy all files from the current directory to the working directory in the container
     COPY . .
    
     # Install the project dependencies specified in package.json
     RUN npm install
    
     # Expose port 5000 so that the APIs can be accessed from outside the container
     EXPOSE 5000
    
     # Define the command to run the application
     CMD ["npm", "start"]
    
  3. (Optional but recommended) Create a .dockerignore file. This file helps you avoid copying certain parts of your source code into the Docker container. For example, you might want to prevent your dev .env files or node_modules from being copied when the COPY . . command is executed.

     # .dockerignore
     node_modules
     .env
    
  4. Build your Docker Image with docker build -t express-backend .

  5. Running this image is slightly different than before. We have new arguments. docker run -p 8000:5000 -e PORT=5000 -e API_KEY=<your_api_key_here> express-backend These arguments are -p and -e .

    1. Port Mapping (i.e. -p 8000:5000 ): We are writing this to tell docker engine to that open an empty port 8000 and connect/map container's port 5000 to our local machine's 8000. Why do we need to do this ? Because container is supposed to be as isolated as possible.

    2. Environment Variable (i.e. -e PORT=5000 or -e API_KEY=<your_api_key_here>) : Here are defining our environment variables which are called instead docker container.

  6. Now, your server is running inside the container. (here I have same port inside container and outside the container).

  7. Just a minor thing right now. Port mapping... We need to discuss what port to hit and why. So, originally, we const PORT = process.env.PORT || 3000;, which meant we are letting env to decide which port to open or api endpoint. In our case, we are -e PORT=5000 which means the application itself is running on port 5000 of docker container. And then we are mapping docker container's 5000 port to our local device's 8000 Port. That's why we need to hit localhost:8000 .

Docker Layers and Caching

FROM node:22-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
#EXPOSED 5000 Port so that we can call apis from outside the container
EXPOSE 5000 
CMD ["npm", "start"]

This section is crucial, so please pay close attention. When you build your Docker image multiple times with code changes, you might notice lines like CACHED [1/5], CACHED [2/5], etc. These indicate that certain steps (or layers) were cached because they were identical to those in the previous image build. Docker caches these intermediate build results to save time and resources, avoiding redundant tasks.

So, what are "Layer"?

🐋
Docker Layers are the smallest steps or instructions in a Dockerfile. They are treated like intermediate outputs that are cached in most cases. This caching helps to optimize the build process for future builds.

To optimize your Dockerfile, you should structure it so that the steps most likely to change are placed later in the build process. For example, in our Dockerfile, the RUN npm install command depends on a set of dependencies that don't change often, whereas the COPY . . command runs every time we make changes to our codebase.

One analogy people often use is that Docker layers are like the layers of an onion. Each time you run the Docker build command, you make a onion inside-out. First, you add the innermost layer, which is the base image of node-alpine. Then, you copy the files and run npm install. If we build our Docker image multiple times, npm install will be called more often than we change package.json.

A sliced red onion with three arrows pointing to it. The arrows are labeled "Node-alpine," "COPY . .," and "RUN npm install."

By rearranging these commands, we can reduce the number of layers that need to be rebuilt. Specifically, if we move the RUN npm install command above the COPY . . command, we only need to rebuild the final step (copying the source code) when the source code changes. Here's the optimized version of the same Dockerfile:

FROM node:22-alpine

# Create app directory
WORKDIR /usr/src/app

# Copy only package.json files as npm install requires this particular file.
COPY package*.json ./
# Install dependencies in empty project with just only package.json(s)
RUN npm install

# Copy back the source code o the application.
COPY . .

CMD ["npm", "start"]

In this optimized Dockerfile, Docker only needs to recompute the last step (COPY . .) when the source code changes, significantly speeding up the build process. This way, you only rebuild what’s necessary, improving efficiency and saving time.

Optimize your Dockerfile by placing less frequently changing steps, like RUN npm install, above frequently changing ones, like COPY . .. Layers are steps in a Dockerfile, and leveraging Docker's caching of these layers reduces build times.

Next Part

That's all for today, in next part we will see how to make multi-container system with docker compose also look into how to "communicate" with each other with shared networks and shared space.

Next Part: https://blogs.shashankdaima.com/docker-for-dummies-part-3-docker-compose

Bye-Bye