Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Docker Container Development: Virtualization

Tech May 13 1

Docker's core value lies in virtualization, or more specifically, environment isolation. Through virtualization technology, Docker implements virtual environments that solve dependency issues in configuration and deployment, enabling decoupling.

My understanding of virtualization comes from "Operating Systems: Three Easy Pieces", which I highly recommend. For the history of container technology, I suggest reading this article on Zhihu.

Docker Basic Concepts

Docker provides the ability to package and run applications in loosely isolated environments called containers.

  • Image: An image is a read-only template used to create Docker containers.
  • Container: A container is a runnable instance, which is the runtime instance of an image.

The relationship between these two concepts is similar to classes and objects in object-oriented programming.

Docker Ecosystem

  • Docker Registry: A Docker registry is a storage and distribution place for Docker images.
  • Docker Client: A CLI tool used to interact with the Docker server.
  • Docker Server: A daemon process that manages Docker objects such as images, containers, networks, and volumes.
  • Docker Hub: The repository for all custom images, similar to GitHub.

The development of any technology is inseparable from the support of its ecosystem, which is also an important reason for Docker's success. Technologies like Git and GitHub, Node.js and npm, Python and PyPI all have similar ecosystems.

Regarding Docker installation, I won't elaborate here. You can refer to the official documentation: Get Docker.

However, I'm curious about how Docker runs on Windows and Mac since it's based on the Linux kernel. The installation on these platforms is relatively more troublesome. For my practice, I installed it in a virtual machine using Debian.

It should be noted that after installing Docker, Docker-related operations typically require sudo privileges. For convenient use, you should add the user you want to use to the docker group with the command: sudo usermod -aG docker $USER

Using Docker Containers

Creating Containers from Images

The general process of creating a container:

  1. The Docker daemon first tries to find the image in the local repository.
  2. If the image is not available locally, it will be pulled from a remote repository.
  3. After the Docker daemon successfully obtains the image, it creates a container from the image.

Example:

# The hello-world image is used for testing. The following command creates and runs the corresponding container
docker run hello-world

The output would show:

# This indicates the hello-world image is not available locally
Unable to find image 'hello-world:latest' locally

# The following is the actual remote pull and container creation
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:ffb13da98453e0f04d33a6eee5bb8e46ee50d08ebe17735fc0779d0349e889e9
Status: Downloaded newer image for hello-world:latest

# The following is the container output, with everything after the first line omitted for brevity
Hello from Docker!
...

  • The pull operation can be performed separately using docker pull <image-name>[:<version>]
  • The syntax for run is approximately docker run <image-name>[:<version>]
  • Use docker ps to view all running containers. Adding the -a option shows all available containers.

A brief explanation of the column headers:

  • CONTAINER ID: Shows the unique ID of each container
  • IMAGE: The image used to create the container
  • COMMAND: The command executed in the container at startup
  • CREATED: When the container was created
  • STATUS: The current status of the container
  • PORTS: Any port mapping for the container
  • NAMES: The name of the container, which is randomly generated and unique if not set

Let me explain the layers in the image, specifically the 2db29710123e: Pull complete from hello-world:

  • Since each image is built on top of the Linux kernel, it shares some common dependencies that can be reused by other images.
  • Docker bundles these dependencies in a stack called layers.
  • Only RUN, COPY, and ADD instructions create layers; other instructions create temporary intermediate images that don't increase the build size.
# Test pulling a larger image like nginx
docker pull nginx

The result shows:

Using default tag: latest
latest: Pulling from library/nginx

# Divided into five layers
f1f26f570256: Pull complete 
7f7f30930c6b: Pull complete 
2836b727df80: Pull complete 
e1eeb0f1c06b: Pull complete 
86b2457cc2b0: Pull complete 
9862f2ee2e8c: Pull complete 
Digest: sha256:2ab30d6ac53580a6db8b657abf0f68d75360ff5cc1670a85acb5bd85ba1b19c0
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

  • Layering doesn't depend on image size; it's determined by the designer's approach to common dependencies. Sometimes a small image can have many layers.

For interactive mode containers, let's use python:3.6 as an example. First, create the container with docker run python:3.6.

  • When using ps to check, you'll notice no containers are running because the container wasn't actually running. You need to use the -a option to see all containers.
CONTAINER ID   IMAGE         COMMAND     CREATED             STATUS                         PORTS     NAMES
8cc71dccbb99   python:3.6    "python3"   10 minutes ago      Exited (0) 10 minutes ago                cool_mendel
fba35ad4d200   hello-world   "/hello"    About an hour ago   Exited (0) About an hour ago             elastic_wing

We've been saying that Docker uses the Linux kernel for containers. Let's create an interactive bash container.

When running a container, use the -it option (which is actually two options used together). The syntax is: docker run -it <image-name> bash

  • -t is the full option --tty: It allocates a pseudo tty.
  • -i is the full option --interactive: It keeps STDIN open even if not connected. The meaning of "interactive" in Chinese helps understand this.
# Create an interactive tty container with python:3.6
docker run -it python:3.6 bash

The result shows we're now in a virtual Linux operating system:

root@84779de65a0b:/#

Interactive process:

# Check your permissions with id
root@84779de65a0b:/# id
uid=0(root) gid=0(root) groups=0(root)

# Exit with exit. The system warns that if you exit the container, it will stop. You can verify with docker ps
root@84779de65a0b:/# exit
exit
learn@debian10:~$

Now we can create containers with accessible bash.

How to Use a Stopped Docker Container Again

Through docker ps -a, we already have three containers. Now let's learn how to run a stopped container.

docker ps -a

CONTAINER ID   IMAGE         COMMAND     CREATED          STATUS                      PORTS     NAMES
84779de65a0b   python:3.6    "bash"      8 minutes ago    Exited (0) 3 minutes ago              romantic_meninsky
8cc71dccbb99   python:3.6    "python3"   37 minutes ago   Exited (0) 37 minutes ago             cool_mendel
fba35ad4d200   hello-world   "/hello"    2 hours ago      Exited (0) 11 minutes ago             elastic_wing

  • The syntax is docker start <container-id>. We'll choose romantic_meninsky (created earlier with docker run -it python:3.6 bash). By analogy, to stop a running container, use docker stop <container-id>, and to restart, use docker restart <container-id>.
learn@debian10:~$ docker start 84779de65a0b
84779de65a0b
learn@debian10:~$ docker ps
CONTAINER ID   IMAGE        COMMAND   CREATED          STATUS         PORTS     NAMES
84779de65a0b   python:3.6   "bash"    16 minutes ago   Up 6 seconds             romantic_meninsky

The romantic_meninsky container is running again. You can log back into the container's bash with docker exec -it <container-id|container-name> bash. The | means "or", so you can use either the container ID or name. For example: docker exec -it 84779de65a0b bash is equivalent to docker exec -it romantic_meninsky bash.

docker exec -it 84779de65a0b bash

The result shows we're back in the romantic_meninsky container:

root@84779de65a0b:/#

Operations inside the container:

# We'll install some tools in the container so we can write Python scripts
# Change the apt source (faster with domestic mirrors) with: sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
root@84779de65a0b:/# sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list

# Update apt cache with: apt update
root@84779de65a0b:/# apt update

# Install an editor of your choice (vim, nano, etc.)
root@84779de65a0b:/# apt install vim

# Create hello-world.py with the editor
root@84779de65a0b:/# vim hello-world.py

In the vim editor, press 'i' to enter edit mode:

print("Hello World")

In vim, press Esc to switch modes, then type ':wq' to save and exit.

# Run hello-world.py. This is just for testing, but you could write other Python programs and run them
root@84779de65a0b:/# python hello-world.py
Hello World

# Exit
root@84779de65a0b:/# exit
exit

  • We can use this method for container deveelopment.
  • In actual development environments, integration with editors like Visual Studio Code is also common.

Creating Images from Containers

After these operations, the container has undergone changes. Now let's package this container as an image. The syntax is docker commit -m "<commit-message>" <container-id|name> <new-image-name>:<version>. This is somewhat similar to a Git commit operation, where -m adds a comment message.

docker commit -m "python36 Hello World Test" 84779de65a0b my-python36-hello-world:1.0

The result will output a SHA256 hash as the checksum for the generated image:

sha256:2f12c4d8f189633c4b0b58e6496f429f125a125cdd47e016991af721569a763e

You can view the local image repository with docker images:

REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
my-python36-hello-world   1.0       2f12c4d8f189   2 minutes ago   958MB
nginx                     latest    080ed0ed8312   7 days ago      142MB
python                    3.6       54260638d07c   15 months ago   902MB
hello-world               latest    feb5d9fea6a5   18 months ago   13.3kB

The following steps can push a local image to the remote Docker Hub repository:

  • You need your own Docker Hub account and should log in with docker login -u <username>. It's recommended to use Access Tokens (set up at https://hub.docker.com/). Log out with docker logout.
  • Set a tag with the syntax docker tag <image-name>:<version> <username>/<image>:<version>. Tags are similar to snapshots for virtual machines - they should record versions.
  • Push with the syntax docker push <username>/<image>:<version>.
# Set tag: docker tag my-python36-hello-world:1.0 shadow7749/my-python36-hello-world:1.0
learn@debian10:~$ docker tag my-python36-hello-world:1.0 shadow7749/my-python36-hello-world:1.0
# Check changes in local image repository
learn@debian10:~$ docker images
REPOSITORY                           TAG       IMAGE ID       CREATED          SIZE
my-python36-hello-world              1.0       2f12c4d8f189   29 minutes ago   958MB
shadow7749/my-python36-hello-world   1.0       2f12c4d8f189   29 minutes ago   958MB
nginx                                latest    080ed0ed8312   7 days ago       142MB
python                               3.6       54260638d07c   15 months ago    902MB
hello-world                          latest    feb5d9fea6a5   18 months ago    13.3kB
# Push (ensure you're logged in)
learn@debian10:~$ docker push shadow7749/my-python36-hello-world:1.0
The push refers to repository [docker.io/shadow7749/my-python36-hello-world]
ff540de539ad: Pushed 
aa4c808c19f6: Mounted from library/python 
8ba9f690e8ba: Mounted from library/python 
3e607d59ef9f: Mounted from library/python 
1e18e7e1fcc2: Mounted from library/python 
c3a0d593ed24: Mounted from library/python 
26a504e63be4: Mounted from library/python 
8bf42db0de72: Mounted from library/python 
31892cc314cb: Mounted from library/python 
11936051f93b: Mounted from library/python 
1.0: digest: sha256:790bdc67737ed40e747fa9f1b7fb2831ac09031811036d2c9bda3a4b4eb56b94 size: 2430

After completion, you can log in to https://hub.docker.com to confirm if the image has been pushed to the remote repository.

This is a simple Docker container development workflow:

Pull image → Create container → Develop container → Generate image → Tag → Push image
                ^                                    |
                |------------- Continue development --------------|

Additional Information: Removing Containers and Images

  • To remove a container, you must first stop it, then use docker rm <container-id>.
  • A quick way to clean up all terminated containers is docker container prune.
  • To remove an image, use docker rmi <image-id>. The extra 'i' in 'rmi' stands for image.

Next topic: Docker Container Data: Persistence

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.