Docker、Dockerfile 和 Docker Desktop

1,587 阅读4分钟

Docker Desktop

官网: www.docker.com/

下载地址:docs.docker.com/desktop/ins…

Docker Desktop 会内置 docker 命令,安装好后,在 cmd 窗口输入 docker -v 可查看版本信息。

download.png

面板信息

Containers 是镜像跑起来的容器,Images 是本地的所有镜像,Volumes 管理数据卷。

顶部搜索栏可以搜到开源 Hub 库的镜像。

1.png

拉镜像 - pull

1、搜索 nginx 镜像,点击 pull(搜索这步需要网络条件);

2.png

docker 命令

# docker pull 仓库地址/命名空间/镜像名:版本号
仓库地址: 没有显式指定仓库地址时,默认会从DockerHub查找镜像;拉取私有仓库的镜像,需要指定仓库地址。
命名空间: 截图最后有一个名为 ubuntu/mysql 的镜像,其中 ubuntu 是命名空间,用以区分不同的个人或组织发布的镜像。没有显式指定命名空间时,默认会查找官方团队发布的镜像。
镜像名称: 需要拉取的镜像的名称。
标签: 没有显式指定标签时,默认会拉取 latest 标签, latest 表示这是最新的版本。
​
通过 docker pull 拉取镜像并不是必须的,在 docker run 时,如果本地不存在指定镜像,Docker会自动拉取。
docker pull nginx:stable-alpine3.20-perl

2、pull 下来之后,就可以在本地 Images 看到了。

3.png

运行镜像 - run

点击 run (三角形图标)运行镜像,填入所需参数

4.png

  • Container name:容器名称。不填则会随机生成一个

  • Host Ports:映射端口号。容器内跑的 nginx 服务是在 80 端口,要把宿主机(本机)的某个端口映射到 容器的80 端口才可以访问

  • Volumes:数据卷,把宿主机的某个目录挂载到容器内。

    因为容器是镜像跑起来的,下次再用这个镜像跑的还是同样的容器,那你在容器内保存的数据就会消失。所以我们都是把某个宿主机目录,挂载到容器内的某个保存数据的目录,这样数据是保存在宿主机的,下次再用镜像跑一个新容器,只要把这个目录挂载上去就行。

    • Container path:宿主机目录
    • Host path:映射目录
  • Environment variables:环境变量

    • Variable:变量名
    • Value:变量值

5.png

docker 命令

  • -d:后台运行
  • -p:端口映射
  • -v:指定数据卷挂载目录
  • -e:指定环境变量
  • --name:容器名
# docker run -d --name 容器名 -p 本机端口:3000 -v 宿主机映射目录:镜像映射目录 -e 变量名=变量值 仓库地址/命名空间/镜像名:镜像版本号
​
docker run -d --name nginx-test -p 1234:3000 -v D:/nginx-test:/usr/share/nginx/html -e KEY1=VALUE1 nginx:latest 

挂载本地的 E:\nginx-test 到容器内的 /usr/share/nginx/html 目录,这里的 E:\nginx-test 可以换成宿主机的任何目录,点击 run 运行成功。

6.png

E:\nginx-test 目录里面添加一点内容,例如 index.html ,通过访问 http://localhost:1234/index.html就能看到效果了。

7.png

点击 Files 标签就可以看到容器内的文件,可以看到 /usr/share/nginx/html 被标识为 mounted,就是挂载目录的意思。

8.png

具体的挂载目录,在搜索镜像的时候有写

9.png

部分命令

# 当前在运行的容器
docker ps
# 获取所有容器
docker ps -a
​
# 获取镜像列表
docker images
​
# 查看容器日志
docker logs ${容器id}# 查看容器详情
docker inspect ${容器id}# 启动一个已经停止的容器
docker start ${容器id}# 删除一个容器
docker rm ${容器id}# 停止一个容器
docker stop ${容器id}

Dockerfile

如何自己制作一个这样的镜像?docker 容器内就是一个独立的系统环境,怎样在这样一个系统上安装 nginx 服务?

答:通过 dockerfile 声明要做哪些事情,执行一些命令,复制一些所需文件,docker build 的时候就会根据这个 dockerfile 来自动化构建出一个镜像来。

基础的

简单的 dockerfile 文件配置:

# FROM 指定一个基础镜像来继承使用
FROM node:latest
​
# WORKDIR 指定当前工作目录
WORKDIR /app
​
# COPY 把当前执行目录的所有内容 复制到容器的/app目录中
COPY . .
​
# RUN 执行命令
RUN npm config set registry https://registry.npmmirror.com/
​
RUN npm install -g http-server
​
# 声明当前容器访问的网络端口
EXPOSE 8080
​
# 容器启动的时候执行的命令
CMD ["http-server", "-p", "8080"]
  • 先通过 FROM 继承了 node 基础镜像,里面就有 npmnode 这些命令了
  • 通过 WORKDIR 指定容器的当前工作目录,没有的话会自动创建
  • 然后通过 COPYDockerfile 同级目录下的内容复制到容器 /app
  • 之后通过 RUN 执行 npm install,全局安装 http-server
  • 通过 EXPOSE 指定要暴露的端口
  • CMD 指定容器跑起来之后执行的命令,这里就是执行 http-server 把服务跑起来。

把这个配置文件保存为 Dockerfile,然后在同级添加一个 index.html

10.png

随后在当前目录打开终端,执行命令:

执行 docker build 之前,可以提前在 docker desktop 上将基础镜像 node 下载下来,不然跑命令时,容易出现镜像包pull不下来的情况

# 打包命令: (注意最后有个符号 .)
# docker build -t name:tag -f filename .
# docker build -t 镜像名:标签名(版本号) -f Dockerfile文件名 .
​
docker build -t aaa:ccc -f Dockerfile .

Dockerfile 是在守护进程 docker daemonbuild 的,没启动 docker daemon 的时候是不能 build 的。

命令行工具会和 docker daemon 交互来实现各种功能,比如 docker build 的时候,会把 Dockerfile 和它的构建上下文(也就是所在目录)打包发送给 docker daemon 来构建镜像。

执行完后,打开 docker desktopImages 即可看到生成的镜像了

11.png

随后运行这个镜像,访问 localhost 就能看到 index.html的内容

Dockerfile 构建成镜像,通过容器跑起来的流程:

Dockerfile => docker build => 得到 docker 镜像 => docker run => docker 容器

.dockerignore

镜像越小性能越好,所以 docker 支持你通过 .dockerignore 声明哪些不需要发送给 docker daemon。

示例:

# 忽略所有 md 结尾的文件
*.md
# 不包括 README.md
!README.md
# 忽略 node_modules 下 的所有文件
node_modules/
# 忽略 a.txt、b.txt、c.txt 这三个文件
[a-c].txt
# .DS_Store 是 mac 的用于指定目录的图标、背景、字体大小的配置文件,这个一般都要忽略
.DS_Store
​
.git/
.vscode/
.dockerignore
.eslintignore
.eslintrc
.prettierrc
.prettierignore

docker build 时,会先解析 .dockerignore,把该忽略的文件忽略掉,然后把剩余文件打包发送给 docker daemon 作为上下文来构建产生镜像。

这就像你在 git add 的时候,.gitignore 下配置的文件也会被忽略一样。

技巧提升

alpine

使用 alpine 镜像,而不是默认的 linux 镜像

docker 容器内跑的是 linux 系统,各种镜像的 Dockerfile 都会继承 linux 镜像作为基础镜像。

alpine 只是去掉了很多 linux 里用不到的功能,使得镜像体积更小。

FROM node:22.9.0-alpine3.20

多阶段构建

以下方这个工程化项目的 Dockerfile 为例

FROM node:22.9.0-alpine3.20
​
WORKDIR /app
​
# 将当前项目的package.json复制到 /app 工作目录
COPY ./package.json .
​
RUN npm config set registry https://registry.npmmiroor.com
​
RUN npm install
​
# 复制起其余剩下的文件
COPY . .
​
RUN npm run build
​
EXPOSE 3000
​
CMD [ "node", "./dist/main.js" ]
  • 问题一:为什么要先复制 package.json ,安装依赖之后再复制其他文件?

    • docker 是分层存储的,Dockerfile 里的每一行命令都会创建新的一层,会做缓存,每次 docker build 的时候,只会从变化的层开始重新构建,没变的层会直接复用。

也就说现在这种写法,如果 package.json 没变,那么就不会执行 npm install,直接复用之前的。如果一开始就把所有文件复制进去,那不管 package.json 变没变,任何一个文件变了,都会重新 npm install,这样没法充分利用缓存,性能不好。

  • 问题二:源码和很多构建的依赖是不需要的,但是现在都保存在了镜像里。实际上我们只需要构建出来的 ./dist 目录下的文件和运行时的依赖。

    • 多阶段构建。使用 AS 定义阶段名称
    • 只会留下最后一个阶段的镜像,最终构建出来的镜像里是没有源码的,有的只是 dist 的文件和运行时依赖。
# as build stage 为当前阶段镜像起一个别名
FROM node:22.9.0-alpine3.20 AS build-stage

# 指定当前目录为容器内的 /app
WORKDIR /app

# 将 package.json 复制到容器内的 /app 目录
COPY ./package.json .

RUN npm install

# 把其余的文件复制过去
COPY . .

RUN npm run build

# production stage --- docker build 之后,只会留下最后一个阶段的镜像
FROM node:22.9.0-alpine3.20 AS production-stage

# 通过--form= 复制构建build-stage阶段 生成的目录文件 到当前目录
# 只复制 dist目录,package.json
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

# 只会安装 dependencies 的依赖
RUN npm install --production

EXPOSE 3000

# 容器跑起来以后执行 node ./app/main.js 命令。
CMD [ "node", "/app/main.js" ]

CMD 结合 ENTRYPOINT

  • CMD 是容器启动时默认执行的命令,如果 docker run 时指定了其他命令,CMD 命令会被覆盖

    • 例如:docker run test echo "重写命令"
  • ENTRYPOINT 是容器的入口点,不会被 docker run 时指定的命令覆盖,而是作为参数传递给 ENTRYPOINT 命令

# Dockerfile

FROM node:22.9.0-alpine3.20
CMD ["echo", "你好", "docker"]

# 在 docker build 后直接 run 再对比下 下方命令的 run,看看差异 (test是容器名称,根据build的实际情况来)
docker run test echo "命令被覆盖了"
  • 结合写法
# Dockerfile

FROM node:22.9.0-alpine3.20

ENTRYPOINT ["echo", "张三"]

CMD ["到此一游"]

# run
docker run test
docker run test "你吃了吗"

当没传参数的时候,执行的是 ENTRYPOINT + CMD 组合的命令,而传入参数的时候,只有 CMD 部分会被覆盖,起到了默认值的作用。所以,用ENTRYPOINT + CMD 的方式更加灵活。

ADD 和 COPY

  • 新建一个文件夹,在里面随意新建几个文件,随后使用 tar 命令将文件夹内容打成 tar
# 打 tar 包命令 (打包当前目录下的test文件夹,并生成test.tar.gz压缩包)
tar -czvf test.tar.gz ./test
 
# 解压tar包命令 (解压test.tar.gz压缩包到当前目录)
tar -xzvf test.tar.gz 

12.png

  • 创建 Dockerfile

    • 下方 Dockerfile 省略了部分内容
FROM node:22.9.0-alpine3.20

# 会解压 .tar .tar.gz .zip等
ADD ./test.tar.gz /app/aaa

# 下载文件
# ADD https://example.com/file.txt /app/file.txt

COPY ./test.tar.gz /app/bbb
  • 打包
docker build -t test-add-copy -f Dockerfile .
  • 随后打开 docker desktop 的容器 testfile 选项卡,找到对应目录

13.png

可以看到,ADDtar.gz 给解压然后复制到容器内了,COPY 没有解压,它把文件整个复制过去了。

ADDCOPY 都可以用于把目录下的文件复制到容器内的目录下,但是 ADD 会自动解压 tar.gz 文件。

由于 COPY 指令更为简单且用途明确,对于大多数仅需复制文件或目录的场景, 推荐使用COPY。当你需要利用 ADD 的额外功能时,才应选择使用 ADD

Docker Compose

当项目依赖的服务较多时,每个容器都要单独配置运行,指定网络。使用 Docker Compose ,可以通过一个YAML文件定义服务,并同时运行它们。

Docker Compose将所管理的容器分为三层:工程(Project)、服务(Service)、容器(Container)

在要部署项目的目录创建一个 docker-compose.yml 文件

# 指定Docker Compose配置文件的版本
version: '3.8'

services: 
  # 定义应用服务,名为 app
  app:
    image: '仓库地址/命名空间/镜像名称:标签'
    # 将容器的8080端口映射到宿主机的8080端口
    ports: 
      - 8080:8080 
    volumes:
      # 将 docker-compose.yml 所在目录映射到容器中的 /app 目录(在 Dockerfile 中给定的工作目录) 
      - ./:/app 
    # 定义启动依赖,会先启动 mysqldb 和 redisdb,再启动 app
    depends_on:
      - mysqldb
      - redisdb
    # 指定容器启动后执行的命令
    command: ["java", "-jar", "app.jar"]   
    # 如果服务非手动停止,Docker会自动尝试重启服务
    restart: always

  # 定义一个MySQL服务,名为 mysqldb 
  # 其他服务连接MySQL数据库时,主机地址就是 mysqldb:3306
  mysqldb:
    image: mysql:8.0.30
    environment:
      - MYSQL_ROOT_PASSWORD=root
    volumes:
      - db_data:/var/lib/mysql

  # 定义一个Redis服务,名为 redisdb
  # 其他服务连接Redis数据库时,主机地址就是 redisdb:6379
  redisdb:
    image: redis:7.2.4
    volumes:
      - redis_data:/data

volumes:
  db_data:
  redis_data:

docker-compose.yml 文件所在的目录执行

docker compose up -d

docker compose up 根据 docker-compose.yml 文件内容启动、创建、连接服务。

-d 参数表示以后台方式运行。

-f 如果文件名称不是 docker-compose.yml ,可以通过 -f 命令指定

每次更改了 docker-compose.yml 文件,都需要重新运行 docker-compose up -d 命令以应用更改。

Docker容器基础概念

14.png

  • Namespace:实现各种资源的隔离

    • PID namespace: 进程 id 的命名空间
    • IPC namespace: 进程通信的命名空间
    • Mount namespace:文件系统挂载的命名空间
    • Network namespace:网络的命名空间
    • User namespace:用户和用户组的命名空间
    • UTS namespace:主机名和域名的命名空间
  • Control Group:实现容器进程的资源访问限制

  • UnionFS:实现容器文件系统的分层存储,镜像合并

15.png

我们通过 dockerfile 描述镜像构建的过程,每一条指令都是一个镜像层。

镜像通过 docker run 就可以跑起来,对外提供服务,这时会添加一个可写层(容器层)。

挂载一个 volume 数据卷到 Docker 容器,就可以实现数据的持久化。

Docker命令

# 查看镜像
docker images

# 查看容器
docker ps

# 查看容器日志命
docker logs ${容器id}

# 查看容器详细信息
docker inspect ${容器id}

# 停止容器
docker stop ${容器id}

# 强制停止容器
docker kill ${容器id}

# 删除容器
docker rm ${容器id}

# 删除镜像
docker rmi ${镜像名}:${标签名}

# 查看docker版本
docker version

# 查看docker信息
docker info

# 查看docker帮助命令
docker --help

# 通过 --build-arg xxx=yyy 传入 ARG 参数的值。
docker build --build-arg aaa=3 --build-arg bbb=4 -t arg-test -f Dockerfile .

# 拉取镜像,没有显式指定仓库地址时,默认会从DockerHub查找镜像;拉取私有仓库的镜像,需要指定仓库地址。
docker pull ${仓库地址/命名空间/镜像名称}:${标签}

# 启动容器命令
# -d 表示后台运行
# -p 3000:3000 表示将容器的3000端口映射到主机的3000端口
# --name 容器名 表示给容器命名
# 镜像名:标签名 表示使用哪个镜像启动容器
docker run -d -p ${访问端口}:3000 --name ${容器名} ${镜像名}:${标签名}

# 在正在运行的容器中执行命令
docker exec -it ${容器} ${命令}