If you are already a Developer or just started with development (Frontend, Backend, Node dev, Python-dev, etc) knowledge of Containers will be incredibly beneficial to your development journey. And if you are planning to learn or learning DevOps it is something necessary to know. This Blog on Docker will help you learn some of the intermediate concepts like Dockerfile and Docker Volume. Before starting with this part 2, I recommend you check part 1 of the "Docker in Practice" series where we covered the Basics of Docker.
Dockerfile
A Dockerfile is a text file that contains instructions for building a Docker image. A Docker image is a lightweight, stand-alone, executable package that includes everything needed to run a piece of software, including the application code, system tools, libraries, and runtime.
Create the application we will dockerize
We will containerize a simple Node.js application. Suppose you know Node and Express, then great. If you don't, then follow the steps.
You will find all the code we use here on my GitHub: https://github.com/debasishbsws/docker-in-practice/tree/main/code
Open up any empty folder.
If you have Node installed, you can open up the terminal, go to that directory, and run
npm init -y
, which will create apackage.json
file with all the default values. Also, you should install Express by runningnpm i express
.Now your
package.json
file will look something like this:{ "name": "code", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node index.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "express": "^4.18.2" } }
Here we just added the "start" script to this file so that the application will start by running
npm start
.If you don't have Node you can create the 'package.json' file and paste the above JSON code.
Create the
index.js
file in the same folder and write a simple express app.const express = require('express'); const app = express(); //values from env const PORT = process.env.PORT || 3000; //request app.get('/', (req, res) => { res.send('<h1>Hello World!!</h1>'); }); //app listening app.listen(PORT, () => { console.log(`App listening at http://localhost:${PORT}`) });
Our application is ready to be Dockerized. If you have Node in your system, you can run npm start
, open a web browser, and navigate to http://localhost:3000
. You should see the message "Hello, World!!" displayed in your browser. We don't need Node installed for this tutorial, as we will run this app inside a container.
Creating a Dockerfile
Create an empty file called Dockerfile
in the folder
touch Dockerfile
Open the Dockerfile
in your favorite text editor.
We first need to define the base image (the image from which we want to build our image). Here we will use the latest LTS (long-term support) version 16
of node
available from the Docker Hub:
# Comments
FROM node:16
Next, we create a directory to hold the application code inside the image, this will be the working directory for your application, and your application code will be inside this folder:
# Create an app directory
WORKDIR /usr/src/app
The base image comes with Node.js and NPM already installed, so we next need to install your app dependencies using the npm
binary. We specify to copy our package.json
file to the image working directory using COPY
instruction and also run npm install
inside the container terminal using RUN
instruction
# Install app dependencies
COPY package*.json ./
RUN npm install
Now we copy our app's source code inside the Docker image.
# Bundle app source
COPY . .
Note that we are only copying the package.json
file rather than copying the entire working directory. This allows us to take advantage of cached Docker layers. For a good explanation, check this.
Our application will listen to the port 3000
, and we need to EXPOSE
that port to have it mapped by the Docker daemon:
EXPOSE 3000
Last, define the command to run your app using CMD
, which determines your runtime. Here we will use npm start
to start the server. Or we can use node index.js
:
CMD [ "npm", "start" ]
Now our Dockerfile
should finally look like this:
# Comments
FROM node:16
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 3000
CMD [ "npm", "start" ]
Note that it is the general structure of a Dockerfile, but every other application may need extra care. Remember, Google is your best friend; always go to the documentation whenever you get stuck.
.dockerignore
Now, if you think about a real scenario, our source folder will not only have the essential files, but there will be many files we do not want to include inside our containers, such as compiled binaries, temporary files, test files, git information, or a README.md file from your image to keep it as small as possible. Here we use the .dockerignore
file.
This file is a list of patterns that tells Docker which files and directories to ignore when building an image.
Create a .dockerignore
file and open that up:
touch .dockerignore
And include these lines in there:
node_modules
.git
.idea
test/*
Dockerfile
.dockerignore
docker-compose*.ymal
This .dockerignore
file tells Docker to ignore the node_modules
directory, the .git
directory, the .idea
directory, any docker-compose
file, and the test
directory; also, we don't want to include the Dockerfile
inside our container, not even the .dockerignore
.
Build the image
To build a Docker image from a Dockerfile, you can use the docker build
command. It reads the instructions in the Dockerfile and creates an image based on those instructions.
Here is the basic syntax of the docker build
command:
docker build [OPTIONS] PATH
PATH
is the path to the directory that contains the Dockerfile and the application source code.
Here are some common options for the docker build
command:
-t
or--tag
: Specifies the name and optionally a tag to the name in thename:tag
format.--file
: Specifies the name of the Dockerfile (default is"Dockerfile"
).--build-arg
: Sets build-time variables for the image.
Here we will build a Docker image from a Dockerfile located in the current directory:
docker build -t my-node-app .
This command will build an image with the tag my-node-app
using the instructions in the Dockerfile
located in the current directory (.
). Now if you run docker image ls
you can see an image named my-node-app
which is the image we build. And if we run that image as a container the app will print "Hello World!" into the browser.
There are also some other types of ways to build an image:
To build an image from a Git repository, you can use the URL of the repository as the PATH
argument:
docker build -t my-image https://github.com/user/repo.git
This command clones the Git repository at the specified URL, build an image with the tag my-image
using the instructions in the Dockerfile
located in the repository, and then deletes the cloned repository.
You can also build an image from a Dockerfile located in a remote repository by specifying the URL of the Dockerfile as the PATH
argument:
docker build -t my-image https://github.com/user/repo/blob/master/Dockerfile
This command will download the Dockerfile from the specified URL, build an image with the tag my-image
using the instructions in the Dockerfile
, and then delete the downloaded Dockerfile.
Run the Image as a container
Now to run the image as a container, we will use the following:
docker run -p 3000:3000 -d --name node-app my-node-app
Environment variables:
Now, if you watch the code of the index.js
file, there is a line const PORT = process.env.PORT || 3000;
that determines the PORT
the application will listen. Here we expect to take the PORT
as an Environment variable, but we are hardcoding it everywhere. How can I set an ENV VAR in a container?
We use-e
or --env
flag when running a Docker container using the docker run
command.
Here is the basic syntax of the docker run
command with the -e
flag:
docker run -e VARNAME=value image [command] [arguments]
Now we can replace our docker run
command with the new one:
docker run -e PORT=3000 -p 3000:3000 -d --name node-app my-node-app
But In a real scenario, we may have multiple Environment variables such as Database URL, Authentication Key, API key, etc., and it is impractical to add all of them one by one in the command line.
Instead, we can also pass environment variables to a Docker container using a file which is a more practical way when we have a bunch of ENV variables. To do this, we can use the --env-file
flag and specify the path to the file that contains the environment variables. The file should contain one environment variable per line in the VARNAME=value
format.
To pass the environment variables from a file called .env
to the Docker container, we can use the following command:
docker run --env-file .env -p 3000:3000 -d --name node-app my-node-app
Using environment variables can be a helpful way to configure the Docker containers and customize their behavior at runtime.
We should also change the Dockerfile
where we have specified to EXPOSE PORT
. We can replace it with
ENV PORT 3000
EXPOSE $PORT
Here if we specify the PORT while running, it will take that PORT, and if we do not pass anything, it will use the default 3000
.
Link to Part 3
In part 3, we will dive deeply into docker volumes, and more on next parts.