A platform for building, running and shipping applications.
Everyone have stumble upon this case right I works on my machine but not yours? How is that Possible?
Here are the reasons why this happens
- One or more files missing (Application is not completely deployed).
- Software version mismatch (If Target machine is using different version of software).
- Different configuration settings (Like env variables are different accross these machines).
Docker will create a package of our application with all of it's dependencies. Like if our App
needs Node 14
and
Mongo 4
. Then it will create a package of Node 14 + Mongo 4 + App and we can ship this package easily anywhere where
docker is supported.
When a new developer comes in a company. They spend 1-2 days on setting of their machine/env so that project would run smoothly. Now they can use docker and simply run a docker up command and docker will download all the necessary packages, software, dependencies they need to run the project smoothly.
$ docker-compose up
Docker helps to create different isolated environment for different project. Our old App1 supprots Node 9 and our new App2 supports Node 14. Docker can create separate isolated environment for both of them where one env supports Node 9 and other Node 14. Both these application can run side by side on same machine without messing with each other.
As we work on many projects. Our development machine gets clutter with so many libraries and tools that are used by different application and then after a while we don't know if we can remove one or more of these tools because we are afraid we might mess up with our application.
But with docker we don't have to worry about this. It is easy and safe to remove/delete one container/env with all of its dependencies at once without any trouble because these containers are running in an isolated environment
$ docker-compose down --rmi all
An isolated environment for running an application.
An abstraction of a machine (physical hardware)
Virtual Machine
supports isolation and all sorts like what docker does. But Vritual Machine
makes use pay more for these
features:
- Each VM needs a full-blown OS.
- Slow to start (since we are starting entire OS).
- Resource intensive (takes slice of actual host physical hardware resource).
On the other hand Containers
:
- Are lightweight (they don't need full blown OS).
- Use OS(Kernel) of the host (all container uses one OS as host these host OS implementation may vary from diff platforms like mac, windows, linux).
- Start quickly (since host OS is already started launching a container is quick).
- Need less hardware resources.
- Command to pull and run ubuntu image from docker hub (ubuntu image will stop immediately)
$ docker run ubuntu
- To see running docker processes/containers
$ docker ps
- To see running as well as stopped containers
$ docker ps -a
- To start a image and interact with it (
-it
interactive)
$ docker run -it ubuntu
- Start a container (
a2f
starting three letters of container id)
$ docker start -i a2f
$ docker run ubuntu
$ docker run -it --name test-linux --rm ubuntu
-it
interractive, --name
custom name for the container, --rm
removes container after you are done with it NOTE: It doesnot removes image.
session 1
root@2146e9627282:/# whoami
root@2146e9627282:/# root
root@2146e9627282:/# useradd -m santosh
root@2146e9627282:/# usermod -s /bin/bash santosh
root@2146e9627282:/# cat /etc/passwd
root@2146e9627282:/# cat /etc/shadown
Now while this terminal session is running open another terminal and try to login as new user. Here we are not quitting this session
because we want santosh
user to persist for this demo. Because if we stop this container then --rm
flag wil remove anything related to this container
session 2
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a6949b11be48 ubuntu "bash" 5 seconds ago Up 4 seconds test-linux
$ docker exec -it a6949b11be48 santosh bash
sant@a6949b11be48:/$
session 1
root@2146e9627282:/# userdel santosh
For deleting user
$ adduser sant33
Just like for user: useradd
, usermod
, userdel
. We have that for group as well
groupadd
, groupmod
, groupdel
.
We can checkout all the groups
$ cat /etc/group
Creating a new group
$ groupadd devs
Adding user to a group
$ useradd -G devs santosh // devs as seconday group of user santosh
or,
$ useradd -g devs santosh // devs as primary group of user santosh
Get a user group name
$ groups $USER
An Image is OS + libs + files + env variables. Let's say it is a class in programming world and container is object.
Isolated process/environment for executing an application which uses it's image file system but (all new containers are isolated by default). Whatever happens in one container doesn't affect other container
var sessionOneLinuxContainer = new UbuntuImage()
var sessionTwoLinuxContainer = new UbuntuImage()
A Dockerfile contains instructions for building an image.
FROM
: Specifying base image. example FROM ubunti:bionic
.
WORKDIR
: Specifying working dir (after this command next commands will be executed in the dir specified by WORKDIR
. example `WORKDIR /home/app/client .
COPY
& ADD
: Copying files and directories example COPY . /home/app/client .
COPY
vs ADD
: ADD is COPY on steroids which has additional features like downloading from the internet and untar files.
Best Practice Tip: Use COPY by default. You can use ADD if you know what you are doing.
RUN
: For executing os commands. example RUN rm -rf node_modules
.
ENV
: For setting environment variables .
EXPOSE
: Telling docker that our container is staring on a certain port.It does not map ports on the host machine. We need to use -p
flag while running a docker image to expose all published ports. example EXPOSE 3000
. EXPOSE
is only for documentation.
example:
$ docker run -p -t react-app
USER
: Specifying user for running our application by.
CMD
& ENTRYPOINT
: For specifying commands that should be executed when we start a container .
RUN
Vs CMD
RUN
: command triggers while we build the docker image
CMD
: command triggers while we launch/run the created docker image.
Dockerfile
FROM node:16.13.0-alpine3.14
WORKDIR /home/node/react-app
COPY . .
RUN npm install
CMD ["npm", "run", "dev"]
.dockerignore
node_modules/
README.md
$ docker build -t react-app .
$ docker run --init --rm -p 3000:3000 react-app
ENV API_URL=http://api.app.com/
Dockerfile
FROM node:16.13.0-alpine3.14
WORKDIR /home/node/react-app
COPY . .
RUN npm install
ENV API_URL=http://api.app.com/
CMD ["npm", "run", "dev"]
Check env variables in linux
$ printenv
$ printenv <env-name>
$ echo $<env-name>
we cannot use multiple CMD
s inside a Dockerfile
. If we are using multiple CMD
, only the last one will take effect.
Also,
RUN
: Build time instruction
CMD
: Run time instruction
# Shell form
# docker will run this command in separate shell
# in linux it is /bin/sh
# on windows cmd
CMD npm start # it will create a new process, and it will mess up cleaning process
vs,
# Exec form (Always use this command)
CMD ["npm", "start"] # it will run on same process
CMD ["npm", "start"]
ENTRYPOINT ["npm", "start"]
CMD
is pretty flexible we can override it as: docker run react-app echo hello
. CMD
command will be overriden by echo hello
ENTRYPOINT
we cannot easily override command when running a container. It take an effort to override it docker run react-app --entrypoint echo hello
.
overriding entrypoint was made this way so that it would be hard for a user to override it because ENTRYPOINT
is used whenever we are sure that this is the command that is gonna execute and there is no any exception.
But many prefer CMD
because of it's flexibility and easeness.
Docker images are collection of layers. So docker build images by reading instructions from Dockerfile
and creates layer after layer. By using this layer docker can determine which instruction are changed and if they are not changed then re-use that layer from cache otherwise re-build it again.
So let's take a scenario heere:
FROM node:16.13.0-alpine3.14
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /home/node/react-app
COPY . .
RUN npm install
ENV API_URL=http://api.app.com/
EXPOSE 3000
CMD npm run dev
Here
FROM node:16.13.0-alpine3.14
will not be changed so let's say - 1 Layer
RUN addgroup app && adduser -S -G app app
will not be changed so - 1 Layer
USER app
will not be changed so - 1 Layer
WORKDIR /home/node/react-app
will not be changed so - 1 Layer
But,
COPY . .
docker would have to check content of file to verify if it can use cache or rebuild.
So if a file is changed then next command would also get rebuild even though we might not require it like
for example RUN npm install
if we are just changing some logic inside our code we don't need to install packages
again. so this will slow down our build.
In order to solve this issue we have to make package installation a separate thing.
FROM node:16.13.0-alpine3.14
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /home/node/react-app
COPY package*.json .
RUN npm install
COPY . .
ENV API_URL=http://api.app.com/
EXPOSE 3000
CMD npm run dev
So now unless package*.json
file itself haven't been changed we don't have to worry about re-installing things again.
When writing instructions inside a Dockerfile
. Stable instructions should on top and Changing instructions should at lower level.
$ docker image prune
This will not remove anything if those dangling images are used by some containers(stopped or running).If this is the case run below command first then you can remove some dangling images
$ docker container prune
$ docker images
$ docker image rm <image-name/image-id>
$ docker network prune
$ docker network rm $(docker network ls -q)
$ docker container stop $(docker container list -aq)
$ docker rmi $(docker images -aq) --force
$ docker system prune
$ docker volume rm $(docker volume ls -q)
Whenever we pull or build an images docker tags them as latest. It is bad practice to do this.
We have to explicitly tag them in production.
latest
is fine in case of development but for production or staging you have to follow proper tagging system. Because
- If something goes wrong without tagging we cannot troubleshoot easily.
- In future you might have to roll back to previous version in production.
We can use code name like buster
, or semantice versioning 3.2.1
or build count 16
, 17
, 18
. Build count comes handy incase if you are using CI/CD pipeline.
$ docker build -t react-app:1 .
By doing this we will see that IMAGE ID
for every tagged images will be same. But if files are changed then we get different IMAGE ID
.
Removing tagged images
$ docker image remove react-app:1
$ docker image tag react-app:latest react-app:1
$ docker image tag <latest-version-num-image-id/react-app:2> react-app:latest
-
Create a repository in docker (e.g. react-app)
-
Now we have uniquely identified image name /react-app e.g. santoshcode/react-app
-
In local tag your latest image to same repo name i.e. santoshcode/react-app
$ docker image tag <latest-tagged-image-id> santoshcode/react-app
This new locally tagged should have or point to same
IMAGE ID
of latest local version of image. -
Commands
$ docker login $ docker push santoshcode/react-app:2 # here 2 is what local latest version was so.
-
So let's say you added new feature and changed some file now
$ docker build -t react-app:3 . # 3 for new version $ docker image tag react-app:3 santoshcode/react-app:3 $ docker push santoshcode/react-app:3
Next push will be faster if no dependencies are changed.
$ docker image save -o react-app.tar <image-name-with-tag>
$ docker image load -i react-app.tar
$ docker images
$ docker ps
$ docker run <image-name/repo-name>
$ docker run -d <image-name>
$ docker run -d --name tomato-react-app <image-name>
$ docker log <container-id>
$ docker log -f <container-id>
$ docker log --help
$ docker run -d -p 3000:3000 --name <container-name> <image-name>
Here, in -p
flag first 3000 is host machine and second 3000 is container port.
docker run
: We run a new container and run a command.
docker exec
: We execute a command and run a container.
$ docker exec <running-container-name> ls
$ docker exec -it <running-container-name> sh
If you want to open shell as xyz user then
$ docker exec -it -u xyz <running-container-name> sh
We can exit of this session by using exit
command but this will not stop our running container.
docker run
: With docker run
we start a new container.
docker start
: With docker start
we start a stopped container.
$ docker stop <container-id/container-name>
$ docker start c1
Either you stop a container and remove it or you can forcly remove a running container with -f
flag.
$ docker rm -f <container-name/id>
$ docker container prune
NOTE:: We should never store our data in container's file system because if we remove a container then file system will also get removed as well so for persisting data we should use Volumes in container.
Volumes can be directory in the house or some where in the cloud.
We don't have to explicitly create volume before using it. We can do
$ docker run -d -p 4000:3000 -v app-data:/home/santosh/data
This is automatically create app-data volume inside host machine and /data folder inside santosh folder insider container.
$ docker volume create <volume-name>
example,
$ docker volume create app-data
$ docker volume inspect app-data
[
{
"CreatedAt": "2021-11-23T08:34:08+05:45",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/app-data/_data",
"Name": "app-data",
"Options": {},
"Scope": "local"
}
]
Here the Driver is local because we are using host machine to persist our data. If we are going to use colud to persist our data then we need to find suitable driver for it.
Mountpoint is where our data is gonna be. Here in this case it is in our host machine.
For windows mountpoint is gonna be in C drive, For Linux we can use the same mountpoint dir path to see the dir but in case of mac as we know mac uses virtual linux machine to run docker under the hood so when we visit mountpoint path dir in our mac machine then we wouldn't be able to visit it.
$ docker cp <container-id>:<container-path-to-file> <host-machine-location>
example,
$ docker cp 3f3b7d93136d:/home/santosh/log.txt .
$ docker cp <host-machine-path-to-file> <container-id>:<path-inside-container>
example,
$ docker cp secret.txt 3f3b7d93136d:/home/santosh/data
Build a new image and tag it properly and push it to production
We don't want to build a new image every time we change out code. Also we don't want to manually copy our code to container filesystem.
For Development what we can do is use binding. Binding container to our host source code.
$ docker run -d -p 5001:3000 -v "$(pwd)":<container-source-code-path> react-app
example,
$ docker run -d -p 5001:3000 -v "$(pwd)":/home/santosh/react-app react-app
Here, $(pwd) means hosts source code/project dir and later is where your source code resides inside your container.
$ docker container rm -f $(docker container list -aq)
Here, -a
flag refers to stopped containers and -q
flag provides us with all the container ids.
$ docker rmi -f $(docker images -aq)
$ docker volume rm $(docker volume ls -q)