入门 Docker

235 阅读10分钟

背景

传统物理服务器部署:需要一台物理服务器,安装操作系统,安装应用程序。这种方式缺点:部署非常慢,成本高,资源浪费,迁移和扩展慢。解决这些问题的办法就是虚拟化技术。

为什么有虚拟机还要有 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改变之后层执行的身份
CMD1)作为 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 面向项目进行管理,一个项目可以有多个服务。

YAML 语法

指令关键字简介
build1)指定 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 打包后的静态文件 image.png

# 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

参考资源

Docker 从入门到实践