Docker for developers

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 logo

Introduction

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:

REPOSITORYTAGIMAGE IDCREATEDSIZE
redis7.0.15d1241776ca744 weeks ago130MB
redislabs/redisinsight1.13.176f704dba29e15 months ago1.15GB
nginxalpine2b70e4aaac6b3 months ago42.6MB
postgreslatestb0b90c1d95795 weeks ago425MB

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 IDIMAGECOMMANDCREATEDSTATUSPORTSNAMES
9ddf07593a31golang:latest"bash"4 minutes agoUp 4 minutesgo

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 🤝.

golang