Giter Site home page Giter Site logo

jayfate's Introduction

jayfate's People

Contributors

jayfate avatar

Stargazers

 avatar

Watchers

 avatar

jayfate's Issues

使用nodejs&mysql&docker来搭建API服务

Docker提供轻量级容器来运行我们的服务,以便于快速开发/交付软件。在本文我们将探讨如何使用Docker Compose来集成 Nodejs Express 和 MySQL 应用。

Node.js 和 MySQL 与 Docker 概述

我们可以使用 docker compose 在容器中同时提供 nodejs API 服务和 mysql 数据库,具体的步骤为:

  • 创建一个使用 MySQL 数据库的 应用
  • 创建 nodejs 应用的 Dockerfile
  • 创建 docker-compose.yml
  • 设置 docker compose 的环境变量

目录结构:

docker-nodejs-mysql (main) $ tree 
.
├── README.md
├── docker-compose.yml
└── jayfate-app
    ├── Dockerfile
    ├── README.md
    ├── app
    │   ├── config
    │   │   └── db.config.js
    │   ├── controllers
    │   │   └── tutorial.controller.js
    │   ├── models
    │   │   ├── index.js
    │   │   └── tutorial.model.js
    │   └── routes
    │       └── turorial.routes.js
    ├── package.json
    └── server.js

创建 Nodejs 应用程序

jayfate-app 运行 npm init -y,并修改 package.json 的内容为:

jayfate-app/package.json

{
  "name": "nodejs-express-sequelize-mysql",
  "version": "1.0.0",
  "description": "Node.js Rest Apis with Express, Sequelize & MySQL",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "nodejs",
    "express",
    "rest",
    "api",
    "sequelize",
    "mysql"
  ],
  "author": "jayfate",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^10.0.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "mysql2": "^2.0.2",
    "sequelize": "^5.21.2"
  }
}

然后编写 jayfate-app/server.js

// 导入`dotenv` 用于设置`process.env`,用于设置端口
require("dotenv").config();
const express = require("express");
const cors = require("cors");

const app = express();

var corsOptions = {
  origin: "http://localhost:8081"
};

app.use(cors(corsOptions));

// parse requests of content-type - application/json
app.use(express.json());

// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));

const db = require("./app/models");

db.sequelize.sync();
// // drop the table if it already exists
// db.sequelize.sync({ force: true }).then(() => {
//   console.log("Drop and re-sync db.");
// });

// simple route
app.get("/", (req, res) => {
  res.json({ message: "Welcome to jayfate application." });
});

require("./app/routes/turorial.routes")(app);

// set port, listen for requests
const PORT = process.env.NODE_DOCKER_PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

然后编写 mysql 数据库的配置

app/config/db.config.js

module.exports = {
  HOST: process.env.DB_HOST,
  USER: process.env.DB_USER,
  PASSWORD: process.env.DB_PASSWORD,
  DB: process.env.DB_NAME,
  port: process.env.DB_PORT,
  dialect: "mysql",
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  }
};

app/models/index.js

onst dbConfig = require("../config/db.config.js");

const Sequelize = require("sequelize");
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
  host: dbConfig.HOST,
  dialect: dbConfig.dialect,
  port: dbConfig.port,
  operatorsAliases: false,

  pool: {
    max: dbConfig.pool.max,
    min: dbConfig.pool.min,
    acquire: dbConfig.pool.acquire,
    idle: dbConfig.pool.idle
  }
});

const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;

db.tutorials = require("./tutorial.model.js")(sequelize, Sequelize);

module.exports = db;

编写 jayfate-app/.env.sample 来设置环境变量

jayfate-app/.env.sample

DB_HOST=localhost
DB_USER=root
DB_PASSWORD=123456
DB_NAME=jayfate_db
DB_PORT=3306

NODE_DOCKER_PORT=8080

编写 Dockerfile

编写我们的 nodejs 应用的 Dockerfile

jayfate-app/Dockerfile

FROM node:14

WORKDIR /jayfate-app
COPY package.json .
RUN npm install
COPY . .
CMD npm start

其中:

  • FROM:指定 Node.js的镜像
  • WORKDIR:工作目录
  • COPYCOPY package.json .package.json 文件复制到容器中,COPY . . 复制项目中的所有文件。
  • RUN:在容器内执行命令 npm install安装package.json中的依赖项。
  • CMD: 镜像构建后运行 npm start

编写 Docker Compose 配置

在项目目录的根目录中创建 docker-compose.yml

docker-compose.yml

让我们来实现细节。

docker-compose.yml

version: '3.8'

services:
  mysqldb:
    image: mysql:5.7
    restart: unless-stopped
    env_file: ./.env
    environment:
      - MYSQL_ROOT_PASSWORD=$MYSQLDB_ROOT_PASSWORD
      - MYSQL_DATABASE=$MYSQLDB_DATABASE
    ports:
      - $MYSQLDB_LOCAL_PORT:$MYSQLDB_DOCKER_PORT
    volumes:
      - db:/var/lib/mysql
  app:
    depends_on:
      - mysqldb
    build: ./jayfate-app
    restart: unless-stopped
    env_file: ./.env
    ports:
      - $NODE_LOCAL_PORT:$NODE_DOCKER_PORT
    environment:
      - DB_HOST=mysqldb
      - DB_USER=$MYSQLDB_USER
      - DB_PASSWORD=$MYSQLDB_ROOT_PASSWORD
      - DB_NAME=$MYSQLDB_DATABASE
      - DB_PORT=$MYSQLDB_DOCKER_PORT
    stdin_open: true
    tty: true

volumes: 
  db:
  • version:将使用 Docker Compose 文件格式版本

  • services:指定隔离容器中的单独服务,我们有两个服务:app(Nodejs)和mysqldb(MySQL 数据库)

  • volumes:命名卷在重新启动后使我们的数据保持活动状态

  • mysqldb

    • image:指定 Docker 镜像

    • restart:配置重启策略

    • env_file:指定 .env 的路径

    • environment:指定环境变量

    • ports:指定端口

    • volumes:映射卷文件夹

  • app

    • depends_on:指定 app 服务依赖 mysqldb 服务先启动

    • build:在构建时应用的配置选项,我们在 Dockerfile 中使用相对路径定义

    • environment:指定Node 应用程序的环境变量

    • stdin_opentty:构建容器后保持终端开启

请注意,主机端口 ( LOCAL_PORT) 和容器端口 ( DOCKER_PORT) 是不同的。docker compose 内部各服务之间通信使用容器端口,我们外部访问使用主机端口。

定义 MySQL 的环境变量

使用 .env 定义MySQL的环境变量:

.env

MYSQLDB_USER=root
MYSQLDB_ROOT_PASSWORD=123456
MYSQLDB_DATABASE=jayfate_db
MYSQLDB_LOCAL_PORT=3307
MYSQLDB_DOCKER_PORT=3306

NODE_LOCAL_PORT=6868
NODE_DOCKER_PORT=8080

运行 Docker Compose 应用

可以通过一下命令来运行 Docker Compose 应用

docker-compose up
# 或者以后台方式运行
docker-compose up -d

运行时日志如下:

$ docker-compose up -d
Creating network "node-mysql_default" with the default driver
Creating volume "node-mysql_db" with default driver
Pulling mysqldb (mysql:5.7)...
5.7: Pulling from library/mysql
...
...
`docker-compose build` or `docker-compose up --build`.
Creating node-mysql_mysqldb_1 ... done
Creating node-mysql_app_1     ... done

可以使用 docker ps 查看当前运行中的容器:

$ docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED         STATUS              PORTS                                                  NAMES
b8b12819d371   node-mysql_app   "docker-entrypoint.s…"   2 minutes ago   Up About a minute   0.0.0.0:6868->8080/tcp, :::6868->8080/tcp              node-mysql_app_1
b0d665c00073   mysql:5.7        "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes        33060/tcp, 0.0.0.0:3307->3306/tcp, :::3307->3306/tcp   node-mysql_mysqldb_1

docker images 查看 Docker 镜像:

$ docker images
REPOSITORY            TAG            IMAGE ID       CREATED         SIZE
node-mysql_app        latest         9d0109ff706c   5 minutes ago   965MB
node                  14             e0ab58ea4a4f   6 minutes ago   944MB
mysql                 5.7            8cf625070931   6 minutes ago   448MB

停止应用程序

通过一下命令停止 docker-compose 服务

docker-compose down

或者停止 docker-compose 服务并删除所有容器和镜像

docker-compose down --rmi all

结论

今天我们已经成功为 MySQL 和 Nodejs 应用程序创建了 Docker Compose 文件。现在我们可以通过非常简单的方式使用 Docker 部署 Nodejs Express 和 MySQL:docker-compose.yml

源码

可以在Github上查看本文对应的源码

https://www.bezkoder.com/docker-compose-nodejs-mysql/

docker 学习笔记

一、入门文档

  1. Docker 入门教程-阮一峰
  2. docker入门到实践

docker 新手练习

https://www.jianshu.com/p/bd5a8945e071

docker run -d -p 81:80 nginx
# 81 是宿主本机端口
# -d 以后台守护进程启动

docker run -d -p 92:80 --name container-name -v `pwd`:/usr/share/nginx/html nginx
# --name 指定运行起来的容器名
# nginx 运行使用的镜像名
# -v:外部路径:内部路径,可以进行文件的映射,可以进行数据的保存,将数据保存在外部存储盘中

docker run -it --rm ubuntu:18.04 bash
# -it:以交互式终端方式运行,一个是 -i:交互式操作,一个是 -t 终端。我们这里打算进入 bash 执行一些命令并查看返回结果,因此我们需要交互式终端。
# --rm:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 docker rm。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 --rm 可以避免浪费空间。

docker build -t zcdf .
# -t tag
# . 镜像构建上下文  https://yeasy.gitbook.io/docker_practice/image/build#qi-ta-docker-build-de-yong-fa

docker image ls
# 列出已经下载下来的镜像,只会显示顶层镜像

docker image ls -a
# 显示包括中间层镜像在内的所有镜像

docker image ls ubuntu
# 根据仓库名列出镜像
docker image ls ubuntu:18.04
# 列出特定的某个镜像,也就是说指定仓库名和标签

docker image ls -f since=mongo:3.2
# 列出 在 mongo:3.2 之后建立的镜像

docker image prune
# 删除 虚悬镜像

docker image rm 501ad78535f0
# 删除镜像,实际上是删除镜像标签。
# 当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。
docker image rm centos

docker image rm $(docker image ls -q redis)
# 删除所有仓库名为 redis 的镜像

docker image rm $(docker image ls -q -f before=mongo:3.2)
# 删除所有在 mongo:3.2 之前的镜像

docker run --name webserver -d -p 80:80 nginx
# 以 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口
docker exec -it webserver bash
# 以交互式终端方式进入 webserver 容器,并执行了 bash 命令,
# 可以将 nginx 首页(/usr/share/nginx/html/index.html)内容修改为  <h1>Hello, Docker!</h1>

aaa

docker diff webserver
# 查看对 webserver 容器的存储层具体的改动
# docker commit 命令,可以将容器的存储层保存下来成为镜像

docker commit \
    --author "Tao Wang <[email protected]>" \
    --message "修改了默认网页" \
    webserver \
    nginx:v2
# --author 是指定修改的作者,而 --message 则是记录本次修改的内容。

docker run --name web2 -d -p 81:80 nginx:v2
# 运行我们制作的镜像

Dockerfile示例

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
# FROM 指定 基础镜像
# RUN 执行命令行命令,RUN 指令执行命令有两种格式: shell 格式 和 exec 格式
# shell 格式: RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
# exec 格式: RUN ["可执行文件", "参数1", "参数2"]

Dockerfile 中每一个指令都会建立一层镜像

FROM debian:stretch

RUN set -x; buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps
# 这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件

构建镜像

docker build -t nginx:v3 .
# docker build [选项] <上下文路径/URL/->

Dockerfile 文件所在目录执行 docker build -t nginx:v3 . 来构建 docker 镜像。后有一个 . 指定 上下文路径。当构建镜像的时候,用户指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

一般来说,应该将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,可以用 .dockerignore,剔除不需要不需要的文件。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

使用用 Git repo 进行构建

# $env:DOCKER_BUILDKIT=0
# export DOCKER_BUILDKIT=0
docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

这行命令指定了构建所需的 Git repo,并且指定分支为 master,构建目录为 /amd64/hello-world/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

其他构建方式

  1. 用给定的 tar 压缩包构建
docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

  1. 从标准输入中读取 Dockerfile 进行构建
docker build - < Dockerfile

cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

  1. 从标准输入中读取上下文压缩包进行构建
docker build - < context.tar.gz

如果发现标准输入的文件格式是 gzipbzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

Dockerfile 指令详解

COPY

格式:

  • COPY [--chown=<user>:<group>] <源路径>... <目标路径>
  • COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/

COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

ADD

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

CMD

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD/bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

一些初学者将 CMD 写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让我们来看几个场景。

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

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" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

docker run myip
# 当前 IP:61.148.226.66 来自:北京市 联通

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

docker run myip -i
# docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

docker run myip curl -s http://myip.ipip.net -i

这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:

FROM ubuntu:18.04

RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

这次我们再来尝试直接使用 docker run myip -i

docker run myip
# 当前 IP:61.148.226.66 来自:北京市 联通

docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	find . \! -user redis -exec chown redis '{}' +
	exec gosu redis "$0" "$@"
fi

exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

ENV

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

下列指令可以支持环境变量展开: ADDCOPYENVEXPOSEFROMLABELUSERWORKDIRVOLUMESTOPSIGNALONBUILDRUN

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo ${DOCKER_USERNAME}

使用上述 Dockerfile 会发现无法输出 ${DOCKER_USERNAME} 变量的值,要想正常输出,你必须在 FROM 之后再次指定 ARG

# 只在 FROM 中生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

对于多阶段构建,尤其要注意这个问题

# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 1

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 2

对于上述 Dockerfile 两个 FROM 指令都可以使用 ${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在每个阶段分别指定:

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

VOLUME

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app
RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

WORKDIR /app

RUN echo "hello" > world.txt

如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd 的工作目录为 /a/b/c

USER

格式:USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

注意,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy

HEALTHCHECK 支持下列选项:

  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 DockerfileHEALTHCHECK 可以这么写:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。

使用 docker build 来构建这个镜像:

docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker container ls 看到最初的状态为 (health: starting)

docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次 docker container ls,就会看到健康状态变化为了 (healthy)

docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。

docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

LABEL

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。

LABEL <key>=<value> <key>=<value> <key>=<value> ...

我们还可以用一些标签来申明镜像的作者、文档地址等:

LABEL org.opencontainers.image.authors="yeasy"

LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"

SHELL

格式:SHELL ["executable", "parameters"]

SHELL 指令可以指定 RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]

SHELL ["/bin/sh", "-c"]

RUN lll ; ls

SHELL ["/bin/sh", "-cex"]

RUN lll ; ls

两个 RUN 运行同一命令,第二个 RUN 运行的命令会打印出每条命令并当遇到错误时退出。

ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell

SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
CMD nginx

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(exited)的容器重新启动。

因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。

新建并启动

例如,下面的命令输出一个 “Hello World”,之后终止容器。

$ docker run ubuntu:18.04 /bin/echo 'Hello world'
Hello world

这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。

下面的命令则启动一个 bash 终端,允许用户进行交互。

$ docker run -t -i ubuntu:18.04 /bin/bash
root@af8bae53bdd3:/#

其中,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。

在交互模式下,用户可以通过所创建的终端来输入命令,例如

root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

启动已终止容器

可以利用 docker container start 命令,直接将一个已经终止(exited)的容器启动运行。

$ docker container start [OPTIONS] CONTAINER [CONTAINER...]

可以在伪终端中利用 pstop 来查看进程信息。

root@ba267838cc1b:/# ps
  PID TTY          TIME CMD
    1 ?        00:00:00 bash
   11 ?        00:00:00 ps

可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。

后台运行

更多的时候,需要让 Docker 在后台运行而不是直接把执行命令的结果输出在当前宿主机下。此时,可以通过添加 -d 参数来实现。

下面举两个例子来说明一下。

如果不使用 -d 参数运行容器。

$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

容器会把输出的结果 (STDOUT) 打印到宿主机上面

如果使用了 -d 参数运行容器。

$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 docker logs 查看)。

注: 容器是否会长久运行,是和 docker run 指定的命令有关,和 -d 参数无关。

使用 -d 参数启动后会返回一个唯一的 id,也可以通过 docker container ls 命令来查看容器信息。

$ docker container ls
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
77b2dc01fe0f  ubuntu:18.04  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

要获取容器的输出信息,可以通过 docker container logs 命令。

$ docker container logs [container ID or NAMES]
hello world
hello world
hello world
. . .

终止容器

可以使用 docker container stop 来终止一个运行中的容器。

此外,当 Docker 容器中指定的应用终结时,容器也自动终止。

例如对于上一章节中只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。

终止状态的容器可以用 docker container ls -a 命令看到。例如

$ docker container ls -a
CONTAINER ID        IMAGE                    COMMAND                CREATED             STATUS                          PORTS               NAMES
ba267838cc1b        ubuntu:18.04             "/bin/bash"            30 minutes ago      Exited (0) About a minute ago                       trusting_newton

处于终止状态的容器,可以通过 docker container start 命令来重新启动。

此外,docker container restart 命令会将一个运行态的容器终止,然后再重新启动它。

进入容器

在使用 -d 参数时,容器启动后会进入后台。

某些时候需要进入容器进行操作,包括使用 docker attach 命令或 docker exec 命令,推荐大家使用 docker exec 命令,原因会在下面说明。

attach 命令

下面示例如何使用 docker attach 命令。

$ docker run -dit ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia

$ docker attach 243c
root@243c32535da7:/#

注意: 如果从这个 stdin 中 exit,会导致容器的停止。

exec 命令

docker exec 后边可以跟多个参数,这里主要说明 -i -t 参数。

只用 -i 参数时,由于没有分配伪终端,界面没有我们熟悉的 Linux 命令提示符,但命令执行结果仍然可以返回。

-i -t 参数一起使用时,则可以看到我们熟悉的 Linux 命令提示符。

$ docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
69d137adef7a        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           zealous_swirles

$ docker exec -i 69d1 bash
ls
bin
boot
dev
...

$ docker exec -it 69d1 bash
root@69d137adef7a:/#

使用docker attach 命令进入 container(容器)有一个缺点,那就是每次从 container 中退出到前台时,container 也跟着退出了。

要想退出 container 时,让 container 仍然在后台运行着,可以使用docker exec -it命令。每次使用这个命令进入 container,当退出container 后,container 仍然在后台运行,命令使用方法如下:

docker exec -it container1 /bin/bash

这样输入exit或者按键 Ctrl + C 退出 container 时,这个 container 仍然在后台运行,通过:docker ps就可以查找到。通过

docker container stop container1

可以终止 container1。

导出和导入容器

导出容器

如果要导出本地某个容器,可以使用 docker export 命令。

$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:18.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test
$ docker export 7691a814370e > ubuntu.tar

这样将导出容器快照到本地文件。

导入容器快照

可以使用 docker import 从容器快照文件中再导入为镜像,例如

$ cat ubuntu.tar | docker import - test/ubuntu:v1.0
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

此外,也可以通过指定 URL 或者某个目录来导入,例如

$ docker import http://example.com/exampleimage.tgz example/imagerepo

注:用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息(即仅保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大。此外,从容器快照文件导入时可以重新指定标签等元数据信息。

删除容器

可以使用 docker container rm 来删除一个处于终止状态的容器。例如

$ docker container rm trusting_newton
trusting_newton

如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。

清理所有处于终止状态的容器

docker container ls -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用下面的命令可以清理掉所有处于终止状态的容器。

$ docker container prune

镜像仓库管理

Docker 官方公共仓库 Docker Hub,其中已经包括了数量超过 2,650,000 的镜像。

常用命令:

$ docker login
$ docker logout
$ docker search centos
$ docker search centos --filter=stars=N
$ docker pull centos

$ docker tag ubuntu:18.04 username/ubuntu:18.04

$ docker image ls

REPOSITORY                                               TAG                    IMAGE ID            CREATED             SIZE
ubuntu                                                   18.04                  275d79972a86        6 days ago          94.6MB
username/ubuntu                                          18.04                  275d79972a86        6 days ago          94.6MB

$ docker push username/ubuntu:18.04

$ docker search username

NAME                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
username/ubuntu

自动构建

2021 年 7 月 26 日之后,该项功能仅限付费用户使用。

自动构建(Automated Builds)可以自动触发构建镜像,方便升级镜像。

有时候,用户构建了镜像,安装了某个软件,当软件发布新版本则需要手动更新镜像。

而自动构建允许用户通过 Docker Hub 指定跟踪一个目标网站(支持 GitHubBitBucket)上的项目,一旦项目发生新的提交 (commit)或者创建了新的标签(tag),Docker Hub 会自动构建镜像并推送到 Docker Hub 中。

Docker 数据管理

在容器中管理数据主要有两种方式:

  • 数据卷(Volumes)

  • 挂载主机目录 (Bind mounts)

数据卷

数据卷 是一个可供一个或多个容器使用的特殊目录,它绕过 UnionFS,可以提供很多有用的特性:

  • 数据卷 可以在容器之间共享和重用

  • 数据卷 的修改会立马生效

  • 数据卷 的更新,不会影响镜像

  • 数据卷 默认会一直存在,即使容器被删除

注意:数据卷 的使用,类似于 Linux 下对目录或文件进行 mount,镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。

创建数据卷

$ docker volume create my-vol

查看所有的 数据卷

$ docker volume ls

DRIVER              VOLUME NAME
local               my-vol

在主机里使用以下命令可以查看指定 数据卷 的信息

$ docker volume inspect my-vol
[
    {
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

启动挂载数据卷的容器

在用 docker run 命令的时候,使用 --mount 标记来将 数据卷 挂载到容器里。在一次 docker run 中可以挂载多个 数据卷

下面创建一个名为 web 的容器,并加载一个 数据卷 到容器的 /usr/share/nginx/html 目录。

$ docker run -d -P \
    --name web \
    # -v my-vol:/usr/share/nginx/html \
    --mount source=my-vol,target=/usr/share/nginx/html \
    nginx:alpine

查看数据卷

在主机里使用以下命令可以查看 web 容器的信息

$ docker inspect web

数据卷 信息在 "Mounts" Key 下面

"Mounts": [
    {
        "Type": "volume",
        "Name": "my-vol",
        "Source": "/var/lib/docker/volumes/my-vol/_data",
        "Destination": "/usr/share/nginx/html",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

删除数据卷

$ docker volume rm my-vol

数据卷 是被设计用来持久化数据的,它的生命周期独立于容器,Docker 不会在容器被删除后自动删除 数据卷,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 docker rm -v 这个命令。

无主的数据卷可能会占据很多空间,要清理请使用以下命令

$ docker volume prune

挂载主机目录

挂载一个主机目录作为数据卷

使用 --mount 标记可以指定挂载一个本地主机的目录到容器中去。

$ 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

上面的命令加载主机的 /src/webapp 目录到容器的 /usr/share/nginx/html目录。这个功能在进行测试的时候十分方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,以前使用 -v 参数时如果本地目录不存在 Docker 会自动为你创建一个文件夹,现在使用 --mount 参数时如果本地目录不存在,Docker 会报错。

Docker 挂载主机目录的默认权限是 读写,用户也可以通过增加 readonly 指定为 只读

$ docker run -d -P \
    --name web \
    # -v /src/webapp:/usr/share/nginx/html:ro \
    --mount type=bind,source=/src/webapp,target=/usr/share/nginx/html,readonly \
    nginx:alpine

加了 readonly 之后,就挂载为 只读 了。如果你在容器内 /usr/share/nginx/html 目录新建文件,会显示如下错误

/usr/share/nginx/html # touch new.txt
touch: new.txt: Read-only file system

查看数据卷的具体信息

在主机里使用以下命令可以查看 web 容器的信息

$ docker inspect web

挂载主机目录 的配置信息在 "Mounts" Key 下面

"Mounts": [
    {
        "Type": "bind",
        "Source": "/src/webapp",
        "Destination": "/usr/share/nginx/html",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
],

挂载一个本地主机文件作为数据卷

--mount 标记也可以从主机挂载单个文件到容器中

$ docker run --rm -it \
   # -v $HOME/.bash_history:/root/.bash_history \
   --mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
   ubuntu:18.04 \
   bash

root@2affd44b4667:/# history
1  ls
2  diskutil list

这样就可以记录在容器输入过的命令了。

提供 web 服务

暂时略过

Docker Compose

Docker Compose 是 Docker 官方编排(Orchestration)项目之一,负责快速的部署分布式应用。

目前不再需要安装 docker-compose,并使用 docker compose 命令代替 docker-compose

首先介绍几个术语。

  • 服务 (service):一个应用容器,实际上可以运行多个相同镜像的实例。

  • 项目 (project):由一组关联的应用容器组成的一个完整业务单元。

可见,一个项目可以由多个服务(容器)关联而成,Compose 面向项目进行管理。

最常见的项目是 web 网站,该项目应该包含 web 应用和缓存。

下面我们用 Python 来建立一个能够记录页面访问次数的 web 网站。

web 应用

新建文件夹,新建 app.py

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

Dockerfile

新建 Dockerfile

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

docker-compose.yml

新建 docker-compose.yml

version: '3'
services:

  web:
    build: .
    ports:
     - "5000:5000"
     # 第一个 5000 为宿主机器端口号,第二个为容器内端口号

  redis:
    image: "redis:alpine"

运行 compose 项目

$ docker-compose up

此时访问本地 5000 端口,每次刷新页面,计数就会加 1。

使用Homebrew安装Mysql

步骤

  1. brew update
  2. brew install mysql

img

其中运行 mysql_secure_installation 可以设置密码

运行 brew service start mysql 可以后台启动 mysql

运行 mysql.server start 前台启动 mysql (关闭控制台,服务停止)

按照 brew 的提示运行 mysql_secure_installation 设置密码,运行后会报错:

 > mysql_secure_installation

Securing the MySQL server deployment.

Enter password for user root:
Error: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)

这个错误是因为MySQL服务还没启动

  1. 启动MySQL服务

mysql.server start

img

在我的机器上第一次没报错,以后运行此命令有可能会报权限错误。尝试修改权限但是最后还是不行,最后采用sudo启动解决此问题。

sudo mysql.server start

img

  1. 设置密码

mysql_secure_installation

如果报权限错误执行 sudo mysql_secure_installation

Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD PLUGIN can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD plugin?

Press y|Y for Yes, any other key for No: y

There are three levels of password validation policy:

LOW    Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary                  file
// 这里提示选一个密码强度等级
Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1
Please set the password for root here.
// 然后按照所选的密码强度要求设定密码
New password:

Re-enter new password:

Estimated strength of the password: 50
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
 ... Failed! Error: Your password does not satisfy the current policy requirements

New password:

Re-enter new password:

Estimated strength of the password: 100
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.
// 这里删除默认无密码用户
Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.


Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.
// 禁止远程root登录,我选的是不禁止。因为我的mac上的数据库不会放到公网上,也不会存什么敏感数据
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : no

 ... skipping.
By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.

// 这里删除默认自带的test数据库
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y
 - Dropping test database...
Success.

 - Removing privileges on test database...
Success.

Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.

All done!
  1. 连接MySQL

mysql -u root -p

最后特别注意启动方式的问题:

运行 brew service start mysql 可以后台启动mysql

运行 mysql.server start 前台启动mysql

如果提示权限问题使用sudo指令,我的笔记本上如果不加此指令各种权限问题。

docker-composes使用.sql文件初始化mysql容器

使用docker-compose和MySQL容器导入SQL文件,可以按照以下步骤进行操作:

  1. 创建一个MySQL Docker容器,并将需要导入的SQL文件放入一个本地目录中。例如,将SQL文件放在本地目录/path/to/sql/file中。
  2. docker-compose.yml文件中添加MySQL服务定义,并将本地SQL文件挂载到MySQL容器中。示例配置如下:
version: '3'

services:
  mysql:
    image: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - /path/to/sql/file:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"

在这个示例配置中,我们使用了MySQL官方的Docker镜像,并将本地的SQL文件目录挂载到容器内的/docker-entrypoint-initdb.d目录中,该目录是MySQL容器启动时会执行的初始化脚本所在的目录。

  1. 运行docker-compose up命令来启动MySQL容器和导入SQL文件。在启动容器时,MySQL将自动执行/docker-entrypoint-initdb.d目录中的SQL文件。
docker-compose up -d

启动后,您可以通过MySQL客户端连接到容器中的MySQL服务器并验证导入的SQL文件是否成功。例如,可以使用以下命令连接到MySQL容器:

mysql -h 127.0.0.1 -u root -p

在输入密码后,您应该可以看到已成功连接到MySQL服务器,然后可以运行SQL命令来验证是否导入了SQL文件。

总之,您可以通过docker-compose和MySQL容器将SQL文件导入到MySQL数据库中。步骤包括创建一个MySQL Docker容器,将SQL文件挂载到容器中的初始化脚本目录中,然后使用docker-compose up命令启动容器。

iTerm2中设置跳转&删除等快捷键

作为一名软件工程师,我发现自己每天都在使用终端来运行各种各样的命令。我目前选择的终端是iTerm2,我已经愉快地使用了很多年了。

每当我在新的Mac上设置iTerm2时,我做的第一件事就是为命令提示符中的常见导航和编辑操作配置熟悉的键盘快捷键。例如,我通常会配置⌥←的快捷键,让我跳到光标下的单词的开头。

要在iTerm2中配置自定义键盘快捷键,请打开偏好对话框,并导航到 "配置文件""按键""按键映射"标签。

image.png

点击 "+"按钮添加一个新的按键映射,或者双击现有的按键映射来编辑它。对于 "跳到单词开头 "命令,选择 "发送转义序列 "动作并发送转义序列Esc+b

image.png

现在,每当你在iTerm2中输入一个命令时,真的很容易跳回到单词(甚至是多个单词)的开头,以插入更多的文字或删除命令的一部分--不再需要反复按←键来逐字导航。

下面是我为各种跳转和删除命令配置的全部键盘快捷键列表。

快捷方式 命令 Action 发送
⌥← 跳到字的开头 Send Escape Sequence b
⌥→ 跳到字的末尾 Send Escape Sequence f
⌘← 跳到行的开头 Send Hex Code 0x01
⌘→ 跳到行尾 Send Hex Code 0x05
⌥⌫ 删除到字的开头 Send Hex Code 0x17
⌘⌫ 删除整行 Send Hex Code 0x15

navicat连接docker-mysql

1:获取MySQL镜像

运行 docker pull mysql

[root@MyCentos7-1 ~]# docker pull mysql  
Using default tag: latest  
latest: Pulling from library/mysql  
85b1f47fba49: Pull complete   
2a809168ab45: Pull complete   
Digest: sha256:1a2f9361228e9b10b4c77a651b460828514845dc7ac51735b919c2c4aec864b7  
Status: Downloaded newer image for mysql:latest  

2:启动MySQL镜像

[root@MyCentos7-1 ~]# docker run --restart=always --name kitking-mysql -e MYSQL_ROOT_PASSWORD=rad_xxx -p 3306:3306 -d mysql  
eb3dbfb0958f5c856323e4d8da60d43194884ff05d7adac1ec059adb66ac7f7b  

docker run是启动容器的命令;

--name:指定了容器的名称,方便之后进入容器的命令行

-itd:其中,i是交互式操作,t是一个终端,d指的是在后台运行

-p:指在本地生成一个随机端口,用来映射mysql的3306端口

-e:设置环境变量

MYSQL_ROOT_PASSWORD=emc123123:指定了mysql的root密码

mysql:指运行mysql镜像

3:进入MySQL容器

运行 docker exec -it kitking-mysql /bin/bash

[root@MyCentos7-1 ~]# docker exec -it kitking-mysql /bin/bash
root@my-mysql-v1-nths4:/usr/local/mysql# 

4:进入MySQL

运行 mysql -uroot -p

root@my-mysql-v1-nths4:/usr/local/mysql# mysql -uroot -p 
Enter password: 
mysql> show databases;

+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
+--------------------+
3 rows in set (0.02 sec)

5:进行配置,使外部工具可以连接

接着,由于mysql中root执行绑定在了localhost,因此需要对root进行授权,代码如下,

mysql> ALTER user 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; 
Query OK, 0 rows affected (0.01 sec) 
mysql>  
mysql> FLUSH PRIVILEGES; 
Query OK, 0 rows affected (0.01 sec) 

最后,使用navitecat测试mysql连接,如下,

img

使用Docker运行Mysql

要运行 MySQL 容器,首先需要拉取 docker 镜像

# docker pull mysql:latest
docker pull mysql:5.7

启动MySQL容器

docker run -d --name mysql_server -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
  • MYSQL_ROOT_PASSWORD: 设置root 用户密码
  • -d : 以后台方式运行
  • mysql_server: 容器名称

可以使用 docker exec 进入 mysql_server 容器

docker exec -it mysql_server bash
  • -it: 以交互终端方式执行

要从主机获得与 mysql 控制台的直接交互式会话,请键入:

docker exec -it mysql_server mysql -uroot -p

Docker MySQL 环境变量

当我们启动一个新实例时,我们可以通过 docker run 命令传递一个或多个环境变量来调整 MySQL 实例的配置。

  • MYSQL_ROOT_PASSWORD - 设置 MySQL root 用户的密码。必选变量
  • MYSQL_USER、MYSQL_PASSWORD - 创建新的 MySQL 用户并设置用户密码。可选变量
  • MYSQL_DATABASE - 启动时创建一个新数据库。如果创建了用户,该用户将被授予对数据库的超级用户访问权限。可选变量
  • MYSQL_ALLOW_EMPTY_PASSWORD - 如果设置为“yes”,新容器可以使用空 root 密码启动
  • MYSQL_RANDOM_ROOT_PASSWORD - 如果设置为“yes”,将为 root 用户创建一个随机初始密码

启动 mysql_server 容器并创建 test_db 数据库

docker run -d --name mysql_server -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=  mysql:5.7

创建 myuser 用户并设置密码

docker run -d --name mysql_server -e MYSQL_ROOT_PASSWORD=123456 -e MYSQL_DATABASE=test_db -e MYSQL_USER=myuser -e MYSQL_PASSWORD=mypass  mysql:5.7

使用空 root 密码启动 mysql_server 容器

docker run -d --name mysql_server -e MYSQL_ALLOW_EMPTY_PASSWORD="yes" -e MYSQL_ROOT_PASSWORD="" mysql:5.7

与另一个 docker 容器链接

通常,您需要将 mysql 实例与其他容器链接。例如,以下命令将启动一个新的 Apache HTTPD 容器,其中包含指向名为“mysql_server”的 mysql 实例的链接。

docker run -d --name apache-web --link mysql_server:mysql httpd:latest

apache 容器现在可以访问 mysql 容器内的 mysql 服务器。连接mysql服务器时,mysql主机应该是MySQL容器的名称或ID。

mysql -p -h mysql_server

启动具有远程访问功能的 MySQL 容器

为了允许远程访问MySQL容器,我们需要在创建新实例时将主机端口3306与容器端口3306映射。

docker run -d -p 3306:3306 --name mysql_server -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

从主机上,您可以使用127.0.0.1 作为 MySQL 主机来访问 MySQL 控制台。

mysql -p -h 127.0.0.1

请注意,端口映射应在创建容器时完成。

使用自定义 MySQL 配置文件

启动配置文件是/etc/mysql/my.cnf文件,该文件又包含在/etc/mysql/conf.d或/etc/mysql/mysql.conf.d目录中找到的以 . .cnf 扩展名

当我们启动一个新的容器时,我们可以将配置目录替换为主机上的目录。例如,在主机上创建一个名为 /var/config 的目录。

mkdir /var/config

然后在 /var/config 目录中创建 mysqld.cnf 文件,并将任何 mysql 配置添加到 mysqld.cnf 文件中。

touch /var/config/mysqld.cnf

现在我们可以将 /var/config 目录挂载为 mysql 容器内的 /etc/mysql/mysql.conf.d 。

docker run -d --name mysql_server -v /var/config:/etc/mysql/mysql.conf.d -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

根据上面的示例,我们使用-v标志将 /etc/mysql/mysql.conf.d 目录替换为本地 /var/config 目录。

使用自定义数据目录

同样,我们也可以从主机挂载 MySQL 数据目录。docker mysql 上的默认数据目录是“/var/lib/mysql”。

首先在主机上创建数据目录:

mkdir -p /mysql/data

然后使用本地数据目录启动服务器实例:

docker run -d --name mysql_server -v /mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

从主机备份数据库

使用 docker exec 命令,我们可以从主机创建数据库转储。

以下命令将从“mysql_server”实例转储“example_db”数据库。

docker exec mysql_server sh -c 'exec mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" example_db' > example_db.sql

以下命令将从 mysql 容器转储所有数据库。

docker exec mysql_server sh -c 'exec mysqldump --all-databases -u root -p"$MYSQL_ROOT_PASSWORD"' > all_databases.sql

Google搜索技巧

1. 使用引号精确搜索(“”)

当我们搜索特定内容时,可以是用双引号包裹索引内容,以减少Google搜搜的猜测。这时,Google就会完全按照我们给出的搜索内容进行检索。检索结果中只显示包含确切内容的结果。

比如,搜索learn JavaScript array,那么搜索引擎就会按照任意顺序去搜索包含这三个词的内容: image.png 而如果使用引号包裹内容,搜索的就是精准内容: image.png

2. 使用连字符排除内容(-)

有时候,我们搜索的内容可能有多层含义,或者不想看到和某个信息相关的搜索,就可以使用连字符进行排除。

比如,搜索JavaScript,排除维基百科中的相关内容: image.png

3. 使用星号填充内容(*)

可以使用星号来填充文本之间的空格,比如在搜索时,只记得其中一部分内容,另一部分忘记了,就可以这样用。

例如,我们忘记了Git中撤销commit命令的第二个单词,可以这样搜索: image.png 这里就是用星号通配符来代替不确定内容,以让谷歌完成搜索工作。

4. 搜索指定范围(...)

可以使用...来搜索指定数字范围的内容,这些数字可以是年份、版本等。

比如,搜索:JavaScript in 2021….2022image.png

5. 搜索指定站点内容(search:)

可以使用 **search:网站域名 搜索内容 **的形式来在指定网站内搜索内容。

比如,在W3C中搜索:search: w3schools.com javascript Array image.png

6. 搜索指定文件类型内容(filetype)

可以使用filetype来获取确定文件类型的搜索结果。可以使用这种方式来搜索电子书、文档等类型的内容。

比如,搜索:React ebook filetype:pdf image.png 可以看到,所有搜到的内容都是包含PDF的网页。

7. 查找多个关键字(AND)

如果我们查找的内容包含多个关键字,那么AND就派上用场了。

比如,搜索:React AND CSS image.png

8. 查找其中一个关键字(OR)

可以使用OR来查询多个关键词中的一个。

比如,搜索:React OR Vue image.png

9. 查询关键字的定义(Define)

可以使用Define关键词来查搜索关键词的定义。

比如,搜索:Define: JavaScript image.png

10. 搜索相关内容(+)

如果我们想要搜索多个相关的关键词时,就可以使用加号来连接多个相关的关键词。

比如,搜索:CSS style+React image.png

11. 查找相似网站(related)

可以使用related关键字来查询与指定站点相似的站点。

比如,搜索:related: w3schools.comimage.png

12. 搜索指定站点内容(site)

可以使用site关键字来搜索指定站点中的内容。

比如,搜索:site: github.com React image.png

13. 搜索指定时间范围(before、after)

可以使用after来搜索指定时间之后的内容,使用before搜索指定时间之前的内容。

比如,搜索:after: 2021 learn react image.png

webpack持久化缓存

出于构建安全考虑,默认情况下 webpack 不会启用持久化缓存。

一个典型的持久化缓存配置:

cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] // 当你 CLI 自动添加它时,你可以忽略它
    }
}

为了处理构建过程中的依赖关系,webpack 提供了三个新工具:

构建依赖(Build dependencies)

此为全新的配置项 cache.buildDependencies,它可以指定构建过程中的代码依赖。为了使它更简易,webpack 负责解析并遵循配置值的依赖。

值类型有两种:文件和目录。目录类型必须以斜杠(/)结尾。其他所有内容都解析为文件类型。

对于目录类型来说,会解析其最近的 package.json 中的 dependencies。对于文件类型来说,我们将查看 node.js 模块缓存以寻找其依赖。

示例:构建通常取决于 webpack 本身的 lib 文件夹:你可以这样配置:

cache.buildDependencies: {
    defaultWebpack: ["webpack/lib/"]
}

webpack/lib 或 webpack 依赖的库(如,watchpackenhanced-resolved 等)发生任何变化时,其缓存将失效。webpack/lib 已是默认值,默认情况下无需配置。

另一个示例:构建依旧取决于你的配置文件。具体配置如下:

cache.buildDependencies: {
    config: [__filename]
}

__filename 变量指向 node.js 中的当前文件。

当配置文件或配置文件中通过 require 依赖的任何内容发生更改时,也会使得持久化缓存失效。当配置文件通过 require() 引用了所有使用过的插件时,它们也会成为构建依赖项。

如果配置文件通过 fs.readFile 读取文件,则将不会成为构建依赖项,因为 webpack 仅遵循 require()。你需要手动将此类文件添加到 buildDependencies 中。

缓存版本(Version)

构建的某些依赖项不能单纯的依靠对文件的引用,如,从数据库读取的值,环境变量或命令行上传递的值。对于这些值,我们给出了新的配置项 cache.version, 类型为 string。

cache: {
    version: `${process.env.GIT_REV}`
}

缓存名(Name)

在某些情况下,依赖关系会在多个不同的值间切换,并且对于每个值更改都会使得持久化缓存失效,这显然是浪费资源的。对于这类值,我们给出了新的配置项 cache.name

cache.name 类型为 string。传递值将创建一个隔离且独立的持久化缓存。

cache.name 被用于对文件名进行持久化缓存。确保仅传递短小且 fs-safe 的名称。

示例:你的配置可以使用 --env.target mobile|desktop 参数为移动端或 PC 用户创建不同的构建。具体配置如下:

cache: {
    name: `${env.target}`
}

managedPaths

webpack 默认会忽略对 node_modules 的缓存分析,以避免过大的不必要的性能损耗。可以通过配置 cache.managedPaths: [] 禁用此行为,或者加入其他需要忽略的目录。

Watching

watch 状态并设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。写入缓存将同时写入内存缓存和文件系统缓存。

文件系统缓存会等到编译过程完成且编译器处于空闲状态才进行缓存,以避免额外延迟编译过程。

cache.idleTimeoutcache.idleTimeoutForInitialStore,它们控制着持久化缓存之前编译器必须空闲的时长。cache.idleTimeout 默认为 60s,cache.idleTimeoutForInitialStore 默认为 0s。由于序列化阻止了事件循环,因此在序列化缓存时不进行缓存检测。此延迟尝试避免由于快速编辑文件,而在 watch 模式下导致重新编译造成的延迟,同时尝试为下一次冷启动保持持久化缓存的最新状态。这是一个折中的解决方案,可以设置适合你工作流的值。较小的值会缩短冷启动时间,但会增加延迟重新构建的风险。

错误处理

发生错误要恢复持久化缓存的方式,可以通过删除整个缓存并进行全新的构建,或者通过删除有问题的缓存 entry 并使得该项目保持未缓存状态来进行。

# 删除缓存
rm -rf ./node_modules/.cache/webpack

也可以开启 infrastructureLogging 来进行 debug

module.exports = {
  infrastructureLogging: {
    debug: /webpack\.cache/
  },
};

Webpack 缓存的内部工作流

  • webpack 读取缓存文件。

    • 没有缓存文件 -> 未构建缓存
    • 缓存文件中的 versioncache.version 不匹配 -> 没有构建缓存
  • webpack 将解析快照(resolve snapshot)与文件系统进行对比

    • 匹配到 -> 继续后续流程

    • 没有匹配到:

      • 再次解析所有解析结果(resolve results

        • 没有匹配到 -> 未构建缓存
        • 匹配到 -> 继续后续流程
  • webpack 将构建依赖快照(build dependencies snapshot)与文件系统进行对比

    • 没有匹配到 -> 未构建缓存
    • 匹配到 -> 继续后续流程
  • 对缓存 entry 进行反序列化(在构建过程中对较大的缓存 entry 进行延迟反序列化)

  • 构建运行(有缓存或没有缓存)

    • 追踪构建依赖关系

      • 追踪 cache.buildDependencies
      • 追踪已使用的 loader
  • 新的构建依赖关系已解析完成

    • 解析依赖关系已追踪
    • 解析结果已追踪
  • 创建来自所有新解析依赖项的快照

  • 创建来自所有新构建依赖项的快照

  • 持久化缓存文件序列化到磁盘

序列化

所有支持序列化的 class 都需要注册一个序列化器,基本数据类型和引用数据类型(string,number,Array,Set,Map,RegExp,plain objects,Error)的序列化器都已被注册,如果自定义的模块(module)需要序列化,就需要对改模块注册序列化器:

webpack.util.serialization.register(Constructor, request, name, serializer);

Constructor 应为一个模块(module) class 或构造器函数,配合模块序列化器,用于模块序列化。

request 相当于注册的模块序列化器的 id ,webpack 内部通过 require(request) 加载对应的模块序列化器。

name 被用于区分具有相同 request 的多个模块序列化器调用。

serializer 是至少拥有 serializedeserialize 两个方法的对象。

当需序列化对象时,请调用 serializer.serialize(object, context)context 是至少拥有一个 write(anything) 方法的对象 此方法将内容写入输出流。传递的值也会被序列化。

当需要反序列化对象时,请调用 serializer.deserialize(context)context 是至少拥有一个 read(): anything 方法的对象。此方法会反序列化输入流中的某些内容。deserialize 必须返回反序列化后的对象。

serializedeserialize 应以相同的顺序读取和写入相同的对象。

示例:

// some-module/lib/MyClass.js
class MyClass {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        this.c = undefined;
    }
}

register(MyClass, "some-module/lib/MyClass", null, {
    seralize(obj, { write }) {
        write(obj.a);
        write(obj.b);
        write(obj.c);
    }
    deserialize({ read }) {
        const obj = new MyClass(read(), read());
        obj.c = read();
        return obj;
    }
});

使用Node.js、Express、Sequelize和MySQL来搭建的Rest-API服务

在本文中,我们将使用 Express、Sequelize 和 MySQL 来构建 Node.js Restful CRUD API 服务。该 API 服务可以作为 Tutorial 网站的后端服务。

首先在需要在本机上安装Node.js 和 MySQL,启动 mysql,并创建 testdb

$ CREATE DATABASE testdb
$ SHOW DATABASES
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| testdb             |
+--------------------+
5 rows in set (0.00 sec)

接下来开发基于 Express 的 Web 服务器,包括以下步骤:

  1. 添加 MySQL 数据库的配置,
  2. 使用 Sequelize 创建 Tutorial 模型,
  3. 编写 Controller
  4. 定义处理所有 CRUD 操作(包括自定义查找器)的路由。

下面是本 Rest API 服务的接口列表:

方法 Urls Actions
GET api/tutorials 获取所有教程
GET api/tutorials/:id 获取指定 id 的教程
POST api/tutorials 添加新教程
PUT api/tutorials/:id 更新指定 id 的教程
DELETE api/tutorials/:id 删除指定 id 的教程
DELETE api/tutorials 删除所有教程
GET api/tutorials/published 查找所有已发表的教程
GET api/tutorials?title=[kw] 查找所有标题包含 'kw' 的教程

源码的项目结构:

├── README.md
├── app
│   ├── config
│   │   └── db.config.js
│   ├── controllers
│   │   └── tutorial.controller.js
│   ├── models
│   │   ├── index.js
│   │   └── tutorial.model.js
│   └── routes
│       └── turorial.routes.js
├── node_modules
├── package.json
├── pnpm-lock.yaml
└── server.js

创建 Node.js 应用程序

首先,我们创建一个文件夹:

$ mkdir nodejs-express-sequelize-mysql
$ cd nodejs-express-sequelize-mysql
$ npm init -y
# 安装必要的模块:`express`、`sequelize`、`cors`和`mysql2`
$ npm install express sequelize mysql2 cors --save

package.json文件应如下所示:

{
  "name": "nodejs-express-sequelize-mysql",
  "version": "1.0.0",
  "description": "Node.js Rest Apis with Express, Sequelize & MySQL",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "nodejs",
    "express",
    "rest",
    "api",
    "sequelize",
    "mysql"
  ],
  "author": "JayFate",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "mysql2": "^2.3.3",
    "sequelize": "^6.32.0"
  }
}

开发 Express Web 服务器

在根目录中,创建 server.js

const express = require("express");
const cors = require("cors");

const app = express();

var corsOptions = {
  origin: "http://localhost:8081"
};

app.use(cors(corsOptions));

// parse requests of content-type - application/json
app.use(express.json());

// parse requests of content-type - application/x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }));

// simple route
app.get("/", (req, res) => {
  res.json({ message: "Welcome to bezkoder application." });
});

// set port, listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

我们可以运行 node server.js 启动 server,访问 http://localhost:8080/,输出如下图

节点 js-express-sequelize-mysql-示例-设置-服务器

配置MySQL和Sequelize

创建 app/config/db.config.js 编写 mysql 配置

// app/config/db.config.js
module.exports = {
  HOST: "127.0.0.1",
  USER: "root",
  PASSWORD: "123456Ab",
  DB: "testdb",
  dialect: "mysql",
  pool: {
    max: 5, // 最大连接数
    min: 0, // 最小连接数
    acquire: 30000,
    idle: 10000 // 连接在释放之前的最长空闲时间(毫秒)
  }
};

初始化 Sequelize

创建 app/models/index.js 并初始化 Sequelize

const dbConfig = require("../config/db.config.js");

const Sequelize = require("sequelize");
const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, {
  host: dbConfig.HOST,
  dialect: dbConfig.dialect,
  operatorsAliases: false,

  pool: {
    max: dbConfig.pool.max,
    min: dbConfig.pool.min,
    acquire: dbConfig.pool.acquire,
    idle: dbConfig.pool.idle
  }
});

const db = {};

db.Sequelize = Sequelize;
db.sequelize = sequelize;

db.tutorials = require("./tutorial.model.js")(sequelize, Sequelize);

module.exports = db;

然后在 server.js 中调用 db.sequelize.sync() 同步数据库

// ...
const app = express();
app.use(...);

const db = require("./app/models");
// db.sequelize.sync({ force: true })
db.sequelize.sync()
  .then(() => {
    console.log("Synced db.");
  })
  .catch((err) => {
    console.log("Failed to sync db: " + err.message);
  });

// ...

定义 Sequelize 模型

创建 app/models/tutorial.model.js 用于定义 Tutorial 模型

module.exports = (sequelize, Sequelize) => {
  const Tutorial = sequelize.define("tutorial", {
    title: {
      type: Sequelize.STRING
    },
    description: {
      type: Sequelize.STRING
    },
    published: {
      type: Sequelize.BOOLEAN
    }
  });

  return Tutorial;
};

这个 Tutorial 模型代表MySQL 数据库中的。sequelize 将自动生成教程表idtitledescriptionpublishedcreatedAtupdatedAt 列。

初始化 Sequelize 后,可以使用 Sequelize 的 API 代替编写CRUD函数

  • 创建一个新教程:create(object)
  • 通过 id 查找教程:findByPk(id)
  • 获取所有教程:findAll()
  • 按 id 更新教程:update(data, where: { id: id })
  • 删除教程:destroy(where: { id: id })
  • 删除所有教程:destroy(where: {})
  • 按标题查找所有教程:findAll({ where: { title: ... } })

定义Controller

创建 app/controllers/tutorial.controller.js

const db = require("../models");
const Tutorial = db.tutorials;
const Op = db.Sequelize.Op;

// Create and Save a new Tutorial
exports.create = (req, res) => {
  
};

// Retrieve all Tutorials from the database.
exports.findAll = (req, res) => {
  
};

// Find a single Tutorial with an id
exports.findOne = (req, res) => {
  
};

// Update a Tutorial by the id in the request
exports.update = (req, res) => {
  
};

// Delete a Tutorial with the specified id in the request
exports.delete = (req, res) => {
  
};

// Delete all Tutorials from the database.
exports.deleteAll = (req, res) => {
  
};

// Find all published Tutorials
exports.findAllPublished = (req, res) => {
  
};

我们来分别实现这些功能

创建一个新对象

创建并保存新教程:

exports.create = (req, res) => {
  // Validate request
  if (!req.body.title) {
    res.status(400).send({
      message: "Content can not be empty!"
    });
    return;
  }

  // Create a Tutorial
  const tutorial = {
    title: req.body.title,
    description: req.body.description,
    published: req.body.published ? req.body.published : false
  };

  // Save Tutorial in the database
  Tutorial.create(tutorial)
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while creating the Tutorial."
      });
    });
};

检索对象(有条件)

从数据库中检索所有教程/按标题查找:

exports.findAll = (req, res) => {
  const title = req.query.title;
  var condition = title ? { title: { [Op.like]: `%${title}%` } } : null;

  Tutorial.findAll({ where: condition })
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving tutorials."
      });
    });
};

我们用来req.query.title从请求中获取查询字符串并将其视为findAll()方法的条件。

检索单个对象

查找带有以下内容的单个教程id

exports.findOne = (req, res) => {
  const id = req.params.id;

  Tutorial.findByPk(id)
    .then(data => {
      if (data) {
        res.send(data);
      } else {
        res.status(404).send({
          message: `Cannot find Tutorial with id=${id}.`
        });
      }
    })
    .catch(err => {
      res.status(500).send({
        message: "Error retrieving Tutorial with id=" + id
      });
    });
};

更新对象

id更新请求中由 标识的教程:

exports.update = (req, res) => {
  const id = req.params.id;

  Tutorial.update(req.body, {
    where: { id: id }
  })
    .then(num => {
      if (num == 1) {
        res.send({
          message: "Tutorial was updated successfully."
        });
      } else {
        res.send({
          message: `Cannot update Tutorial with id=${id}. Maybe Tutorial was not found or req.body is empty!`
        });
      }
    })
    .catch(err => {
      res.status(500).send({
        message: "Error updating Tutorial with id=" + id
      });
    });
};

删除对象

删除指定的教程id

exports.delete = (req, res) => {
  const id = req.params.id;

  Tutorial.destroy({
    where: { id: id }
  })
    .then(num => {
      if (num == 1) {
        res.send({
          message: "Tutorial was deleted successfully!"
        });
      } else {
        res.send({
          message: `Cannot delete Tutorial with id=${id}. Maybe Tutorial was not found!`
        });
      }
    })
    .catch(err => {
      res.status(500).send({
        message: "Could not delete Tutorial with id=" + id
      });
    });
};

删除所有对象

从数据库中删除所有教程:

exports.deleteAll = (req, res) => {
  Tutorial.destroy({
    where: {},
    truncate: false
  })
    .then(nums => {
      res.send({ message: `${nums} Tutorials were deleted successfully!` });
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while removing all tutorials."
      });
    });
};

按条件查找所有对象

查找所有教程published = true

exports.findAllPublished = (req, res) => {
  Tutorial.findAll({ where: { published: true } })
    .then(data => {
      res.send(data);
    })
    .catch(err => {
      res.status(500).send({
        message:
          err.message || "Some error occurred while retrieving tutorials."
      });
    });
};

可以稍微修改此控制器以返回分页响应:

{
    "totalItems": 8,
    "tutorials": [...],
    "totalPages": 3,
    "currentPage": 1
}

定义路由

当客户端使用 HTTP 请求(GET、POST、PUT、DELETE)向端点发送请求时,我们需要通过设置路由来确定服务器将如何响应。

这些是我们的路线:

  • /api/tutorials:获取、发布、删除
  • /api/tutorials/:id:获取、放置、删除
  • /api/tutorials/published: 得到

app/routes文件夹中创建一个turorial.routes.js,内容如下:

module.exports = app => {
  const tutorials = require("../controllers/tutorial.controller.js");

  var router = require("express").Router();

  // Create a new Tutorial
  router.post("/", tutorials.create);

  // Retrieve all Tutorials
  router.get("/", tutorials.findAll);

  // Retrieve all published Tutorials
  router.get("/published", tutorials.findAllPublished);

  // Retrieve a single Tutorial with id
  router.get("/:id", tutorials.findOne);

  // Update a Tutorial with id
  router.put("/:id", tutorials.update);

  // Delete a Tutorial with id
  router.delete("/:id", tutorials.delete);

  // Delete all Tutorials
  router.delete("/", tutorials.deleteAll);

  app.use('/api/tutorials', router);
};

server.js中 将路由挂载到 app 上

...

// 将路由挂载到 app 上
require("./app/routes/turorial.routes")(app);

// set port, listen for requests
const PORT = ...;
app.listen(...);

测试 API

首先使用 navicat 给 testdb 中的 tutorials 表输入一些测试数据:

image

然后运行我们的 Node.js 应用程序 node server.js

$ node server.js
Server is running on port 8080.
Executing (default): DROP TABLE IF EXISTS `tutorials`;
Executing (default): CREATE TABLE IF NOT EXISTS `tutorials` (`id` INTEGER NOT NULL auto_increment , `title` VARCHAR(255), `description` VARCHAR(255), `published` TINYINT(1), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `tutorials`
Drop and re-sync db.

访问 http://localhost:8080/api/tutorials 输出如下图:

image
  1. POST /tutorials使用Api创建新教程

  2. 节点js-express-sequelize-mysql-example-demo-create-object

  3. 创建一些新的教程后,您可以检查 MySQL 表:

  4. mysql> select * from tutorials;
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    | id | title             | description       | published | createdAt           | updatedAt           |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    |  1 | JS: Node Tut #1   | Tut#1 Description |         0 | 2019-12-13 01:13:57 | 2019-12-13 01:13:57 |
    |  2 | JS: Node Tut #2   | Tut#2 Description |         0 | 2019-12-13 01:16:08 | 2019-12-13 01:16:08 |
    |  3 | JS: Vue Tut #3    | Tut#3 Description |         0 | 2019-12-13 01:16:24 | 2019-12-13 01:16:24 |
    |  4 | Vue Tut #4        | Tut#4 Description |         0 | 2019-12-13 01:16:48 | 2019-12-13 01:16:48 |
    |  5 | Node & Vue Tut #5 | Tut#5 Description |         0 | 2019-12-13 01:16:58 | 2019-12-13 01:16:58 |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
  5. GET /tutorials使用Api检索所有教程

  6. 节点 js-express-sequelize-mysql-示例-演示-检索-对象

  7. GET /tutorials/:id使用Api按 id 检索单个教程

  8. 节点js-express-sequelize-mysql-example-demo-get-single-object

  9. PUT /tutorials/:id使用Api更新教程

  10. 节点js-express-sequelize-mysql-示例-演示-更新-对象

  11. tutorials更新某些行后检查表:

  12. mysql> select * from tutorials;
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    | id | title             | description       | published | createdAt           | updatedAt           |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    |  1 | JS: Node Tut #1   | Tut#1 Description |         0 | 2019-12-13 01:13:57 | 2019-12-13 01:13:57 |
    |  2 | JS: Node Tut #2   | Tut#2 Description |         0 | 2019-12-13 01:16:08 | 2019-12-13 01:16:08 |
    |  3 | JS: Vue Tut #3    | Tut#3 Description |         1 | 2019-12-13 01:16:24 | 2019-12-13 01:22:51 |
    |  4 | Vue Tut #4        | Tut#4 Description |         1 | 2019-12-13 01:16:48 | 2019-12-13 01:25:28 |
    |  5 | Node & Vue Tut #5 | Tut#5 Description |         1 | 2019-12-13 01:16:58 | 2019-12-13 01:25:30 |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
  13. 查找标题包含“node”的所有教程:GET /tutorials?title=node

  14. node-js-express-sequelize-mysql-example-demo-find-objects

  15. GET /tutorials/published查找所有已发布的使用Api 的教程

  16. 节点 js-express-sequelize-mysql-示例-演示-查找活动对象

  17. DELETE /tutorials/:id使用Api删除教程

  18. node-js-express-sequelize-mysql-示例-演示-删除-对象

  19. id=2 的教程已从tutorials表中删除:

  20. mysql> select * from tutorials;
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    | id | title             | description       | published | createdAt           | updatedAt           |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
    |  1 | JS: Node Tut #1   | Tut#1 Description |         0 | 2019-12-13 01:13:57 | 2019-12-13 01:13:57 |
    |  3 | JS: Vue Tut #3    | Tut#3 Description |         1 | 2019-12-13 01:16:24 | 2019-12-13 01:22:51 |
    |  4 | Vue Tut #4        | Tut#4 Description |         1 | 2019-12-13 01:16:48 | 2019-12-13 01:25:28 |
    |  5 | Node & Vue Tut #5 | Tut#5 Description |         1 | 2019-12-13 01:16:58 | 2019-12-13 01:25:30 |
    +----+-------------------+-------------------+-----------+---------------------+---------------------+
  21. 删除所有使用DELETE /tutorialsApi 的教程

  22. 节点js-express-sequelize-mysql-示例-演示-删除所有对象

  23. 现在表中没有行tutorials

  24. mysql> SELECT * FROM tutorials;
    Empty set (0.00 sec)
    

您可以使用Axios 的简单 HTTP 客户端来检查它。

axios-请求-示例-获取-后-放置-删除

或者:使用 Fetch API 的简单 HTTP 客户端

docker删除所有容器和镜像

In Docker, if we have exited a container without stopping it, we then need to stop them manually as it has not stopped on exit. Similarly, for images, we need to delete them from top to bottom as some containers or images might be dependent on the base images, we can any time download the base image at. So it is a good idea to delete unwanted or dangling images from the current machine.

How To Delete The Images in Docker?

Remove Image

To delete the image by the ImageId/Name we can use the following command. To know more about how to build a docker image with the help of Dockerfile refer to Concept of Dockerfile.

docker rmi <imageId/Name>

Force Remove Image

To force remove the docker Images by the ImageID/Name we can use the following command.

docker rmi -f <imageId/Name>

Note: We can’t remove the images by force or normally while the container is running.

Dangling Images

Dangling Images are those that don’t map to either the repository or the tag. The command used is to remove the dangling images. To know more about how to tag Docker images by referring to Docker image tags.

docker image prune

Removing all Images

We can remove all images in the docker-machine to remove unwanted clutter and space in the system. We can anyways fetch the latest version or specific versioned image from the docker registry or from the cache.

docker rmi $(docker images -q)

Remove all the images.

How To Delete Containers In Docker

Before deleting the containers we need to stop the container first for that we use the command.

docker stop <containerId/Name>

The Difference Between Docker Stop & Docker Kill

Docker stop will first send a SIGTERM signal before killing the process with a SIGKILL signal and a grace period. When Docker kill sends SIGKILL, it immediately terminates the process.

  • Stop all running Containers: In order to stop the containers which have not exited. This might happen when the command used in the Docker image is left running. The command should be exited and this will in turn stop the container. To stop the container when you have not exited the container by stopping the command, you need to run the following command.
docker stop $(docker ps -aq)
  • Delete The Container: If the container is stopped then we can use the following command to delete the container.
docker rm  <containerId/Name>
  • Force Delete The Container: We can force remove the containers while they are running without stopping them by using the below command.
docker rm -f <containerId/Name>

Stop the Containers

Remove all Containers

To remove all containers from the docker machine, we need to get the ids of all the containers. We can simply get the ids of the containers with the command docker ps -aq, then by using the docker rm command, we can remove all the containers in the docker-machine.

docker rm $(docker ps -aq)

Remove all the containers

Remove all Stopped Containers

To remove all containers which are stopped/exited, we can use filters in the ps command argument. We can’t directly remove a container if it is not stopped. We can stop containers that are not exited or are running by using the -f argument to the ps command in docker, the -f or –filter option takes in a filter like status=exited or status=running or name and so on. We can filter out to stop the specific containers according to the requirement.

docker rm $(docker ps -aq --filter  status="exited")

After filtering out the container which is running, we can use the stop command to stop those containers with the -q to silence the numeric ids associated with those containers.

docker stop $(docker ps --filter status=running -q)

This will stop all the containers and thus we can now remove the containers from the docker-machine. We can even filter the containers which are stopped here to remove only those whose status is exited.

docker rm $(docker ps --filter status=exited -q)

delete the stopped containers.

  • The below command removed all the containers which are in the existing state. That means the containers stopped.
docker container prune

Docker container Prune

Mac 安装及配置 oh-my-zsh

Mac 安装及配置 oh-my-zsh

zsh是一个Linux下强大的shell,zsh是bash的增强版,其实zsh和bash是两个不同的概念,zsh更加强大。通常zsh配置起来非常麻烦,且相当的复杂,所以oh-my-zsh是为了简化zsh的配置而开发的,因此oh-my-zsh算是zsh的配置。

1. 开始安装

# 安装zsh
brew install zsh
# 将 zsh 设为默认shell
chsh -s /bin/zsh
# 安装 oh-my-zsh,选择以下 3 种方式之一即可
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
# 或者 sh -c "$(wget -qO- https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
# 或者 sh -c "$(fetch -o - https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
source ~/.zshrc

2. 安装插件

1. git

默认开启的插件,提供了大量 git 的alias.

vim .zshrc

plugins=(git)

# 配置 alias

# macOS aliasses
if [[ $OSTYPE == darwin* ]]; then
alias flush='dscacheutil -flushcache'
# Apps
alias browse="open -a /Applications/Google\ Chrome.app"
# * Browse Azure Portal
alias azure="browse https://preview.portal.azure.com"
fi

alias t='tig'
alias ll='ls -al'
alias la='ls -a'
alias apt-get='sudo apt-get'
alias tsn='ts-node'


# git
alias ga='git add'
alias gb='git branch'
alias gc='git commit'
alias gco='git checkout'
alias gcob='git checkout -b'
alias gf='git fetch'
alias grb='git rebase'
alias grs='git reset'

# npm
alias ninrb='sudo npm install && npm run bootstrap'
alias ninrbx='sudo npm install && npx lerna bootstrap && npx lerna link'


# quickapp
alias cdqa='cd /Users/11104760/.quick-app-ide/extensions/'
alias jb='joy build'
alias js='joy serve'

# mac
alias pwdp='pwd|pbcopy'
alias yc='yarn && code .'

bindkey "^X\x7f" backward-kill-line

2. 安装 autojump

brew install autojump

vim .zshrc
  1. 找到 plugins=,在后面添加autojump:plugins=(git autojump)
  2. 新开一行,添加:[[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] && . $(brew --prefix)/etc/profile.d/autojump.sh
  3. c. :wq保存退出,重启终端。
source ./.zshrc
# 之后可以通过 j  path...  快速跳转路径

3. 安装 zsh-syntax-highlighting

git clone --depth=1 https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

然后激活这个插件,修改 ~/.zshrc

中加入插件的名字plugins=( [plugins...] zsh-syntax-highlighting)

# ...
plugins=( [plugins...] zsh-syntax-highlighting)

source ~/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

最后

source ~/.zshrc

4. 安装zsh-autosuggestions语法历史记录插件

git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

vim ~/.zshrc

plugins=(zsh-autosuggestions)

# 最后一行:
source $ZSH_CUSTOM/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zshrc

3. (.zshrc)源文件内容

# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:/usr/local/bin:$PATH

# Path to your oh-my-zsh installation.
export ZSH=$HOME/.oh-my-zsh

# Set name of the theme to load --- if set to "random", it will
# load a random theme each time oh-my-zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME="robbyrussell"

# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )

# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"

# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"

# Uncomment one of the following lines to change the auto-update behavior
# zstyle ':omz:update' mode disabled  # disable automatic updates
# zstyle ':omz:update' mode auto      # update automatically without asking
# zstyle ':omz:update' mode reminder  # just remind me to update when it's time

# Uncomment the following line to change how often to auto-update (in days).
# zstyle ':omz:update' frequency 13

# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true"

# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"

# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"

# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"

# Uncomment the following line to display red dots whilst waiting for completion.
# You can also set it to another string to have that shown instead of the default red dots.
# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
# COMPLETION_WAITING_DOTS="true"

# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"

# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"

# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder

# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(
  git
  autojump
  zsh-syntax-highlighting
  )

source $ZSH/oh-my-zsh.sh

[[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] && . $(brew --prefix)/etc/profile.d/autojump.sh

# User configuration

# export MANPATH="/usr/local/man:$MANPATH"

# You may need to manually set your language environment
# export LANG=en_US.UTF-8

# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
#   export EDITOR='vim'
# else
#   export EDITOR='mvim'
# fi

# Compilation flags
# export ARCHFLAGS="-arch x86_64"

# Set personal aliases, overriding those provided by oh-my-zsh libs,
# plugins, and themes. Aliases can be placed here, though oh-my-zsh
# users are encouraged to define aliases within the ZSH_CUSTOM folder.
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig="mate ~/.zshrc"
# alias ohmyzsh="mate ~/.oh-my-zsh"

DISABLE_AUTO_UPDATE="true"

# 配置 alias

# macOS aliasses
if [[ $OSTYPE == darwin* ]]; then
alias flush='dscacheutil -flushcache'
# Apps
alias browse="open -a /Applications/Google\ Chrome.app"
# * Browse Azure Portal
alias azure="browse https://preview.portal.azure.com"
fi

alias t='tig'
alias ll='ls -al'
alias la='ls -a'
alias apt-get='sudo apt-get'
alias tsn='ts-node'


# git
alias ga='git add'
alias gb='git branch'
alias gc='git commit'
alias gco='git checkout'
alias gcob='git checkout -b'
alias gf='git fetch'
alias grb='git rebase'
alias grs='git reset'

# npm
alias ninrb='sudo npm install && npm run bootstrap'
alias ninrbx='sudo npm install && npx lerna bootstrap && npx lerna link'


# quickapp
alias cdqa='cd /Users/11104760/.quick-app-ide/extensions/'
alias jb='joy build'
alias js='joy serve'

# mac
alias pwdp='pwd|pbcopy'
alias yc='yarn && code .'

bindkey "^X\x7f" backward-kill-line

source ~/.bash_profile


source /Users/pjpj/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

source $ZSH_CUSTOM/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh

.bash_profile

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

Dockerfile多From用法

先说老版本的Docker中为什么不支持多个FROM指令,在17.05版本之前的Docker,只允许Dockerfile中出现一个FROM指令,这得从镜像的本质说起。

你可以简单理解Docker的镜像是一个压缩文件,其中包含了你需要的程序和一个文件系统。其实这样说是不严谨的,Docker镜像并非只是一个文件,而是由一堆文件组成,最主要的文件是

Dockerfile 中,大多数指令会生成一个层,比如下面的两个例子:

示例一,foo镜像的Dockerfile

# 基础镜像中已经存在若干个层了
FROM ubuntu:21.04

# RUN指令会增加一层,在这一层中,安装了 git 软件
RUN apt-get update \
  && apt-get install -y --no-install-recommends git \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

示例二,bar镜像的Dockerfile

FROM foo

# RUN指令会增加一层,在这一层中,安装了 nginx
RUN apt-get update \
  && apt-get install -y --no-install-recommends nginx \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

假设基础镜像 ubuntu:21.04 已经存在5层,使用第一个Dockerfile打包成镜像 foo,则foo有6层,又使用第二个Dockerfile打包成镜像bar,则bar中有7层。

如果 ubuntu:21.04 等其他镜像不算,如果系统中只存在 foo 和 bar 两个镜像,那么系统中一共保存了多少层呢?

是7层,并非13层,这是因为,foo和bar共享了6层。层的共享机制可以节约大量的磁盘空间和传输带宽,比如你本地已经有了foo镜像,又从镜像仓库中拉取bar镜像时,只拉取本地所没有的最后一层就可以了,不需要把整个bar镜像连根拉一遍。

Docker镜像的每一层只记录文件变更,在容器启动时,Docker会将镜像的各个层进行计算,最后生成一个文件系统,这个被称为联合挂载。对此感兴趣的话可以进入了解一下AUFS

Docker的各个层是有相关性的,在联合挂载的过程中,系统需要知道在什么样的基础上再增加新的文件。那么这就要求一个Docker镜像只能有一个起始层,只能有一个根。所以,Dockerfile中,就只允许一个 FROM 指令。因为多个 FROM 指令会造成多根,则是无法实现的。

多个 FROM 指令

但为什么 Docker 17.05 版本之后允许 Dockerfile 支持多个 FROM 指令了呢,莫非已经支持了多根?

多个 FROM 指令并不是为了生成多根的层关系,最后生成的镜像,仍以最后一条 FROM 为准,之前的 FROM 会被抛弃,那么之前的FROM 又有什么意义呢?

每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但是,能够将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义。

最大的使用场景是将编译环境和运行环境分离,比如,之前我们需要构建一个Go语言程序,那么就需要用到go命令等编译环境,我们的Dockerfile可能是这样的:

# Go语言环境基础镜像
FROM golang:1.10.3

# 将源码拷贝到镜像中
COPY server.go /build/

# 指定工作目录
WORKDIR /build

# 编译镜像时,运行 go build 编译生成 server 程序
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 指定容器运行时入口程序 server
ENTRYPOINT ["/build/server"]

基础镜像 golang:1.10.3 是非常庞大的,因为其中包含了所有的Go语言编译工具和库,而运行时候我们仅仅需要编译后的 server 程序就行了,不需要编译时的编译工具,最后生成的大体积镜像就是一种浪费。

将程序编译和镜像打包分开,选择增加构Go语言构建工具,然后在构建步骤中编译。

最后将编译接口拷贝到镜像中就行了,那么Dockerfile的基础镜像并不需要包含Go编译环境:

# 不需要Go语言编译环境
FROM scratch

# 将编译结果拷贝到容器中
COPY server /server

# 指定容器运行时入口程序 server
ENTRYPOINT ["/server"]

提示: scratch 是内置关键词,并不是一个真实存在的镜像。 FROM scratch 会使用一个完全干净的文件系统,不包含任何文件。 因为Go语言编译后不需要运行时,也就不需要安装任何的运行库。 FROM scratch 可以使得最后生成的镜像最小化,其中只包含了 server 程序。

在 Docker 17.05版本以后,就有了新的解决方案,直接一个Dockerfile就可以解决:

# 编译阶段
FROM golang:1.10.3

COPY server.go /build/

WORKDIR /build

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server

# 运行阶段
FROM scratch

# 从编译阶段的中拷贝编译结果到当前镜像中
COPY --from=0 /build/server /

ENTRYPOINT ["/server"]

这个 Dockerfile 的玄妙之处就在于 COPY 指令的 --from=0 参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0代表第一个阶段。除了使用数字,我们还可以给阶段命名,比如:

# 编译阶段 命名为 builder
FROM golang:1.10.3 as builder

# ... 省略

# 运行阶段
FROM scratch

# 从编译阶段的中拷贝编译结果到当前镜像中
COPY --from=builder /build/server /

更为强大的是,COPY --from不但可以从前置阶段中拷贝,还可以直接从一个已经存在的镜像中拷贝。比如,

FROM ubuntu:21.04

COPY --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/

我们直接将etcd镜像中的程序拷贝到了我们的镜像中,这样,在生成我们的程序镜像时,就不需要源码编译etcd了,直接将官方编译好的程序文件拿过来就行了。

有些程序要么没有apt源,要么apt源中的版本太老,要么干脆只提供源码需要自己编译,使用这些程序时,我们可以方便地使用已经存在的Docker镜像作为我们的基础镜像。但是我们的软件有时候可能需要依赖多个这种文件,我们并不能同时将 nginx 和 etcd 的镜像同时作为我们的基础镜像(不支持多根),这种情况下,使用 COPY --from 就非常方便实用了。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.