背景
传统物理服务器部署:需要一台物理服务器,安装操作系统,安装应用程序。这种方式缺点:部署非常慢,成本高,资源浪费,迁移和扩展慢。解决这些问题的办法就是虚拟化技术。
为什么有虚拟机还要有 Docker 呢?
传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。
Docker 是在操作系统进程层面的隔离,而虚拟机是在物理资源层面的隔离。
Docker 概念
- 镜像
是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。
是分层存储
- 容器
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间 [。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。
- 仓库
像代码库一样,存放镜像的
安装 Docker
# macOS
# HomeBrew方式安装
brew install --cask docker
# 检查是否安装成功(命令输出有信息)
docker version
docker info
命令操作
镜像
# 获取镜像
docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
镜像仓库地址 默认为 docker.io
仓库名 = <用户名>/<软件名> 默认官方用户名 library
# 查看顶层镜像
docker image ls
# 所有镜像 (含有中间层镜像 不需要特意删除)
docker image ls -a
# digests 显示镜像摘要
docker image ls --digests
# 过滤出虚悬镜像 新旧镜像同名,旧镜像取消,标签 仓库显示 <none>
docker image ls -f dangling=true
## 删除
docker image prune
# 查看镜像、容器、数据卷所占用的空间
docker system df
# 删除镜像
docker image rm [选项] <镜像1> [<镜像2> ...]
docker rmi xxx
组合使用
# 删除 redis 镜像
docker image rm $(docker image ls -q redis)
# 删除 mongo:3.2 之前安装的镜像
docker image rm $(docker image ls -q -f before=mongo:3.2)
容器
# 新建并启动
docker run
-d 后台运行
-it 前台交互运行的方式
--name 指定要创建容器的名称
-v 宿主机目录 与 容器目录间映射
-p 将容器的端口映射到宿主机的端口
--rm 容器退出后删除,不跟 -d 同时使用
--restart 容器重启策略 no always 等
# 启动已终止的容器
docker container start <容器>
# 查看启动的容器
docker container ls
docker container ls -a 所有容器包括终止的
# 查看容器输出信息
docker container logs <容器>
docker inspect <容器>
# 终止容器
docker container stop
# 进入容器
docker exec -it xx /bin/bash
# 导入导出
docker export <containerId> > xx.tar
cat xx.tar | docker import - test/ubuntu:v1.0
docker load 导入是镜像存储文件, import 是快照
# 删除容器
docker container rm xx
## 删除运行中的容器
docker rm -f
## 删除所有终止的容器
docker container prune
# 容器重命名
docker rename <oldname> <newname>
数据卷
可以供一个或多个容器使用的特殊目录,可以在容器之间共享和重用,修改会立马生效,更新不会影响镜像,默认会一直存在,即使容器被删除
# 创建数据卷
docker volume create my-vol
# 查看
docker volume ls
docker volume inspect my-vol
# 删除
docker volume rm my-vol
## 删除无主的
docker volume prune
# 挂载卷
docker run -d -P \
--name web \
# -v my-vol:/usr/share/nginx/html \
--mount source=my-vol,target=/usr/share/nginx/html \
nginx:alpine
# 挂载主机目录作为数据卷
# 加载主机的 /src/webapp 目录到容器的 /usr/share/nginx/html目录,本地目录为绝对路径
docker run -d -P \
--name web \
# -v /src/webapp:/usr/share/nginx/html \
--mount type=bind,source=/src/webapp,target=/usr/share/nginx/html \
nginx:alpine
Dockerfile
每个指令都会建立一层,确保每一层都是真正需要的东西,无关的要清理掉
构建镜像
docker build [选项] <上下文路径/URL/->
在 Dockerfile 所在目录执行
docker build -t nginx:v3 . 末尾的点 指定 docker 引擎上下文 -t 最终的镜像名称
Docker 是 C/S 设计,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。
比如COPY ./package.json /app/,这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json。因此,COPY 这类指令中的源文件的路径都是相对路径。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
- 指令
| 指令 | 简介 |
|---|---|
| FROM | 指定基础镜像 如 nginx、scratch(空镜像) |
| RUN | 后面跟要执行的命令(shell 格式,exec 格式,数组里面用双引号) |
| COPY | 将构建上下文中的<源路径>文件目录复制到容器内的<目标路径>,注意:1)源路径是文件夹时,将文件夹的内容复制到目标路径;2)目标路径可以是容器内绝对路径,或相对于工作目录的相对路径 |
| ADD | 和 COPY 性质一致,适合需要自动解压缩的场合 |
| WORKDIR | <工作目录> 指定工作目录,以后各层的当前目录为指定的目录,不存在会创建 |
| USER | 改变之后层执行的身份 |
| CMD | 1)作为 ENTERYPOINT 的默认参数;2)单独使用时为指定默认的容器主进程的启动命令 |
| ENTRYPOINT | 指定容器启动参数,运行时可替换 |
| ENV | 指定环境变量 |
| ARG | 设置构建环境的环境变量,ARG <参数名>[=<默认值>],1)可用 --build-arg 覆盖,2)生效范围,FROM 之前 |
| EXPOSE | 仅仅声明容器提供的端口,不会自动和宿主端口映射 |
| LABEL | 添加元数据 |
| ONBUILD | 当前镜像不执行,其他镜像引用时执行 |
| VOLUME | 定义匿名卷,防止用户忘记挂载时,也能执行,不会向容器存储层写入大量数据。docker run -v mydata:/data,替代 Dockerfile 配置 |
CMD 与 ENTRYPOINT 区别
#1) Dockerfile 查看本地 IP
FROM ubuntu:18.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://myip.ipip.net" ]
#2) 构建镜像
docker build -t myip .
#3)查看IP
docker run myip
当我想查看 HTTP 头信息时,直接 docker run myip -i 会报错,因为跟在镜像名后面的命令会替换 CMD 默认值, -i 替换了原来的 CMD 值,当执行 docker run myip (相当于 docker run myip curl -s myip.ipip.net),而 docker run myip -i (相当于docker run myip -i),要实现 docker run myip -i 效果,将 Dockerfile 中 CMD 改为 ENTRYPOINT
- 多阶段构建
FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从上一阶段镜像中复制文件
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]
# 只构建某一阶段
docker build --target builder -t username/imagename:tag .
容器网络
# -P 随机映射一个端口到内部容器开放的网络端口
docker run -d -P nginx:alpine
# 本地的 80 端口映射到容器的 80 端口,默认会绑定本地所有接口上的所有地址
docker run -d -p 80:80 nginx:alpine
# 指定映射使用一个特定地址
docker run -d -p 127.0.0.1:80:80 nginx:alpine
# 指定地址任意端口
docker run -d -p 127.0.0.1::80 nginx:alpine
# 容器互联 --link 建议自定义网络
# 新建网络
docker network create -d bridge my-net
docker run 时 --network 指定
# 查看网络
docker network ls
BuildKit
下一代的镜像构建组件。
RUN --mount=type=cache每次获取依赖的时间,大大增加了镜像构建效率,同时也避免了生成了大量的中间层镜像
# syntax = docker/dockerfile:experimental
FROM node:alpine as builder
WORKDIR /app
COPY package.json /app/
# id 为 my_app_npm_module 的缓存文件夹挂载到了 /app/node_modules 文件夹中。多次执行也不会产生多个中间层镜像
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
--mount=type=cache,target=/root/.npm,id=npm_cache \
npm i --registry=https://registry.npm.taobao.org
COPY src /app/src
# 执行时需要用到 node_modules 文件夹,node_modules 已经挂载,命令也可以正确执行。
RUN --mount=type=cache,target=/app/node_modules,id=my_app_npm_module,sharing=locked \
# --mount=type=cache,target=/app/dist,id=my_app_dist,sharing=locked \
npm run build
FROM nginx:alpine
# COPY --from=builder /app/dist /app/dist
# 为了更直观的说明 from 和 source 指令,这里使用 RUN 指令
# 将上一阶段产生的文件复制到指定位置,from 指明缓存的来源,这里 builder 表示缓存来源于构建的第一阶段,source 指明缓存来源的文件夹
RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \
# --mount=type=cache,target/tmp/dist,from=my_app_dist,sharing=locked \
mkdir -p /app/dist && cp -r /tmp/dist/* /app/dist
Docker Compose
我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。它允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目。
Compose 面向项目进行管理,一个项目可以有多个服务。
| 指令关键字 | 简介 |
|---|---|
| build | 1)指定 Dockerfile 路径;2)context: 路径; dockerfile: 文件名; args: 构建镜像变量;cache_from: 镜像缓存 |
| command | 覆盖容器启动后默认执行的命令 |
| container_name | 容器名 |
| depends_on | 解决容器的依赖、启动先后的问题 |
| env_file | 从文件中获取环境变量,可以为单独的文件路径或列表 |
| environment | 设置环境变量 |
| expose | 暴露端口,但不映射到宿主机 |
| image | 指定为镜像名称或镜像 ID |
| labels | 为容器添加 Docker 元数据 |
| ports | 端口 |
| volumes | 数据卷所挂载路径设置 |
| network_mode | --network 参数一样的值 |
| networks | 配置容器连接的网络 |
实操
利用 nginx 容器访问 vue 打包后的静态文件
# vue-cli 初始化项目,默认即可
vue create learndk
# 打包
npm run build
# 方案一
# 将 docker run 命令保存在 vueapp.sh 文件中,执行用 sh vueapp.sh
docker run \
-p 3000:80 \
-d --name dxxtest \
--mount type=bind,source=项目绝对路径/nginx,target=/etc/nginx/conf.d \
--mount type=bind,source=项目绝对路径/dist,target=/usr/share/nginx/html \
nginx
#方案二
# docker-compose.yml 内容
version: '3.3'
services:
nginx:
image: nginx
volumes:
- ./dist:/usr/share/nginx/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
ports:
- "3000:80"
# 启动
docker-compose up -d