The first step to start learning docker is to distinguish two word, image
and container
. By knowing them you can go deeper and learn some useful command to work with docker in your projects for development and also use it for easy deployment. Docker also has some advance concepts like Docker Swarm
that it is an orchestration tool. You will also learn some basic knowledge about it in this post.
Docker is one of the most important tools in the world of software engineering. If you transfer the code written on your laptop to another laptop, you may face problems such as the absence of required tools and dependencies, and probably your solution is to install all the required items, but there is another problem, and that is that you may need packages and frameworks in different versions for different applications, in this case managing different versions and using the correct version will be a problem. This is one of the problems that docker will solve for us. Docker solves the problem of dependencies by creating an isolated environment that has a separate kernel from our main operating system for each running application. Also, unlike virtualization, in docker every isolated environment called a container, if a common dependency with other containers, there is no need to copy it in a new container and it can use the same one.
Well, to start working with Docker, you need to install it first. You can go here to download Docker according to your operating system and hardware. Docker has a very long history, which we will not discuss here, so we will go directly to working with Docker. For the first step, you need to pull a Docker image from a Docker registry (we use the official Docker registry (Docker Hub)). We will use official Redis image here. We pull the Redis image with the following command:
docker pull redis:latest
Wait until all image layers are downloaded. After that, you can check the available images with the following command:
docker images
or
docker image ls
You should see a table like the one below but just one row:
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
---|---|---|---|---|
redis | 7.0.15 | d1241776ca74 | 4 weeks ago | 130MB |
redislabs/redisinsight | 1.13.1 | 76f704dba29e | 15 months ago | 1.15GB |
nginx | alpine | 2b70e4aaac6b | 3 months ago | 42.6MB |
postgres | latest | b0b90c1d9579 | 5 weeks ago | 425MB |
Now it's time to talk about the difference between image and container. A container is an isolated running application or service, while an image is a template for launching and running a container. The image is actually not a process until we run a container by copying it, at this time it is called a container. If we want to see active and running containers:
docker ps
Likewise, if we want to see all the previous containers that have been stopped, we use the -a
flag:
docker ps -a
Now let's bring up a golang container in a practical way and execute some command and code inside it. The first step is to pull the Golang image:
docker pull golang:latest
After it is downloaded, you can see it in your image list. Now we can create a container from this image:
docker run -dt --name go golang:latest
By running this command, you will see a SHA256 hash in the terminal:
9ddf07593a31514d3e3213c9d67024e1ed227ba81d2fd2521a165cf68ce09c3e
Now, if you check the list of running containers, you will see something like this:
CONTAINER ID | IMAGE | COMMAND | CREATED | STATUS | PORTS | NAMES |
---|---|---|---|---|---|---|
9ddf07593a31 | golang:latest | "bash" | 4 minutes ago | Up 4 minutes | go |
From this table, we can understand that a container is running, its ID
is written in the first column, and in the second column, we can understand what image this container is made of. In the third column, it shows the initial command of the container. If you are familiar with Dockerfile
and its Instructions
, you should know that the initial command is determined by CMD
or ENTRYPOINT
. We can also understand from the ports column that the container we ran did not provide us with any port to access the container from the host.
Now we can connect to the container and write a piece of Go code and run it(Instead of the name of the container, you can put an initial part of the ID so that it is clear which container is meant):
docker exec -it go bash
Now we have access to an interactive environment inside the container. To run the Go code, we first need to write the code in a file and save it. (you can use any terminal text editor, even if it was not installed in the container, you can install it, of course, this is not a standard method, and later you can use other methods for We will discuss putting the file inside the container, but for now we will do it this way). I use the following simple code:
package main
import (
"fmt"
"io/ioutil"
)
func writeFile(filename, content string) error {
return ioutil.WriteFile(filename, []byte(content), 0644)
}
func readFile(filename string) (string, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
return string(data), nil
}
func main() {
// Write to file
filename := "test.txt"
content := "This is a test file."
err := writeFile(filename, content)
if err != nil {
fmt.Printf("Error writing to file: %v\n", err)
return
}
fmt.Println("File written successfully.")
// Read from file
readContent, err := readFile(filename)
if err != nil {
fmt.Printf("Error reading from file: %v\n", err)
return
}
fmt.Printf("File content: %s\n", readContent)
}
You can run the code from inside the container. But we have to find a better method because in this method the possibility of error is very high and also if the size of the project and the number of files are very large, then copying the files individually into the container is very difficult.
I have to summarize one point that you can create custom images by writing Dockerfile
and building them. Here are some of the Instructions
that you need to write Dockerfile
:
- FROM
- WORKDIR
- COPY
- ADD
- RUN
- ENV
- EXPOSE
- USER
- CMD
- ENTRYPOINT
Unfortunately, there is not enough space here to explain the Dockerfile
and its Instructions
, but you can refer to the Docker document for reading. Now let's create our own image, which includes the codes we need by default:
#Base image
FROM golang:latest
WORKDIR /opt/app
COPY . .
RUN go build -o app .
CMD ["bash"]
If we have the above Dockerfile
in the main folder of our project, we can make our image so that our codes are automatically copied into the image. Let me explain the Docker instructions in the above file. In the first instruction, which is FROM
, we determine what our initial image should be, which we will make changes to in the future instructions.
In the second instruction, WORKDIR
, we determine where our starting point is in the file system of the containers that are created from this image. If this folder does not exist, it will be created.
The third instruction needs no explanation! Because our Dockerfile
is located in the main folder of the project, the entire project is copied into the container and in the WORKDIR
location that we specified.
In the fourth instruction, we compiled the project code and output it with the name app
. In general, we can execute any command on the image with the RUN
command. By executing each instruction, we actually add a layer to our image. Unfortunately, there is no place to explain layers
and how to optimize them.
And finally, with the CMD
instruction, we can specify a command that will always be executed when starting the execution of each container from this image (We can change this command when the container starts running).
Now we can create our image:
docker build -t my-app:1.0.0 .
But we usually do not do this. Because in most projects we have more than one service, we need several Dockerfiles
to create the required images, which would be tedious to do manually. So, we have to write the docker-compose.yml
file so that we can start and stop all the services we need at once. Of course, you need to install docker-compose
before proceeding.
version: '3'
services:
go:
build:
dockerfile: Dockerfile
context: .
volumes:
- .:/opt/app
container_name: golang-test
stdin_open: true
tty: true
The syntax of the file is quite clear, so I will not give an explanation. Let me briefly explain about volumes
that we can use them to sync
files and data between containers and host machines.
Here I defined a volume
from the current folder to the folder inside the container where I copied the project codes for easier development. Any change in either side will change the files in the other side and this is very good for when we are developing the project.
To run the services defined in the docker-compose.yml
file, we must use docker-compose
:
docker-compose up -d
And also to stop services at once:
docker-compose down
Finally, in order to run the project more easily without the need to manually connect to the container and run it, I will write a simple Makefile
.
.DEFAULT_GOAL := run
run:
@docker exec -it golang-test go run .
fmt_vet:
@docker exec -it golang-test go fmt ./... && go vet ./...
.PHONY: run fmt_vet
And that's it :)
I hope that I will be able to convey more content in the next articles. Thanks for reading 🤝.