Docker Container Development: Virtualization
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:
- The Docker daemon first tries to find the image in the local repository.
- If the image is not available locally, it will be pulled from a remote repository.
- 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 psto view all running containers. Adding the-aoption 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
psto check, you'll notice no containers are running because the container wasn't actually running. You need to use the-aoption 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
-tis the full option--tty: It allocates a pseudo tty.-iis 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 withdocker run -it python:3.6 bash). By analogy, to stop a running container, usedocker stop <container-id>, and to restart, usedocker 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 withdocker 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