docker入门不完全指南,这玩艺儿不用则废!

2023年 7月 10日 29.7k 0

简介

简单地说,docker是一个开放平台,你可以基于它开发、迁移代码以及运行应用。

docker提供了在独立的环境中打包(package)和运行应用的能力,而这种松散(loosely)独立的环境我们一般把它叫做容器(container)。

有了容器,我们可以将我们项目所需要的环境全部一起打包,然后在另一个带有docker的环境比如云服务器上直接安装镜像运行容器,而这个过程我们不需要再去做额外的比如环境搭建之类的。

我们的容器中已经自带了环境了。而且我们可以同时运行多个不同环境的容器,也不用担心它们之间会有环境污染,因为容器之间是相互独立的。

另外dockerCI/CDcontinuous integration and continuous delivery)工作流也很搭。

docker的架构

docker使用的是client-server的架构, 客户端也就是提供给用户api的工具(docker compose[3] 也是一个客户端,用于管理多个容器),它会和docker daemon相当于中间层沟通(后面就叫守卫或者dockerd了),它侧重于构建、运行以及分发容器,中间层可以是同个系统的,也可以是远程的。

客户端和守卫之间是通过基于UNIX socket或者网络接口的REST API来联系的。

docker入门不完全指南,这玩艺儿不用则废!img_client_server

docker daemon

docker守卫(dockerd)会监听请求以及管理docker对象,比如镜像(images)、容器(containers) 、网络以及卷积(volumes)。它也能够和其它守卫建立联系来管理docker服务。

docker client

客户端(docker)是用户主要用来和docker交互的方式,我们输入指令,它会发送给dockerd,然后dockerd会执行它。客户端可以和多个dockerd建立联系。

Docker Desktop

一个可视化应用,支持win/mac/linux三个操作系统,我们可以可视化的创建、构建容器应用和微服务(microservices)。它包含了以下这些内容:Docker daemon (dockerd)the Docker client (docker)Docker ComposeDocker Content TrustKubernetes, and Credential Helper

docker registries

docker有一个注册表用来存储镜像。Docker Hub是一个公共的注册表,任何人都可以从上面下载镜像。而客户端默认也是从这里面下载镜像。当然,你也可以搞一个私有注册表用来放你自己的镜像。

docker objects

前面提到过的镜像、容器、网络、卷积等都是docker对象。

镜像

镜像是一个只读的模板,它提供了创建容器的api。一般情况下,一个镜像是在另一个镜像的基础上自定义的。

比如我们创建的镜像可能是在ubuntu的基础上构建的,不过在这个基础上,我们加了自定义的内容,比如Apacheweb服务器以及我们的应用程序,当然还有相关的配置信息。

后面我们会学习到通过Dockerfile批量执行docker指令,然后会在镜像之上创建一个layer,当我们重新构建镜像的时候只会去构建这个layer

容器

容器是可运行的镜像实例,你可以创建、开始、停止、移除容器。另外我们还可以通过网络来获取容器里的数据,甚至是基于当前容器里的数据再创建一个镜像。

默认情况下,容器是独立的,各容器之间不会有联系。我们可以通过暴露出来的网络端口/数据或者其他容器/主机的底层子系统来操作容器。

容器的定义取决于镜像以及你创建/开启容器时设置的配置项。当容器被移除,任何改变state的数据都将不会被存储。

我们可以用docker run指令创建一个基于某个镜像的容器。比如我们基于ubuntu镜像来创建一个容器

docker run -i -t ubuntu /bin/bash

这里假设我们用的是默认的配置项。当运行完这条指令之后,一个基于ubuntu镜像的容器就创建好了(你可以通过docker ps -a来查看容器是否创建)。

这一条指令执行过程中涉及到了如下几个几步:

  • 如果你本地没有ubuntu镜像,那么Docker就会自动帮你从docker registries上面pull下来,相当于你手动docker pull ubuntu
  • docker创建一个新的容器,相当于你手动docker container create
  • docker分配一个读写文件系统(read-write filesystem)给这个新容器,作为它最终的layer。这一步允许了我们的容器创建或者修改本地文件系统的文件和文件夹。
  • docker给容器创建了一个网络接口,这样就能和默认的网络连接上。这里面包含了给容器分配IP地址。默认情况下,容器可以通过主机的网络连接来连接额外的网络。
  • docker开启容器和调用/bin/bash执行容器。我们这里用到了-i-t两个标志位,所以容器是可通过终端交互的,你可以通过输入关键字来和容器交互,交互的log会被输出在终端。
  • 我们可以通过输入exit(一般ctrl + c也行)来终止交互,这样容器就停止了(注意没有移除)。
  • 底层技术

    docker是使用go语言编写的并且使用linux内核(kernel)的一些特性来提供功能。docker使用了一种叫namespaces的技术来提供独立的工作空间,即容器。当运行一个容器的时候,docker会给这个容器创建一系列命名空间。而这些命名空间提供了一个独立的layer。容器的每一部分都运行在一个单独的命名空间并且它的访问受限于这个命名空间。

    安装

    咱本机可以选择安装可视化工具docker desktop

    我这本机是window,所以接下来操作都基于window下的docker desktop

    由于docker底层使用到了linux内核的部分特性,所以我们并不能直接在window上安装docker

    window上安装linux

    官方文档:安装 WSL | Microsoft Learn

    另外可以搭配:Windows 11 安装 WSL2 - 知乎 (zhihu.com)

    这一点如果你在window上安装过redis应该就知道如何处理了。win10在某个版本之后就支持了将linux系统作为子系统,也就是WSL

    系统版本要求

    docker入门不完全指南,这玩艺儿不用则废!img_required

    满足先决条件之后打开控制面板的程序的启用或关闭Windows功能

    docker入门不完全指南,这玩艺儿不用则废!img_open_settingdocker入门不完全指南,这玩艺儿不用则废!img_change_setting

    勾上这几个项,我的电脑是win11的,没有Hyper-V,也不影响。

    注意这个虚拟机平台必须勾上,不然可能会遇到无法运行ubuntu应用的问题。

    接着打开mcrosoft store,搜索ubuntu,选择评分最高那个

    docker入门不完全指南,这玩艺儿不用则废!img_download_ubuntu

    当然你也可以按传统的方式下载ubuntu

    wsl --install -d Ubuntu 

    安装完之后还不能直接打开,由于当前内核的版本并不是最新的,我们还需要升级下版本

    通过管理员方式打开powershell,然后输入

    bcdedit /set hypervisorlaunchtype auto
    wsl --update 

    稳妥点重启下电脑

    然后打开这个ubuntu应用,最开始会让你设置用户名和密码,root并不能直接使用。

    docker入门不完全指南,这玩艺儿不用则废!img_open_success

    这样就表示在window上安装linux成功了。

    然后你也可以用hostnamectl看下当前版本。

    docker desktop

    回到我们最开始的点,我们是想安装docker desktop

    直接到这点击下载应用:Install Docker Desktop on Windows | Docker Documentation

    然后安装即可。

    docker入门不完全指南,这玩艺儿不用则废!img_docker_desktop

    docker engine(Ubuntu)

    当然,你也可以在之前安装的ubuntu应用打开后通过命令行的方式去安装docker:Install Docker Engine on Ubuntu | Docker Documentation

    sudo apt-get update
    sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

    然后测试下是否正常安装

    sudo docker run hello-world

    如果你之前安装过了,你可能需要在安装之前先移除旧版本

    for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done

    另外移除docker并不会移除原来的容器等,所以如果你不想要之前的数据,你可以执行以下操作

    sudo rm -rf /var/lib/docker
    sudo rm -rf /var/lib/containerd 

    其中/var/lib/docker存放的是镜像、容器、卷积和网络等。具体可见:Install Docker Engine on Ubuntu | Docker Documentation

    get started

    接下来我们将通过以下的几步来了解和学习docker的用法:

    • 构建镜像和运行镜像的容器
    • 使用docker hub分享你的镜像
    • 使用具有数据库的多个容器部署 Docker 应用程序
    • 使用Docker Compose运行程序

    不过在开始之前,我们需要再加深对容器和镜像概念的理解。

    什么是容器?

    容器实际上是一个在你主机上独立、有别于其它进程的沙盒(sandboxed)进程。

    如何实现的独立,前面说过是基于linux内核的namespace技术,具体可见:Demystifying Containers - Part I: Kernel Space | by Sascha Grunert | Medium

    总之,容器具有以下特性:

    • 是镜像的可运行实例,你可以使用create、start、stop以及delete这几个api来操作容器
    • 可以运行在宿主机、虚拟机和云端部署
    • 便携式的(portable),可以运行在所有OS
    • 独立于其它容器,运行它自己软件、二进制(binaries)以及配置

    什么是镜像?

    前面我们知道了容器是运行在镜像之上的文件系统,而容器又可以跑你的应用。那么作为容器的基础:镜像自然就需要包含所有容器需要的东西,比如依赖、配置等。

    我们后面会深入了解到镜像。

    (我怀疑简介和这一章不是同一个老哥写的)

    打包(containerize)一个应用镜像

    接下来我们将搞一个web前端应用,基础环境自然就是nodeJS的。

    什么?你没用过nodejs?请出门左拐:Node.js

    噢,你先别拐,咱这只是将它作为环境依赖安装下,并不会用到里面的api等。

    先在本机创建一个docker-study的文件夹, 然后拉个官方给的demo

    mkdir docker-study
    cd docker-study
    git clone https://github.com/docker/getting-started.git

    这个git分支貌似需要开魔法才能拉下来。。。

    docker入门不完全指南,这玩艺儿不用则废!img_menu

    这应该是一个workspaces。我们的目标在app文件夹里。

    项目里的代码我们不用去探究了解,我们的目的是知道如何打包和运行。

    我们在app根目录创建一个Dockerfile文件。

    t# syntax=docker/dockerfile:1
       
    FROM node:18-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    CMD ["node", "src/index.js"]
    EXPOSE 3000

    这里面几行是在干啥我们暂时不分析,现在我们可以开始构建镜像了。

    docker build -t getting-started .
    • docker build:构建容器镜像的指令
    • -ttag,也就是你镜像的名字
    • .:这最后一个点表示需要在当前目录下查找Dockerfile

    docker入门不完全指南,这玩艺儿不用则废!img_buildingdocker入门不完全指南,这玩艺儿不用则废!img_build_success

    这时你的docker desktop里应该就会看到我们刚打包的镜像

    docker入门不完全指南,这玩艺儿不用则废!img_images

    打包的过程中可能下载了一堆的layer,这是因为我们前面在DockerfileFROM node:18-alpine,我们需要nodejs环境,如果你电脑没有,就会先下载对应的镜像。

    然后我们回过头来分析下Dockerfile里的内容:

    • FROM[13] node:18-alpine:我们希望基于node:18-alpine这个镜像上开始构建镜像。
    FROM [--platform=]  [AS ]
    // or
    FROM [--platform=] [:] [AS ]
    // or
    FROM [--platform=] [@] [AS ]
    • WORKDIR[14] /app:工作目录,其余指令比如RUN/CMD等的执行目录,可以指定多个,下面这样的相当于/a/b/c
    WORKDIR /a
    WORKDIR b
    WORKDIR c
    RUN pwd
    • COPY [15]. .:复制新文件(夹)到目标目录,目标目录作为容器的文件系统目录
    COPY [--chown=:] [--chmod=] ... 
    COPY [--chown=:] [--chmod=] ["",... ""]
    • RUN[16] yarn install --production: 执行yarn install指令,--production是参数。这个api执行时会在最顶层创建一个layer,然后把执行的结果提交出去给下一步指令。
    RUN  // (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
    RUN ["executable", "param1", "param2"] // (exec form)
    • CMD ["node", "src/index.js"]:调用node运行src/index.js文件。 一个Dockerfile只能有一个CMD,如果你写了多个则按最后一个为准。 你可能觉得这个CMDRUN作用有些类似,实际上两者做的事情并不一样,CMD是作为执行容器的默认值,而RUN则是容器执行前的一系列操作。 你也可以用ENTRYPOINT进行覆盖,格式均为JSON数组。
    CMD ["executable","param1","param2"] // (exec form, this is the preferred form)
    CMD ["param1","param2"] // (as default parameters to ENTRYPOINT)
    CMD command param1 param2 // (shell form)
    • EXPOSE[17] 3000: 容器监听的端口,默认是TCP的,也可以设置成UDP。 实际上这里并没有真正的暴露端口,而是相当于文档类型一样。如果有着需要,docker run的时候你可以带上-p参数。
    EXPOSE  [/...]
    //example
    EXPOSE 80/tcp
    EXPOSE 80/udp

    比如:

    docker run -p 80:80/tcp -p 80:80/udp ...

    这个例子如果带上-p,会同时创建两个端口,一个tcp和一个udp的。

    运行容器[18]

    现在我们已经有了镜像,要创建并运行容器只需要简单的一个指令。

    docker run -dp 127.0.0.1:3000:3000 getting-started
    • -dp:是-d-p的简写
    • -p:前面说过了,是--publish的缩写
    • -d:是--detached的缩写,这样你的容器就可以在后台运行
    • 127.0.0.1:3000:3000:格式为HOST: CONTAINER。 127.0.0.1:3000Host,表示主机的地址加端口,这个端口你可以用来暴露到公网等,而后面的3000则表示容器监听的端口,这俩端口不必一致,实际上是做了一层映射。
    • getting-started:我们的镜像名字

    docker入门不完全指南,这玩艺儿不用则废!img_run_containerdocker入门不完全指南,这玩艺儿不用则废!img_containers

    然后我们直接访问localhost:3000

    docker入门不完全指南,这玩艺儿不用则废!img_connect_to_localhost_3000

    这样我们的容器就在后台跑起来了

    下面我们来更新下我们的应用

    更新应用[19]

    首先我们来修改下项目的代码

    找到src/static/js/app.js文件

    - No items yet! Add one above!
    + You have no todo items yet! Add one above! 

    然后回到app文件夹里,我们需要重新构建一次镜像。

    docker build -t getting-started . 

    构建完之后先暂时不要执行运行容器的指令,由于镜像已经被占用了,所以这个时候你运行容器可能会失败,因为当前镜像已经有一个运行中的容器了。

    我们需要先删除对应的容器

    docker-desktop中直接找到container那一栏删掉对应的容器即可,命令行的如下

    docker stop  //  
    docker rm  //  

    docker入门不完全指南,这玩艺儿不用则废!img_remove_container

    移除之前需要先停止容器运行。

    然后再重新执行容器运行的指令

    docker run -dp 127.0.0.1:3000:3000 getting-started

    docker入门不完全指南,这玩艺儿不用则废!img_update_container_success

    这样就完成了。

    分享应用[20]

    我们可以把我们的app上传到docker hub上,这样别人或者自己的另一台机器上就可以下载你这个app了。

    在我们上传自己的app之前,我们需要先注册一个docker账号:https://www.docker.com/pricing?utm_source=docker&utm_medium=webreferral&utm_campaign=docs_driven_upgrade&_gl=1*115ovx7*_ga*NjcwMzA0MTY3LjE2ODAxODI2MzY.*_ga_XJWPQMJYHQ*MTY4NzU3MDU3Mi4xNi4xLjE2ODc1NzI5NjkuMjcuMC4w

    然后来这创建一个仓库:

    docker入门不完全指南,这玩艺儿不用则废!img_create_respositories

    注意要选择public,这样别人才能pull的到你的镜像。

    docker入门不完全指南,这玩艺儿不用则废!img_create_public_image

    当然,我们一般都是自己用的(要钱)。。

    • name:是你发布的镜像的名字

    设置完了点击create

    创建好了屏幕右边可以看到一个指令提示

    docker push [yourname]/[respository]:[tag]
    • tag:这个指的是版本,默认是latest
    • respository:这个要和你的镜像名字对应上
    • yourname:这个是你的用户名,记得先登录docker desktop,如果你是用命令行的,你需要使用一下指令进行登录
    docker login -u YOUR-USER-NAME

    我们回到我们的项目中执行如下指令

    docker push [yourname]/[respository]

    然后你会发现报错了

    docker入门不完全指南,这玩艺儿不用则废!image_not_existed

    没有对应的镜像

    这个时候我们就需要用到tag[21]修改已有镜像的名字

    docker tag getting-started YOUR-USER-NAME/getting-started 

    docker入门不完全指南,这玩艺儿不用则废!img_rename_image

    然后我们再重新执行push指令

    但这里我们又遇到另一个问题

    docker入门不完全指南,这玩艺儿不用则废!img_requested_access_is_denied

    这个问题是因为我们先用docker desktop登陆了,所以我们现在需要先logout,然后重新登录

    docker logout
    
    docker login -u "mazeyqian" -p "Password" docker.io
    
    docker push [yourname]/getting-started

    docker入门不完全指南,这玩艺儿不用则废!img_push_successdocker入门不完全指南,这玩艺儿不用则废!img_push_success

    这样就发布成功了,我们在本机也可以用docker search的方式查看

    docker search [yourname]/getting-started 

    docker入门不完全指南,这玩艺儿不用则废!img_search_image

    然后我们可以将它拉下来运行,如果你有另一台机器,你可以在那台机器上试下,或者来到官方提供的线上平台试下:Play with Docker

    流程和之前一样的,我这里就不演示了

    持久化数据库[22]

    目前我们的数据是非持久性的,每次重新创建容器之后数据就都没了,前面说过容器之间是独立的(即使是基于同一个镜像的容器数据也不共享),所以默认数据是不会出现在容器外的。

    我们现在来将数据同步到本机来实现持久化。

    项目中使用的数据库是SQLite[23] , 默认数据是存储在/etc/todos/todo.db里面,所以如果我们有个东西把里的数据包裹起来放到本机,那么即使镜像都没了我们的数据也不会丢失。

    这个时候就需要用到挂载(mount)卷积(valumes[24])了

    docker volume create todo-db
    
    docker run -dp 127.0.0.1:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started
    • --mount:用于指定要挂载到的对应卷积
    • /etc/todos:卷积挂载的对象,在容器里。

    这样数据就可以持久了,我们可以重新创建容器和运行,然后随便搞点数据之后再移除容器,然后再创建一个容器并运行,这个时候你就会看到之前的数据还保留着

    docker入门不完全指南,这玩艺儿不用则废!img_persist_data

    现在我们的数据就被保留在卷积中了。

    然后我们可以通过docker volume inspect指令去查看卷积存放的位置,确保数据是有持久化的。

    docker volume inspect todo-db
    [
        {
            "CreatedAt": "2019-09-26T02:18:36Z",
            "Driver": "local",
            "Labels": {},
            "Mountpoint": "/var/lib/docker/volumes/todo-db/_data",
            "Name": "todo-db",
            "Options": {},
            "Scope": "local"
        }
    ] 

    (在公司,没有配置环境,所以直接用官方给的例子了。。)

    • Mountpoint:磁盘存放的位置,也就是我们持久化的数据所在位置。

    使用bind的方式挂载[25]

    实际上我们还可以自定义存放的位置,通过bind的方式将主机本机的文件夹绑定到容器里,当分享的文件夹内容发生变化的时候会立即同步容器里的数据。

    我们先来看下这两种方式的区别

    具名卷积(named volumes) 绑定挂载(Bind mounts)
    主机位置(Host location) 由docker选择 开发者设备
    挂载例子 (using --mount) type=volume,src=my-volume,target=/usr/local/data type=bind,src=/path/to/data,target=/usr/local/data
    使用容器内容填充新的卷积
    支持卷驱动程序

    回到我们的app文件夹中,我们来试下

    docker run -it --mount type=bind,,target=/src ubuntu bash // linux/unix
    docker run -it --mount "type=bind,src=$pwd,target=/src" ubuntu bash // windows powerShell
    • --mount:通知docker我这里是使用的绑定挂载的方式
    • -it:会创建一个伪的可交互终端,我们这就是一个ubuntu的容器终端
    • src:当前工作文件夹
    • target:容器里你想绑定的文件夹

    执行了指令之后我们会直接进入容器的文件系统

    docker入门不完全指南,这玩艺儿不用则废!img_ubuntu_terminal

    然后我们进入src文件夹中,创建一个test.txt文件,这个时候你就会看到这个文件在我们的设备上也是同时生成的。

    docker入门不完全指南,这玩艺儿不用则废!img_create_test_file

    然后我们在设备上直接删除这个test.txt文件再在终端输入ls

    这个时候就看不到之前创建的test.txt文件了

    docker入门不完全指南,这玩艺儿不用则废!img_delete_test.txt

    这么一看我们的文件夹确实绑定好了容器src文件夹了

    终结终端使用Ctrl + D

    这种方式下有些东西比如工具之类的我们并不需要单独去安装,都是会同步的。

    然后我们再来将我们的app做一波这个流程,不过在这之前,我们需要先docker ps -a看下是否存在运行中的getting-started容器,如果有,先移除。

    // linux/unix
    docker run -dp 127.0.0.1:3000:3000 
        -w /app --mount type=bind,,target=/app 
        node:18-alpine 
        sh -c "yarn install && yarn run dev"
    
    // windows
    docker run -dp 127.0.0.1:3000:3000 `
        -w /app --mount "type=bind,src=$pwd,target=/app" `
        node:18-alpine `
        sh -c "yarn install && yarn run dev"
    • -dp:前面说过了,就是-d-p的简写
    • -w:指定工作目录,这里指定的是app文件夹作为执行后续指令的文件夹
    • node:18-alpine:不多说,就是基于node镜像搭建容器
    • sh -c "yarn install && yarn run dev":调用sh来执行安装依赖和运行的指令,alpine没有bash的。
    • nodemon[26]: dev这个指令执行的是nodemon src/index.js。这个nodemon是一个node工具,可以在程序内容发生变化的时候自动重新运行程序。

    然后我们可以docker ps -a看下容器是否正常运行

    docker入门不完全指南,这玩艺儿不用则废!img_ps_a

    或者直接浏览器或者curl访问localhost:3000看是否正常。

    如果没正常运行起来,我们可以通过docker logs -f 的方式来查看错误信息

    docker入门不完全指南,这玩艺儿不用则废!img_log_container

    可以看到我这里是正常的log

    然后我们随便找个文件getting-startedappsrcstaticjsapp.js修改内容,注意要在本机修改

    docker入门不完全指南,这玩艺儿不用则废!img_edit

    然后直接回到之前打开的localhost:3000,刷新下,这个时候按钮的文案就变了。

    docker入门不完全指南,这玩艺儿不用则废!img_update_page_success

    当我们开发完毕之后,我们直接删掉这个容器,打包镜像发布就完事了

    docker build -t getting-started .

    当然,这个nodemon并不能做到热更新的效果,如果真要开发直接用webpack/vite即可。

    多容器应用[27]

    前面我们的项目里涉及到的东西都是直接放到同一个容器里的,比如数据库数据等,而实际上应该把他们拆开,尽量做到专注于某部分,因为如果不同技术涉及到的环境、环境变量、工具等会使得这容器变得复杂,维护成本高。

    现在我们再搞个MySQL的容器来运行我们的数据库。

    docker入门不完全指南,这玩艺儿不用则废!img_multi_containers

    不过这里还有个问题,那就是网络。由于容器之间相互独立隔离,它们并不知道彼此,所以也不能直接联系。这个时候我们可以使用network,如果这俩都在同一个网络下,那么它们就可以做到相互联系。

    docker network create todo-app

    创建一个叫做todo-app的网络

    然后我们创建一个MySQL容器并指向这个网络

    // linux/unix
    docker run -d 
         --network todo-app --network-alias mysql 
         -v todo-mysql-data:/var/lib/mysql 
         -e MYSQL_ROOT_PASSWORD=secret 
         -e MYSQL_DATABASE=todos 
         mysql:8.0 
    
    // windows
    docker run -d `
         --network todo-app --network-alias mysql `
         -v todo-mysql-data:/var/lib/mysql `
         -e MYSQL_ROOT_PASSWORD=secret `
         -e MYSQL_DATABASE=todos `
         mysql:8.0
    • -v:是--volume的简写

    docker入门不完全指南,这玩艺儿不用则废!

    • -e:设置环境变量
    • MYSQL_ROOT_PASSWORDMySQL的密码
    • MYSQL_DATABASE:指定的数据库,环境变量相关的具体可见:MySQL Docker Hub listing.
    • --network-alias: 搞个别名,这样比较好分辨,虽然还是同一个network

    docker入门不完全指南,这玩艺儿不用则废!img_network-alias

    这里我们并没有使用docker volume create,而是直接 todo-mysql-data:/var/lib/mysql,这里是通知docker我们想创建一个todo-mysql-data名字的卷积,如果没有这个卷积会自动创建并挂载/var/lib/mysql这个数据。

    docker入门不完全指南,这玩艺儿不用则废!img_auto_create_volumes

    接下来我们来测试下是否正常创建容器

    docker exec -it  mysql -u root -p
    • exec:在一个运行中的容器中执行指令,这里的指令是mysql -u root -p

    docker入门不完全指南,这玩艺儿不用则废!img_exec

    默认用的bash,所以可以不写。

    docker入门不完全指南,这玩艺儿不用则废!img_databases

    可以看到我们前面设置的,这会儿已经有了todos数据库。

    然后exit可以退出

    然后我们准备在另一个容器中连接这个数据库,每一个容器都有自己的ip地址,所以在同一个网络里我们可以通过ip去定位容器。

    我们先用nicolaka/netshoot来试下,这个镜像里面包含着很多的网络工具

    docker run -it --network todo-app nicolaka/netshoot
    
    // 然后在新终端中输入
    dig mysql

    docker入门不完全指南,这玩艺儿不用则废!img_ip

    dig命令是一个DNS工具。

    这个172.18.0.2就是我们的网络,每个人的都不同。

    这个mysql是容器的网络别名,我们前面用--network-alias定义的,到时候docker会解析这个mysql编程正确的ip地址。我们后面连接的时候直接用这个mysql别名即可。

    这个app的项目中支持了几个环境变量可以直接接入mysql,所以我们直接配置下即可。

    // linux/unix
    docker run -dp 127.0.0.1:3000:3000 
       -w /app -v "$(pwd):/app" 
       --network todo-app 
       -e MYSQL_HOST=mysql 
       -e MYSQL_USER=root 
       -e MYSQL_PASSWORD=secret 
       -e MYSQL_DB=todos 
       node:18-alpine 
       sh -c "yarn install && yarn run dev"
    
    // windows
    docker run -dp 127.0.0.1:3000:3000 `
       -w /app -v "$(pwd):/app" `
       --network todo-app `
       -e MYSQL_HOST=mysql `
       -e MYSQL_USER=root `
       -e MYSQL_PASSWORD=secret `
       -e MYSQL_DB=todos `
       node:18-alpine `
       sh -c "yarn install && yarn run dev" 

    别忘了先移除之前app的容器

    然后我们打开页面随便加点数据

    docker入门不完全指南,这玩艺儿不用则废!img_add_items

    然后我们进去mysql容器中,看下数据是否正常

    docker exec -it  mysql -p todos

    查看下数据

    select * from todo_items;

    docker入门不完全指南,这玩艺儿不用则废!img_data

    正常

    使用Docker Compose[28]

    现在我们的项目已经是多容器的了,但是一般一个项目一般不只有两个容器,随着容器的增多后续维护成本会增高。

    另外容器太多,要打包成镜像一个一个发布和安装就会非常繁琐。

    这个时候就需要有一个管理器用来统管这些容器,比如k8s[29]。

    不过这里我们只要用的是Docker Compose

    我们先来安装下(安装docker desktop的忽略,自带了):Install the Compose plugin

    // ubuntu/debain
    sudo apt-get update
    sudo apt-get install docker-compose-plugin
    // RPM-based distros
    sudo yum update
    sudo yum install docker-compose-plugin

    安装完之后,可以检验下安装是否正常

    docker compose version 

    docker入门不完全指南,这玩艺儿不用则废!img_dockeer_compose_version

    另外里面还教了如何手动安装docker compose,如果你想要这种方式请自行翻看文档,这里就不说了。

    安装完之后我们回到app文件夹中创建一个docker-compose.yml的文件

    services:
    • services:最开始需要定义需要管理的服务或者是容器,到时候打包的时候这些定义的服务都将作为其中一部分。

    然后我们定义第一部分

    services:
      app:
        image: node:18-alpine
        command: sh -c "yarn install && yarn run dev"
        ports:
          - 127.0.0.1:3000:3000
        working_dir: /app
        volumes:
          - ./:/app
        environment:
          MYSQL_HOST: mysql
          MYSQL_USER: root
          MYSQL_PASSWORD: secret
          MYSQL_DB: todos
    • app: 这个是service的名字,后面会作为网络的别名,可以是任意的,不必是app
    • image:这个service的镜像
    • command:这个服务开启后要执行的指令,非必要,也不需要固定顺序,不过一般都放在image下面
    • ports:服务端口,这个项有两种写法,一种简洁,另一种详细,具体可见:short syntax,long syntax
    // short example
    ports:
      - "3000"
      - "3000-3005"
      - "8000:8000"
      - "9090-9091:8080-8081"
      - "49100:22"
      - "8000-9000:80"
      - "127.0.0.1:8001:8001"
      - "127.0.0.1:5000-5010:5000-5010"
      - "6060:6060/udp"
    
    // long example
    ports:
      - target: 80
        host_ip: 127.0.0.1
        published: "8080"
        protocol: tcp
        mode: host
    
      - target: 80
        host_ip: 127.0.0.1
        published: "8000-9000"
        protocol: tcp
        mode: host 
    • working_dir:指令的工作目录
    • volumes:卷积的位置,相对于working_dir,这个也有长和短的两种写法,具体可见:short,longs
    • environment:环境变量定义

    ok,然后我们再来配置mysql的。

     services:
      app:
      // ...
      mysql:
        image: mysql:8.0
        volumes:
          - todo-mysql-data:/var/lib/mysql
        environment:
          MYSQL_ROOT_PASSWORD: secret
          MYSQL_DATABASE: todos
    volumes:
      todo-mysql-data:
    • volumes:注意这里的volumes是和services同级的。这个的作用是告知docker compose要创建一个todo-mysql-data的卷积,compose并不能识别是否需要自动创建卷积。详见:Volumes top-level element

    最终这个docker-compose.yml内容如下:

    services:
      app:
        image: node:18-alpine
        command: sh -c "yarn install && yarn run dev"
        ports:
          - 127.0.0.1:3000:3000
        working_dir: /app
        volumes:
          - ./:/app
        environment:
          MYSQL_HOST: mysql
          MYSQL_USER: root
          MYSQL_PASSWORD: secret
          MYSQL_DB: todos
      mysql:
        image: mysql:8.0
        volumes:
          - todo-mysql-data:/var/lib/mysql
        environment:
          MYSQL_ROOT_PASSWORD: secret
          MYSQL_DATABASE: todos
    volumes:
      todo-mysql-data: 

    现在我们可以来试着运行下了,不过在这之前,确保mysqlapp这俩容器都移除了

    然后我们就能运行了

    docker compose up -d 

    docker入门不完全指南,这玩艺儿不用则废!img_compose_up

    • -d:这个和之前介绍的是一样的,都是在后台运行

    这个时候我们在docker ps -a可以看到之前移除的容器又出现了

    docker入门不完全指南,这玩艺儿不用则废!img_containers

    我们再来访问下浏览器

    docker入门不完全指南,这玩艺儿不用则废!img_test_success

    正常访问

    或者你也可以看下log

    docker compose logs -f 

    docker入门不完全指南,这玩艺儿不用则废!img_compose_logs

    然后我们回到docker desktop中,这个时候你会发现容器只剩下一个app

    docker入门不完全指南,这玩艺儿不用则废!img_app

    另外说下移除的步骤

    docker compose down

    这样就移除了,不过需要注意的是默认不会移除卷积,如果你有这个需要,你需要加上参数:--volumes

    docker desktop中点击右侧删除按钮即可,不过通过这种方式移除的是没法子顺便移除卷积的。

    docker入门不完全指南,这玩艺儿不用则废!img_remove_compose

    镜像构建最佳实践[30]

    我们现在已经知道镜像是一个个layer堆叠起来的,但是我们并不清楚这些layer是哪些,这个时候就可以使用

    docker image history 

    这个指令来查看是哪些

    docker入门不完全指南,这玩艺儿不用则废!img_image_history

    有些内容被遮盖了,如果你想看全,你可以带上--no-trunc这个参数

    现在我们可以看到所有的layer了,那么我们就能分析这些layer来优化以达到减少构建时间的效果。

    另外提一嘴,当其中一层layer改变之后,它往上的所有层都会重新创建,因为每层layer都可能依赖于它前面那层。

    我们先来看个例子的Dockerfile

    # syntax=docker/dockerfile:1
    FROM node:18-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    CMD ["node", "src/index.js"] 

    这里每一个指令都会创建一层layer。而根据前面知道的知识,如果其中一层变化了,那么后面所有层都要跟着变。这个时候就很浪费时间了,因为后面的所有层我们没有改动,我们没必要每次都去让它们重新构建,比如这个yarn install --production,每次都需要重新安装依赖。

    这个时候最简单的方案自然就是缓存。

    这里我们只需要调整下yarn install的位置,让它在copy之前执行,那么它就只会安装依赖一次。

    # syntax=docker/dockerfile:1
     FROM node:18-alpine
     WORKDIR /app
     COPY package.json yarn.lock ./
     RUN yarn install --production
     COPY . .
     CMD ["node", "src/index.js"] 

    后面除了package.json之外任何文件的改动都不会再触发这个yarn install

    然后我们再搞个.dockerignore文件用来忽略某些会被复制到的东西,作用和.gitignore一样。

     node_modules

    详情可见:Dockerfile reference

    更多node相关的项目可以参考这篇:Dockerizing a Node.js web app | Node.js

    最后我们再重新构建(别忘了开魔法)

    docker build -t getting-started .

    构建完之后我们改下src/static/index.html文件内容

    docker入门不完全指南,这玩艺儿不用则废!img_change_title

    然后我们再重新构建

    docker入门不完全指南,这玩艺儿不用则废!img_cached

    现在构建就少了这一步了,构建耗时也是少了很多

    然后我们来简单接触下多阶段构建(multi-stage builds)

    多阶段构建的好处:

    • 将一些build-time依赖从runtime依赖分离,减少运行时构建时间
    • 减少镜像体积,只包含程序运行需要的。

    这里以Maven/Tomcat为例子,比如JDK在编译阶段是必要的,但是对于runtime来说并不需要,所以这个时候我们就需要去掉不必要的

    # syntax=docker/dockerfile:1
    FROM maven AS build
    WORKDIR /app
    COPY . .
    RUN mvn package
    
    FROM tomcat
    COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

    这里我们先构建maven,这个构建别名为build,然后第二阶段构建直接从maven里复制需要的内容即可。

    这里我不是很懂,如果说错了,麻烦评论区说下,谢谢~

    再来个react项目的例子

    如果不考虑SSR等,我们甚至不需要node环境,所以直接复制html/js/cssnginx上即可。

    # syntax=docker/dockerfile:1
    FROM node:18 AS build
    WORKDIR /app
    COPY package* yarn.lock ./
    RUN yarn install
    COPY public ./public
    COPY src ./src
    RUN yarn run build
    
    FROM nginx:alpine
    COPY --from=build /app/build /usr/share/nginx/html 

    那么到这我们的入门就差不多了

    总结

    这玩意儿要实战才行,不然一学就会,一用就废~

    另外docker和云原生也有关系,比如k8s

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论