Transcription
In this video, we will cover everything you need to know about Docker to get started, from an overview of the basic concepts and installation to basic commands for working with Docker and packaging your own containers. This video is aimed at those who are just getting acquainted with Docker, but even those who have already tried working with it may find something useful.
In the description of the video, there will be a link to a Telegram channel where you can find all the commands used, a brief summary of this video, and a link to the repository with the code so you can replicate everything we discuss here.
Don't forget to like and ask questions in the comments. Now, let's get started.
What is Docker?
Let's begin with what Docker actually is. Simply put, Docker is a tool, essentially a regular program, that simplifies the process of developing, launching, and distributing software.
But do we really need a separate tool for all these operations? After all, we can install, run, and delete a program without any help from Docker.
Let's explore the problems that Docker solves.
Imagine we are developing a web application, say for AI image generation, like DALL-E. It consists of a web server, a web client, an image generation service, an image processing service, and many other systems. Each component requires certain libraries and may depend on others. These dependencies can intertwine, and different parts of our service may depend on the same libraries and components.
What do we do if one application requires an updated dependency while another application runs on an older version? With Docker and the containerization of each application, this problem can be easily resolved. Each container isolates its environment, and each container has its own versions of libraries that do not conflict with each other due to this isolation.
Another problem in software development is portability across different operating systems. If you are using macOS or Windows for work, when deploying to a remote server running Linux, often what worked locally does not run on the server. This is especially noticeable when using compiled programming languages. If you compile a Go program on a local Mac machine, that same version will not work on Linux or Windows.
A step towards greater portability of programs across systems was the programming language Java, with its virtual machine and the slogan "write once, run anywhere." The compiled bytecode runs on top of the virtual machine and works the same on any platform where the Java virtual machine is present.
Docker and containers essentially allow you to do the same, not only for programs written in other programming languages but also to run entire services in these containers on various operating systems, as long as Docker is installed on those systems.
Of course, Docker shines in the development and deployment of microservices. Managing dozens or hundreds of services and performing continuous deployment becomes a simple task described by regular configuration files.
Now, let's dive into Docker itself, starting with an overview of the basic concepts and components.
Basic Concepts of Docker
To start working with Docker, it's important to understand the main components.
The first is the Docker Engine, which is essentially a client-server application consisting of a server that creates and manages container images and a command-line tool for interacting with this daemon via the provided IP.
Now, let's talk about the main components that Docker works with. First and foremost is the Dockerfile, which contains instructions on how to create an image. This is a template based on which containers are created and run, representing a set of files, directories, symbolic links, and necessary tools.
Next is the Docker container, which is an application built on top of Docker images that runs in a closed filesystem with its own process space.
The easiest way to understand this is through a real-life example. Imagine we want to bake cookies of a certain shape and size. First, we need to draw a blueprint of how the baking mold will look. Then, based on this blueprint, we must create the mold itself. With this mold, we can make as many cookies as we want.
In this example, the blueprint is the Dockerfile, the baking mold is the image, and the cookies are the containers. Creating the baking mold based on the blueprint is the image build process, and the baking process is the creation and running of the container.
This sequence of actions is typical when working with Docker, but we will discuss it in more detail later.
For now, let's focus on installing Docker.
Installing Docker
The easiest and most convenient way to install Docker is to use Docker Desktop. Docker Desktop includes everything you need to work with Docker. In addition to the standard Docker Engine and CLI, Docker Desktop includes other useful tools, including a convenient graphical interface.
Installing Docker is equally simple on Mac, Windows, or Linux. You just need to download the file and follow the standard installation process.
You can check if Docker is installed by using the command `docker -v`. If you see the version of Docker, everything is okay.
Docker Desktop runs like a regular program. After installing Docker Desktop on your computer, you will also have access to a convenient graphical interface for working with Docker.
We will primarily work with Docker from the command line, but for clarity, we will also take a look at the graphical interface. Here, you can see running and stopped containers. For now, it is empty.
Images are available both locally and in your remote repository, along with build history and much more.
Now, let's move to the console and run our very first container.
Basic Commands for Working with Docker
All Docker commands are executed using the command-line tool. To run a container, you simply need to execute the command `docker run` and specify the image tag.
Let's execute the command `docker run ubuntu`. This command will launch a container based on the latest Ubuntu image. Since this is our first run and we do not have the Ubuntu image on our computer, it will also download the image files from the Docker Hub.
However, this container will stop immediately after starting because there is no running application inside the container. This is the logic of how containers work.
To avoid this, we can run the container in interactive mode. For this, we will pass the `-i` flag. In this case, the console input/output will be attached to the container, and the container will continue to run. To emulate a standard terminal, we also need to pass the `-t` flag.
Let's write `docker run -it ubuntu`. Now, in the command line, we can work with the terminal as if it were a real Ubuntu system. For example, we can list directories using the `ls` command.
While the interactive mode is active, the container will run, but as soon as we exit this mode, the container will stop. You can exit the container using the `exit` command.
Let's write `exit`.
Now we have exited the container.
To see running containers, you can use the command `docker ps`. Right now, we see that there are no running containers.
To see all containers, including those that are not running, you can use the same command but pass the `-a` flag.
Let's write `docker ps -a`.
Now we see two containers with the status "exited." The first container stopped immediately after starting, and the second one stopped when we ran Ubuntu in interactive mode.
This output also shows the image on which the container was created, the time of launch and operation, and the unique identifier.
To see all images, you can use the command `docker images`.
Now we see the Ubuntu image with the tag "latest." The same information can be obtained through the graphical interface.
Now, in the containers tab, we see these two containers, and in the images tab, we see the Ubuntu image.
Let's return to the console.
A container can be run in what is called detached mode, and this is usually how it is done. For this, you need to pass an additional `-d` flag.
Let's run the Ubuntu container in this mode. We will write `docker run -d -it ubuntu`.
In response, we see the ID of the created container. In this case, the terminal does not open immediately, and the container runs in the background, regardless of whether there are running processes inside it.
Now, entering the command `docker ps`, we see this container. In the graphical interface, running containers are displayed in green, while stopped ones are in gray.
Now, if we want to attach to the running container, we will use the `attach` command and pass the container ID.
You can detach from a running container without stopping it using the `Ctrl + P` and `Ctrl + Q` commands. This way, we exit the interactive mode without stopping the container.
To stop a container, you can use the `stop` command.
Let's write `docker stop` and the container ID.
Entering the command `docker ps`, we see that there are currently no running containers.
You can restart a container using the `start` command.
Let's write `docker start` and the container ID.
Now this container is running.
We have learned to run containers based on existing images, and this knowledge is enough to start using Docker and experiment with various images on your computer, whether they are databases or web servers, without worrying about the main operating system. Docker reliably isolates everything that happens inside.
On the Docker Hub website, you can find available images of operating systems, databases, programming languages, and everything that may be useful for your work.
Working with Ports
Okay, let's assume we are developing a website and need an Nginx server for our work. Let's run it in a Docker container and do this immediately in detached mode.
We will write `docker run -d nginx`. Here, we also do not find the image locally, and it gets downloaded.
Okay, the container is running. Let's check if it is indeed working using the command `docker ps`.
In addition to the previously launched container, we also see the new container, which is using port 80.
Let's try to access it by hitting that port in the browser. We see that the server is unavailable. The server is currently running in a container, but there is no external access to it because the required port is not open.
Open or exposed ports are the main way to interact between containers.
Let's stop the current container and start a new one with an open port. For this, we will write `docker stop` and pass the ID of our container.
Okay, to run the container with an open port, we need to pass an additional `-p` flag and specify which port of the operating system we are forwarding.
Now, by going to the browser at `localhost:8888`, we will see the welcome page.
What we just observed is one of the main features of containers: you can run as many different containers as you need, and they will be reliably isolated from each other, isolating their own dependencies, and interacting with each other and the outside world only through pre-defined open ports.
Creating Docker Images
Running ready-made images can be useful, but as developers, we are primarily interested in how to create new container images where we will place our applications and then run containers from these images.
Seeing the welcome page of the Nginx server is not very useful; more often, we want to host our own content on the web server, say, displaying product information and more.
Let's fix this by creating our own image based on the already used image.
We will create a project folder called `docker-nginx` and navigate to it.
Let's open this folder in a code editor.
Now, let's create our own static HTML page that we want to see as the start page when accessing our server. For this, we will create a file called `index.html` and write the code.
First, we will change the title to "Hello From Docker." Next, let's add an H1 header in the body of our document that says "Hello World From Docker."
Let's also add a Docker image.
Let's add some custom styles to center the text.
Okay, everything is ready. Now let's package this into a Docker container.
For this, we will create a Dockerfile, which is a file that is simply named `Dockerfile` without any extensions.
The structure of the Dockerfile is very simple. It usually starts with the `FROM` instruction, followed by the base image. Then comes a set of commands with arguments. In our case, the base image will be `nginx`. After the name, you can specify a specific version; if you do not, the latest version will be chosen.
Next, our instruction will set the working directory in the container using the `WORKDIR` command. We need to place the static files in the `html` folder in the container, which is located at `/usr/share/nginx/html`.
So we need to write `USER SHARE/nginx/html`.
Now we just need to copy our `index.html` file into this directory. For this, we will use the `COPY` command. The dot serves as a pointer to the working directory.
So we write `COPY index.html .`.
Our Dockerfile is ready. Now let's go to the terminal and create an image based on this file.
For this, we will use the `build` command. We write `docker build .` since the Dockerfile is in the same directory from which we are executing the commands.
Next, we enter a tag or name for our image. Let's call it `nginx-hello-world`.
In the build logs, you can see the steps that were taken. First, the base image `nginx:latest` was used. Then the working directory was set, and finally, the `index.html` file was copied into this working directory.
Let's check our images now. We will enter `docker images`.
We see that in addition to the previously downloaded Ubuntu and Nginx images, we now also have the image `nginx-hello-world` with the tag "latest" since we did not specify a specific version during the build.
Now let's run a container based on our image and forward the port.
It took us just a couple of lines of code to package our web page.
Note that we currently have two servers running in parallel on our system: one on port 9999 and the other on port 8888, and they do not conflict with each other thanks to the isolation of Docker containers.
Now let's consider a slightly more complex example. We will write our own web application in Go that will also greet us.
For this, we will create a new directory called `docker-go` and navigate to it.
Let's open it in VS Code.
Now, let's write the application itself. We will create a file called `main.go`.
We write `package main`. We will need the `fmt` and `http` packages, so we will write `import "fmt"` and `import "net/http"`.
Now, let's write the `main` function, which is the entry point of our application. In it, we will add a request handler function for the root path, which will be the `helloWorld` function, and we will run our server on port 8080.
Now, let's describe the request handler function. We write `helloWorld` with the signature that takes `http.ResponseWriter` and `*http.Request` as arguments, but we do not need the request.
Inside, we will write `fmt.Fprintf(w, "Hello From Go in Docker :)")`.
The code is ready. Now let's package this application in Docker.
We will also create a Dockerfile for it. In it, we will specify the base image, which will be `golang:1.12.21`.
Next, let's set the working directory to the `app` folder in the container, where we will copy the source code and from which we will run our application.
We will copy the `main.go` file using the `COPY` command.
As we can see, up to this point, the Dockerfile structure is very similar to the previous Dockerfile, but now we need to do more than just copy the source code; we need to compile it into an executable file and run that executable.
For this, we will need two new commands. The first is the `RUN` command, where we can specify what actions we need to take during the image build. In our case, we need to compile the program.
Let's do this by writing `RUN go build -o hello.go main.go`.
After compiling, we need to run this program using the `CMD` command. This command is followed by an array of command-line arguments for execution. We just need to run the `hello.go` file without additional arguments, so we write `CMD ["hello.go"]`.
That's it; our Dockerfile is ready.
Now let's build the image and run the container using the familiar commands.
Let's tag our image, say `1.0`.
We write `docker build -t go-hello-world:1.0 .`.
In the logs, we can also see the steps taken to build our image. The build is complete.
Let's check our images using the command `docker images`.
We see the Go image with the tag `1.0`.
Now let's run a container based on this image, forwarding port 5555 to port 8080 inside the container.
We will write `docker run -p 5555:8080 go-hello-world:1.0`.
Now we have a container running the Go program among other running containers.
Let's go to the browser and see that everything works as expected.
This means we no longer need to worry about the portability of binary files between platforms and building applications for each required architecture. Docker will take care of all of this.
Summary
We have covered the basic concepts and commands for working with Docker. For those who are just getting acquainted with this tool, it may seem complex and confusing. I recommend repeating everything we have done here on your own.
A brief summary of the basic commands and a link to the GitHub project can be found in the description of this video.
Also, try to experiment and create something new on your own.
In the next video, we will take a closer look at working with remote repositories and multi-stage builds in various programming languages, as well as alternatives to Dockerfiles.
Subscribe so you don't miss anything.
As always, thank you for watching, and see you in the next video!