[ELI5] Docker: Basics
If you’re like me, then you’re having a hard time wrapping your head around Docker. Like “What dark magic allows you to instantly run an application with only one command?” Well, we’ll get right into it!
Docker is a containerization platform that packages all the code and its dependencies in one image, essentially all your code in one file! You can think of it as a pre-built computer and all you have to do is turn it on and run it!
So what’s all this jazz about containers, when we have tools like make that automatically recompile large programs for us?
The answer is that Docker is so much more flexible and simpler than anything else. Unlike a make command determining the compatibility of your OS, Docker containers have their OWN OS and file system separate from the computer, so it will always run no matter what! I like to remember it like this; Docker’s command line is like a virtual machine, less intensive and perfectly lightweight.
So the best way to understand something is by doing! So let’s open up our code editor and get started!
Installing Docker
Depending on your machine. you will need to install the appropriate Docker Engine. Otherwise, I am working on Mac with Catalina.
If you have brew, go ahead and install it with:
brew cask install docker
This will install Docker Hub and its command line tools, we can verify this with:
docker --version
Using Docker
From here let’s create a simple flask app in a folder! We can imagine the contents of the folder hosting the flask app as its own file system. Now we can go ahead and make a Dockerfile
, a Dockerfile is basically the instructions needed to create your Image.
touch Dockerfile
Each line in a Dockerfile is a layer and basically, they're the instructions to construct your image.
Let’s start with FROM, if you are familiar with programming, you can think of this as importing a library to use or build off of. In this case, we are not working with libraries, but Base Images. Base Images are Images that are pre-configured operating systems such as python, ubuntu, alpine, and many more on docker hub. These Base Images let you do a lot less work to set up.
So because I am using a python based application, it’s a good idea for me to use python:3. Wait why did I follow a Base Image Python name with a “:3”? This is a tag — a specific build of the Base Image Python. We usually denote our base images with a specific build or latest
while having a colon separating them. It’s good practice to use a specific build so the image will always work, otherwise, a new latest
build may break your application.
The rest is now smooth sailing. Now we need to figure out what our app needs and ‘explain’ those instructions in the Dockerfile
.
Does it need?:
- Libraries / Extra Modules
- Environment variables
- Ports
- Commands it should run
There are more, but these are the most common features an application needs and don’t worry, there are plenty of layer options to tailor to your application needs.
So after we specify a base image, we need to create a directory [or folder] to work in and copy the contents over. We can do this with WORKDIR
and COPY
.
# Base Image
FROM python:3# create the work directory app
WORKDIR /app# Copy the current content of where the
# Dockerfile is to WORKDIR /app
COPY . /app
Now that we have all the contents in the workdir, we still need to install our packages! So hopefully you have a requirements.txt
, if not do:
pip3 freeze > requirements.txt
If you are working with a language or framework that has its libraries localized like Node.js, it is preferred to either add the folder with the libraries in .dockerignore
or copy only the package*.json
over first and install the libraries. Then, copy the rest of the application over, otherwise, it will take a while to build.
The code should look like this:
FROM node:alpineWORKDIR /serverCOPY ./package*.json /serverRUN npm install
Now we need to tell Docker to run a command to install all the requirements, so we use RUN:
RUN pip3 install -r requirements.txt
Now we need environmental variables for the flask app, for this specific case we need FLASK_APP
which is app.py, and the FLASK_RUN_HOST
ENV FLASK_APP=app.pyENV FLASK_RUN_HOST=0.0.0.0
So far simple right?
Next is for documentation purposes: What port does the application run on? Flask on default runs on 5000
so:
EXPOSE 5000
You can specify a protocol with [port]/[protocol]
as well, if needed. Now, all we need to do is run the command to start the server, but wait! I know that you are thinking, “but we don’t use RUN
here, instead we use CMD
!
CMD [“flask”, “run”]
What’s the difference? Well the RUN
instruction is a layer built on top of the base image, and CMD
is used as a default command that can be changed in the command line. So instead of the RUN
instruction that can’t be changed, we prefer CMD
in case we want to do python3 app.py
or any other different command.
Notice the syntax as well, we are passing the CMD
instruction JSON data, instead of CMD flask run
. The JSON one is called exec form instead of the other shell form. The exec form is usually preferred over the shell form for CMD.
Always refer to Docker documentation to check which is preferred!
Now we are done! Your Dockerfile should look like this:
FROM python:latestLABEL GithubArthur NinjaAungWORKDIR /appADD . /appENV FLASK_APP=app.pyRUN pip3 install -r requirements.txtEXPOSE 5000CMD [“flask”, “run”]
The label is not important. It’s for documentation purposes
Now, let’s run it! Go ahead and open Terminal and type:
docker build --tag docker-flask .
--tag
is naming the image that docker is building.
is the location of Dockerfile which is in the current directory
Next, we need to run it.
docker run -p 80:5000 -d --rm --name flask_app docker-flask
-p
or--publish
, however I prefer to remember it as port, because we are connecting our[local Port}
to inside our docker container[container port]
e.g[local Port]:[Container Port]
-d
or—-detach
, run the container in the background, if you want to see what the application is happening remove the-d
--rm
, deletes the container after you stop the process--name
, is the name of the container you are running otherwise docker will give it a randomly generated name e.g “busy-box’docekr-flask
, is the name of the image we are using in the container
And voila, connect to locahost
and you will see your app! Now after we are done appreciating your hard work, let’s close it down.
docker stop flask_app
and if you did not include —-rm
, we can follow the command with:
docker rm flask_app
Docker-Compose
Wait, you’re not satisfied with building, running, and specifying the port every time? Well be not afraid, there is docker-compose.yml
, it has a lot of power and is used to handle multiple containers, but we will go over that next time. All you need to know about this application is this:
version: “3.3”
services:
web:
build: .
ports:
- “5000:5000”
version
is specifying what version the docker-compose file is inservices
is a collection of named services you have / collectionweb
is a type of servicebuild
is the path to the Dockerfileports
is the same as the command — publish [local]:[Container]
Then to run it:
docker-compose up
and that’s the [ELI5] basics of Docker!