Docker实操过程

体会

知乎上搜“docker入门”搜到的笔记总是起一个赚噱头的标题,然后点进去一看,里面大多是一些入门内容的简单罗列,要么对于背景内容长篇大论让读者抓不住重点,要么对于命令API简单罗列,入门者甚至连这些API如何衔接使用,这些命令大概发生了什么可能都没有概念,说是入门教程,实际跟着实操之后依然云里雾里。

image

链接

个人比较欣赏的入门教程是这个:Docker超详细教程 | 观星客 (chasonlee.github.io)

其中概念性介绍足够精炼,同时能说明核心的概念,而且实操部分足够详细,从例子讲起。

更新:该地址已经失效。

个人思考

下面是自己在实操后对一些更具体内容的思考补充。

关于DockerDesktop界面

image

如上图,Docker Desktop安装好后,第一个镜像应该是hello-world。稍后我们会用一下它。我个人的感受是先不要用Docker Desktop的可视化界面创建和操作镜像和容器,应该用命令行,多敲命令能更清楚地发现发生了什么,也能更明晰地感受到docker命令执行之后有一个什么结果。Docker Desktop可以作为已有镜像和容器的可视化展示。

上图中,Images镜像,Containers容器,镜像是容器的模板,容器是镜像的实例。镜像与容器好比C++中类与对象。所以同一个镜像可以有多个实例。

镜像或者是容器如何指定?一种是名字,一种是ID。镜像的名字就是Name:Tag的格式,如上图中的hello-world:latest。容器的名字通常就是创建它时指定的名字,用docker ps -a通常都可以看到。而不管是容器还是镜像,ID就是它创建时被分配的唯一hash码,如上图中的9c7a54a9a43c

可视化展示就不多做截图,下面以命令行为主。主打解释一个命令的诸多细节与效果

关于Docker命令行

(base) C:\Users\80563\Desktop>docker pull chasonlee/ubuntu_demo:latest
latest: Pulling from chasonlee/ubuntu_demo
9ff7e2e5f967: Pull complete
59856638ac9f: Pull complete
6f317d6d954b: Pull complete
a9dde5e2a643: Pull complete
583e635329f1: Pull complete
d74dae086c4f: Pull complete
Digest: sha256:f4396916a5cbb3ece8e5eb74a860d956b8957965675d5dde707ed4f316407b8c
Status: Downloaded newer image for chasonlee/ubuntu_demo:latest
docker.io/chasonlee/ubuntu_demo:latest

(base) C:\Users\80563\Desktop>
  • docker pull拉取镜像
  • chasonlee/ubuntu_demo:latest正是之前的镜像指定格式
(base) C:\Users\80563\Desktop>docker image ls
REPOSITORY              TAG       IMAGE ID       CREATED        SIZE
hello-world             latest    9c7a54a9a43c   6 months ago   13.3kB
chasonlee/ubuntu_demo   latest    59693b89568e   4 years ago    158MB

(base) C:\Users\80563\Desktop>docker images
REPOSITORY              TAG       IMAGE ID       CREATED        SIZE
hello-world             latest    9c7a54a9a43c   6 months ago   13.3kB
chasonlee/ubuntu_demo   latest    59693b89568e   4 years ago    158MB

(base) C:\Users\80563\Desktop>docker ps -a
CONTAINER ID   IMAGE                             COMMAND                   CREATED          STATUS          PORTS                  NAMES
44ebbb32efae   docker/welcome-to-docker:latest   "/docker-entrypoint.…"   31 seconds ago   Up 31 seconds   0.0.0.0:8088->80/tcp   welcome-to-docker
  • docker image ls或者docker images查看镜像
  • docker ps或者docker ps -a查看容器,前者只显示运行中的,后者显示所有的,包括已经停止的
(base) C:\Users\80563\Desktop>docker images
REPOSITORY              TAG       IMAGE ID       CREATED        SIZE
hello-world             latest    9c7a54a9a43c   6 months ago   13.3kB
chasonlee/ubuntu_demo   latest    59693b89568e   4 years ago    158MB

(base) C:\Users\80563\Desktop>docker run --rm hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

(base) C:\Users\80563\Desktop>docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
  • docker images查看有什么镜像,然后根据具体名字运行hello-world(组合招),因为实际中经常记不清具体名字。
  • 根据自带hello-world镜像创建一个容器并运行。
  • 图中这个容器的实际内容就是打印这段文字然后直接结束。
  • --rm表示这个容器如果stop,那就立即删除这个容器,而不是留着。如果没有--rm,那么就会留着这个容器。
  • docker ps显示确实没有了该镜像的容器实例。
  • 可以经常使用docker imagesdocker ps来查看现有的镜像和对应的容器以及ID,名称等
(base) C:\Users\80563\Desktop>docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

(base) C:\Users\80563\Desktop>docker ps -a
CONTAINER ID   IMAGE         COMMAND    CREATED         STATUS                     PORTS     NAMES
a70726ad972b   hello-world   "/hello"   2 minutes ago   Exited (0) 2 minutes ago             amazing_lumiere
  • 没有加--rm的后果

  • 运行完容器,它已经停止(Exited),但是因为没有--rm所以还留着。此时要docker rm a70726ad972b或者docker rm amazing_lumiere手动删除

  • NAMES是amazing-lumiere是因为创建容器时如果不指定容器名(--name),就会自动给一个

  • -it是什么意思:i是交互模式(将当前命令行的终端输入输出接入容器的输入输出),t是伪终端,二者通常结合使用。

  • -d是后台运行的意思,有了这个参数,执行命令后不会将当前命令行转为容器中的命令行,而是只输出创建容器所分配的ID。之后再想进去(必须是容器已经在运行中)可以用docker attach ID

    • ID当然也可以写ContainerName
    • 实际上docker attach ID 的方法并不推荐,因为这样的话当前终端结束会导致该容器一同停止
    • 推荐的用法是exec,个人觉得叫“切入”比较贴切,即临时启动一个终端,让该(已经运行的该容器,如果没有运行需要先docker start ID 运行)执行一条命令,作为当前终端的启动任务,例如
      • docker exec a70726ad972b /bin/bash
      • 这样当前终端退出后不会导致该容器的停止
  • docker exec是针对已存在的容器的,docker run是针对镜像的。

  • Docker Desktop中,容器的log中也可看到容器的命令行输出。

一个综合的例子

image

如上是我在windows上开发的一个项目,这个项目的启动方式是在nl2sql_server_load/ 根目录下运行python -m app_nl2sql_server.app_server来启动。现在需要为它配一个镜像打包到linux服务器上去运行。

首先在项目根目录下放上requirements.txt,写上依赖包

Flask==3.0.3
mysql-connector-python==8.4.0
numpy==1.26.4
pandas==2.2.2
portalocker==2.8.2
pynvml==11.5.0
Requests==2.32.3
tabulate==0.9.0
vanna
vanna[chromadb,openai,mysql]
openpyxl
fastapi
sse_starlette
aiohttp

然后需要在项目根目录下放上Dockerfile ,里面写上(注意看里面的注释)这些指令

FROM robd003/python3.10:latest
# 其实对于部署来说
# 用 FROM python:3.10-slim 会专业
# 因为这个slim更精简,体积更小
# 而我这个后来打完镜像发现有1-2G了

# 将当前工作目录设置为 /app/nl2sql_server_load
# 后面的命令也都将会在WORKDIR上执行,包括你进去容器时也是
WORKDIR /app/nl2sql_server_load

# 复制当前目录(也就是项目根目录)的内容到当前工作目录
# 此时当前工作文件夹为WORKDIR,也就是 /app/nl2sql_server_load
# 第一个句点表示宿主机的当前目录,第二个是容器的当前目录
COPY . .
# 以下是一个小BUG,与本次例子关系不大
# COPY ./nl2sql_server_load/ /app/
# 不要像上面这样写,这样写只会把当前目录下的nl2sql_server_load文件夹的【内容】复制到/app中
# 而不会把这个文件夹作为子文件夹复制过去
# 要把该文件夹复制过去,应该写
# COPY ./nl2sql_server_load/ /app/nl2sql_server_load

# 安装所需的 Python 包
# RUN和下文的CMD不同,RUN作用于构建镜像时,
# 而CMD是在启动每一个由该镜像创建的容器时的默认启动命令,
# 即docker run <image_name>时的启动命令
RUN pip install --no-cache-dir -r nl2sql_server_load/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 那么对于这个CMD指令,如果想(根据镜像)启动容器并且不执行默认的启动命令,应该写
# docker run -it --entrypoint /bin/bash <image_name>
# 表示把/bin/bash作为本次该镜像创建的容器的启动命令,而不是默认的命令

# 暴露应用运行的端口
EXPOSE 63000

# 启动服务
# CMD python -m app_nl2sql_server.app_server
# 现实还是很难一步到位,
# 后来我实操发现还有bug,不能跑起来,所以这句其实没必要写
# 应该在创建并运行容器,在容器里面debug完成确认能跑起来再手动在容器中启动

然后在项目根目录下用该Dockerfile来构建镜像:

docker build -t oms:v1 .

也就是说,假设你有一个名为nl2sql_server_load的项目根目录,其中包含了一个Dockerfile,你想要基于这个Dockerfile创建一个新的镜像,并将其标记为oms,标签为v1。这里的.表示当前目录,也就是包含Dockerfile的目录。

构建完成之后,就可以基于该镜像创建容器

docker run -it --entrypoint /bin/bash -p 127.0.0.1:63000:63000 --name oms2 oms:v1

注意镜像名称要放在最后,不然不行。

注意该命令设置的端口映射。

这个时候就进去了容器,可以装些必要东西,然后再手动运行项目。

检查确认项目成功跑起来之后,可以根据该容器导出镜像文件

docker export oms2 -o container_backup.tar

然后把该.tar文件传到服务器上,再在服务器上用docker导入成镜像

docker import container_backup.tar oms:v2

就可以在服务器上用该镜像创建容器了。

docker run -it --entrypoint /bin/bash -p 127.0.0.1:63000:63000 --name oms2 oms:v2

备注:为什么要先写dockerfile构建镜像,由该镜像创建了容器,再由容器导出镜像文件,再传到服务器上用docker引入该镜像?有两个原因,一是dockerfile创建的镜像oms:v1所创建的容器并不一定直接可用,中间可能有bug。这个时候改完bug的容器相对于oms:v1来说已经有了一个更新。所以应该基于这个最新的容器导出成镜像。第二是服务器不连网,不能直接在服务器上用dockerfile来构建镜像。只能把镜像先打包好再传到服务器上用docker导入。