Published: 01 April 2020
DockerFile
Dockerfile is a way to write instructions for docker so it can perform the tasks as needed.
Think of it as using Terminal on Mac/Linux except you write the instructions once and group them together in a file for Docker to run in the future.
There are many 2 basic parts to any dockerfile:
# DockerFile
# any image can be used as long as it fulfills our system requirements
FROM alpine
RUN apk add --update redis
CMD ["redis-server"]
In the above example, FROM, RUN & CMD are the pre-defined names/types of instructions that Docker recognises. Each of these are followed by the actual commands to execute.
We can now execute this dockerfile inside a directory like so:
docker build .
NOTE: The . in the above command represents the build context or the location of the directory where Dockerfile lives. So, if it lives in the base of a directory/project, and we are executing the above command from the base of the directory, we can simply run the above command as is. Otherwise, we will need to either switch directories or specify the correct path.
This will give us an ImageID which we can use to start a container.
docker run <ImageID>
Build Process In-depth
Let's use the above dockerfile example to explain what goes on behind the scenes:
An interesting feature of Docker is its ability to cache everything. So, if we run the above build command again without making any changes to the Dockerfile, it will be lightening quick because Docker will realise it has already run these steps before and so will serve everything from its memory/cache.
NOTE: If we change the order of the steps and re-run the build command, Docker won't use cache for any steps starting at the line where the change took place.
Tagging an Image
docker build command gives us an ImageID but its harder to remember. So, we can add a name to the image during build and refer to it during our run command.
docker build -t <my-image-name>:<version> .
NOTE: adding a version number is a good practice to differentiate updates made to Docker configuration.
docker run <my-image-name>
NOTE: if no version is specified, the latest version will be used automatically by Docker.
Simple NodeJS Example
Let's try to run a simple nodejs server on our local machine using Docker and Dockerfile. After creating a simple nodeJS project and installing express in it, create a simple GET route which sends back "Hello World". Then create a dockerfile in the root of the project.
#dockerfile in the root of the project
FROM node:alpine
RUN npm install
CMD ["npm","start"]
FROM --> we specified an "alpine" image for NodeJS which contains bare bones installation of Node and some other default commands.
RUN --> we will need all dependencies installed
CMD --> finally we will start up our server
The above Dockerfile will throw an error though because it won't find package.json file. Why??
Remember Docker creates a container which is isolated and has no access to our local machine. Even though the Dockerfile sits in the root of our project, it doesn't have access to any files in that project.
We need to make our files available to Docker for it to run the commands successfully. This can be achieved by copying over the files to Docker Container like so:
COPY . .
The COPY command takes in 2 arguments - the source location and the destination. In our case, we want to copy everything from the root of our project to the root of the Docker Container.
#dockerfile in the root of the project
FROM node:alpine
COPY . .
RUN npm install
CMD ["npm","start"]
COPY
We will find that with the above dockerfile, if we make any change to our code base in NodeJS, Docker will run all steps again including npm install. This is not ideal on bigger projects where we may have many more dependencies and it will take long time to install all those. So, let's only copy package.json first and then all the rest of the files.
#dockerfile in the root of the project
FROM node:alpine
COPY package.json .
RUN npm install
COPY . .
CMD ["npm","start"]
The above step will ensure that dependencies are installed only if we make changes to package.json and not our own codebase.
WORKDIR
Currently, we are storing package.json file and node_modules folder at the root of our container which isn't ideal because it may create conflicts with other folders in that location.
Usually, its best to create your own folder for our app. We can achieve this with WORKDIR command which tells Docker the context of the current project inside the container.
FROM node:alpine
WORKDIR /home/myapp
COPY package.json .
RUN npm install
COPY . .
CMD ["npm","start"]
Now if we run docker build . docker will work in the folder we specified. If the folder doesn't exist, it will be created.
Port Mapping
Even though the above dockerfile will start a server, we won't be able to access it. The same pricinple as before - the container is isolated from our machine, so chrome or any other application on our machine won't find anything running on the port specified in our NodeJS project.
We need to tell docker on run time to re-route any traffic from the local port on our machine to its own container's port like so:
docker run -p <port-on-our-machine>:<port-inside-container> <imageName>
docker run -p 8000:8080 mySampleContainer