Docker 部署 React 全栈应用(一,二)

1,750 阅读15分钟

前言

才知道掘金文章还有字数限制,只能分开发。

之前使用 Vue 全家桶开发了个人博客,并部署在阿里云服务器上,最近在学习 React,于是使用 React 开发重构了自己的博客。

主要技术栈如下:

  • 前台页面:Next.js 搭建服务端渲染页面,利于 SEO
  • 后台管理界面:create-react-app 快速搭建
  • 服务接口:Egg.js + Mysql
  • 部署:Linux + Docker

React 搭建博客前后台部分,这里不会细讲,只会说说中间遇到的一些问题和一些解决方法,具体开发教程可参考 React Hooks+Egg.js实战视频教程-技术胖Blog开发

部署部分这里会是重点讲解,因为也是第一次接触 Docker,这里只记录自己的学习心得,有不对的地方还请多多指教。

恰好本次项目里前台页面是 node 运行,后台界面是静态 HTML,服务接口需要连接 Mysql,我觉得 Docker 来部署这几种情况也是比较全面的例子了,可以给后来同学作为参考,内容比较啰嗦,希望能帮助后来的同学少走一点坑,因为有些是自己的理解,可能会有错误,还请大家指正,互相学习。

项目地址

源码地址:github.com/Moon-Future…

clone 下来参照目录哦~

一、React 篇

博客前台使用 Next.js 服务端渲染框架搭建,后台管理界面使用 create-react-app 脚手架搭建,服务接口使用 Egg 框架(基于 Koa)。后台管理和服务接口没什么好说的,就是一些 React 基础知识,这里主要说下 Next.js 中遇到的一些问题。

项目目录:

blog:前台界面,Next.js

admin:后台管理界面,create-react-app 脚手架搭建

service:前后台服务接口

1. 获取渲染数据

因为是服务端渲染,所以页面初始数据会在服务器端获取后,渲染页面后返回给前端,这里有两个官方 API,getStaticPropsgetServerSideProps,从名字可以稍微看出一点区别。(Next.js 9.3 版本以上,使用 getStaticPropsgetServerSideProps 来替代 getInitialProps。)

getStaticProps:服务端获取静态数据,在获取数据后生成静态 HTML 页面,之后在每次请求时都重用此页面

const Article = (props) => {

    return ()
}
/* 也可
export default class Article extends React.Component {

    render() {
        return
    }
}
*/

export async function getStaticProps(context) {
    try {
        const result = await axios.post(api.getArticleList)
        return {
            props: { articleList: result.data }, // will be passed to the page component as props
        }
    } catch (e) {
        return {
            props: { articleList: [] }, // will be passed to the page component as props
        }
    }
}

export default Article

getServerSideProps:每次请求时,服务端都会去重新获取获取生成 HTML 页面

const Article = (props) => {

    return ()
}
/* 也可
export default class Article extends React.Component {

    render() {
        return
    }
}
*/

export async function getServerSideProps(context) {
    try {
        const result = await axios.post(api.getArticleList)
        return {
            props: { articleList: result.data }, // will be passed to the page component as props
        }
    } catch (e) {
        return {
            props: { articleList: [] }, // will be passed to the page component as props
        }
    }
}

export default Article

可以看到两者用法是一样的。

开发模式npm run dev,两者没什么区别,每次请求页面都会重新获取数据。

生产环境下,需要先npm run build 生成静态页面,使用 getStaticProps 获取数据的话就会在此命令下生产静态 HTML 页面,然后npm run start,后面每次请求都会重用静态页面,而使用 getServerSideProps 每次请求都会重新获取数据。

返回数据 都是对象形式,且只能是对象,key 是 props,会传递到类或函数里面的 props。

博客这里因为是获取博客文章列表,数据随时可能变化,所以选用 getServerSideProps

这里使用 try,catch 捕获异常,防止获取数据失败或者后端接口报错,服务端渲染错误返回不了页面。

2. 页面加载后请求

还有一些数据,我们并不希望在服务端获取渲染到页面里,而是希望页面加载后再操作。

使用 React Hook,可以在 useEffect 中操作:

const Article = (props) => {

    useEffect(async () => {
		await axios.get('')
    }, [])

    return ()
}

export async function getServerSideProps(context) {
    try {
        const result = await axios.post(api.getArticleList)
        return {
            props: { articleList: result.data }, // will be passed to the page component as props
        }
    } catch (e) {
        return {
            props: { articleList: [] }, // will be passed to the page component as props
        }
    }
}

export default Article

这里注意 useEffect 第二个参数,代表是否执行的依赖。

  • 不传第二个参数:每次 return 重新渲染页面时,useEffect 第一个参数函数都会执行
  • 传参 [],如上:代表不依赖任何变量,只执行一次
  • 传参 [value],数组,可以依赖多个变量:代表依赖 value 变量(state 中的值),只在 value 值改变时,执行 useEffect 第一个参数函数

使用 Class,可以在 componenDidMount 中操作:

export default class Article extends React.Component {

    componenDidMount() {
        await axios.get('')
    }
    
    render() {
        return
    }
}

export async function getServerSideProps(context) {
    try {
        const result = await axios.post(api.getArticleList)
        return {
            props: { articleList: result.data }, // will be passed to the page component as props
        }
    } catch (e) {
        return {
            props: { articleList: [] }, // will be passed to the page component as props
        }
    }
}

export default Article

3. 页面动画

页面进入、退出动画找到一个比较好用的库 framer-motionwww.framer.com/api/motion/

先改造一下 pages/_app.js,引入 framer-motion

npm install framer-motion -S
import { AnimatePresence } from 'framer-motion'

export default function MyApp({ Component, pageProps, router }) {
  return (
    <AnimatePresence exitBeforeEnter>
      <Component {...pageProps} route={router.route} key={router.route} />
    </AnimatePresence>
  )
}

在每个页面里通过在元素标签前加 motion 实现动画效果,如 pages/article.js 页面

const postVariants = {
  initial: { scale: 0.96, y: 30, opacity: 0 },
  enter: { scale: 1, y: 0, opacity: 1, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] } },
  exit: {
    scale: 0.6,
    y: 100,
    opacity: 0,
    transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] },
  },
}

const sentenceVariants = {
  initial: { scale: 0.96, opacity: 1 },
  exit: {
    scale: 0.6,
    y: 100,
    x: -300,
    opacity: 0,
    transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] },
  },
}

const Article = (props) => {
  const { articleList, route } = props
  const [poetry, setPoetry] = useState(null)

  const getPoetry = (data) => {
    setPoetry(data)
  }

  return (
    <div className="container article-container">
      <Head>
        <title>学无止境,厚积薄发</title>
      </Head>

      <Header route={route} />

      <div className="page-background"></div>
      <div style={{ height: '500px' }}></div>

      <Row className="comm-main comm-main-index" type="flex" justify="center">
        <Col className="comm-left" xs={0} sm={0} md={0} lg={5} xl={4} xxl={3}>
          <Author />
          <Project />
          <Poetry poetry={poetry} />
        </Col>

        <Col className="comm-center" xs={24} sm={24} md={24} lg={16} xl={16} xxl={16}>
          <motion.div className="sentence-wrap" initial="initial" animate="enter" exit="exit" variants={sentenceVariants}>
            <PoetrySentence staticFlag={true} handlePoetry={getPoetry} />
          </motion.div>
          <div className="comm-center-bg"></div>
          <motion.div initial="initial" animate="enter" exit="exit" variants={postVariants} className="comm-center-content">
            <BlogList articleList={articleList} />
          </motion.div>
        </Col>
      </Row>
    </div>
  )
}

需要实现动画效果的元素标签前加上 motion,在传入 initial,animate,exit,variants 等参数,variants 中

const postVariants = {
  initial: { scale: 0.96, y: 30, opacity: 0 },
  enter: { scale: 1, y: 0, opacity: 1, transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] } },
  exit: {
    scale: 0.6,
    y: 100,
    opacity: 0,
    transition: { duration: 0.5, ease: [0.48, 0.15, 0.25, 0.96] },
  },
}
// initial 初始状态
// enter 进入动画
// exit 退出状态
// 不想有退出动画,不写 exit 变量即可

注意:这里使用 AnimatePresence 改造了 _app.js 后,每个页面都要使用到 motion,否则页面切换不成功,不想要动画的可以如下给默认状态即可:

const Article = (props) =>{
    return (
    	<motion.div initial="initial" animate="enter" exit="exit">
        	...
        </motion.div>
    )
}

4. 页面切换状态

在 Next.js 中使用 import Link from 'next/link' 可以实现不刷新页面切换页面

import Link from 'next/link'

const BlogList = (props) => {
  return (
    <>
      <Link href={'/detailed?id=' + item.id}>
        <div className="list-title">{item.title}</div>
      </Link>
    </>
  )
}

export default BlogList

因为是在服务端渲染,在点击 Link 链接时,页面会有一段时间没任何反应,Next.js 默认会在右下角有一个转动的黑色三角,但实在是引不起用户注意。

这里使用插件 nprogress,实现顶部加载进度条

npm install nprogress -S

还是改造 _app.js

import 'antd/dist/antd.css'
import '../static/style/common.less'
import { AnimatePresence } from 'framer-motion'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import Router from 'next/router'

NProgress.configure({
  minimum: 0.3,
  easing: 'ease',
  speed: 800,
  showSpinner: false,
})
Router.events.on('routeChangeStart', () => NProgress.start())
Router.events.on('routeChangeComplete', () => NProgress.done())
Router.events.on('routeChangeError', () => NProgress.done())

export default function MyApp({ Component, pageProps, router }) {
  return (
    <AnimatePresence exitBeforeEnter>
      <Component {...pageProps} route={router.route} key={router.route} />
    </AnimatePresence>
  )
}

主要使用到 next/router 去监听路由切换状态,这里也可以自定义加载状态。

5. 页面 CSS 加载失败

在 Next.js 开发模式下,当第一次进入某个页面时,发现当前页面样式加载失败,必须刷新一下才能加载成功。

next-css: Routing to another page doesn't load CSS in development mode

Cant change page with 'next/link' & 'next-css'

在 Github 上也查到相关问题,说是在 _app.js 都引入一下,但是我试了下,还是不行,不过好在这种情况只在开发模式下,生产模式下没什么问题,所以也就没在折腾了,就这样刷新一下吧。

6. React Hoos 中实现 setInterval

在 components/PoetrySentence.js 中实现动态写一句诗的效果,在 class 中可以同通过 setInterval 简单实现,但在 React Hoot 中每次 render 重新渲染后都会执行 useEffect,或者 useEffect 依赖[] 就又只会执行一次,这里就通过依赖单一变量加 setTimeout 实现。

在 components/PoetrySentence.js 中

import { useState, useEffect } from 'react'
import { RedoOutlined } from '@ant-design/icons'
import { getPoetry, formatTime } from '../util/index'

const PoetrySentence = (props) => {
  const [sentence, setSentence] = useState('')
  const [finished, setFinished] = useState(false)
  const [words, setWords] = useState(null)
  const { staticFlag, handlePoetry } = props // 是否静态展示

  useEffect(
    async () => {
      if (words) {
        if (words.length) {
          setTimeout(() => {
            setWords(words)
            setSentence(sentence + words.shift())
          }, 150)
        } else {
          setFinished(true)
        }
      } else {
        let tmp = await todayPoetry()
        if (staticFlag) {
          setFinished(true)
          setSentence(tmp.join(''))
        } else {
          setWords(tmp)
          setSentence(tmp.shift())
        }
      }
    },
    [sentence]
  )

  const todayPoetry = () => {
    return new Promise((resolve) => {
      const now = formatTime(Date.now(), 'yyyy-MM-dd')
      let poetry = localStorage.getItem('poetry')
      if (poetry) {
        poetry = JSON.parse(poetry)
        if (poetry.time === now) {
          handlePoetry && handlePoetry(poetry)
          resolve(poetry.sentence.split(''))
          return
        }
      }
      getPoetry.load((result) => {
        poetry = {
          time: now,
          sentence: result.data.content,
          origin: {
            title: result.data.origin.title,
            author: result.data.origin.author,
            dynasty: result.data.origin.dynasty,
            content: result.data.origin.content,
          },
        }
        handlePoetry && handlePoetry(poetry)
        localStorage.setItem('poetry', JSON.stringify(poetry))
        resolve(poetry.sentence.split(''))
      })
    })
  }

  const refresh = () => {
    getPoetry.load((result) => {
      const poetry = {
        time: formatTime(Date.now(), 'yyyy-MM-dd'),
        sentence: result.data.content,
        origin: {
          title: result.data.origin.title,
          author: result.data.origin.author,
          dynasty: result.data.origin.dynasty,
          content: result.data.origin.content,
        },
      }
      handlePoetry && handlePoetry(poetry)
      localStorage.setItem('poetry', JSON.stringify(poetry))
      if (staticFlag) {
        setSentence(poetry.sentence)
      } else {
        setFinished(false)
        setWords(null)
        setSentence('')
      }
    })
  }

  return (
    <p className="poetry-sentence">
      {sentence}
      {finished ? <RedoOutlined style={{ fontSize: '14px' }} onClick={() => refresh()} /> : null}
      <span style={{ visibility: finished ? 'hidden' : '' }}>|</span>
    </p>
  )
}

export default PoetrySentence

useEffect 依赖变量 sentence,在 useEffect 中又去更改 sentence,sentence 更新后触发重新渲染,又会重新执行 useEffect,在 useEffect 中加上 setTimeout 延迟,刚好完美实现了 setInterval 效果。

7. node-sass

原本项目中使用的是 sass,但在后面 docker 部署安装依赖时,实在时太慢了,还各种报错,之前也是经常遇到,所以索性直接换成了 less,语法也差不多,安装起来省心多了。

二、Docker 篇

1. 什么是 Docker

Docker 是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。

容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

2. 为什么要使用 Docker

对我而言,因为现在使用的是阿里云服务器,部署了好几个项目,如果服务器到期后,更换服务器的话,就需要将所有项目全部迁移到新服务器,每个项目又要去依次安装依赖,运行,nginx 配置等等,想想都头大。而使用 Docker 后,将单个项目与其依赖打包成镜像,镜像可以在任何 Linux 中生产一个容器,迁移部署起来就方便多了。

其他而已,使用 Docker 可以让开发环境、测试环境、生产环境一致,并且每个容器都是一个服务,也方便后端实现微服务架构。

3. 安装

Docker 安装最好是参照官方文档,避免出现版本更新问题。docs.docker.com/engine/inst… 英文吃力的,这两推荐一款神奇词典 欧陆词典,哪里不会点哪里,谁用谁说好。

Mac 和 Windows 都有客户端,可以很简单的下载安装,另外 Window 注意区分专业版、企业版、教育版、家庭版

Window 专业版、企业版、教育版

Window 家庭版

因为我这里使用的是阿里云 Centos 7 服务器,所以简单介绍一下在 Centos 下的安装。

Centos 安装 Docker

首先若已经安装过 Docker,想再装最新版,先协助旧版

$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

有三种安装方式:

  1. Install using the repository
  2. Install from a package
  3. Install using the convenience script

这里选择官方推荐的第一种方式安装 Install using the repository

1、SET UP THE REPOSITORY

安装 yum-utils 工具包,设置存储库

$ sudo yum install -y yum-utils
$ sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

2、安装 docker

$ sudo yum install docker-ce docker-ce-cli containerd.io

这样安装的是最新的版本,也可以选择指定版本安装

查看版本列表:

$ yum list docker-ce --showduplicates | sort -r

Loading mirror speeds from cached hostfile
Loaded plugins: fastestmirror
Installed Packages
docker-ce.x86_64            3:20.10.0-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:20.10.0-3.el7                    @docker-ce-stable
docker-ce.x86_64            3:19.03.9-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.8-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.7-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.6-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.5-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.4-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.3-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.2-3.el7                    docker-ce-stable 
docker-ce.x86_64            3:19.03.14-3.el7                   docker-ce-stable
......

选择指定版本安装

$ sudo yum install docker-ce-<VERSION_STRING> docker-ce-cli-<VERSION_STRING> containerd.io

安装完成,查看版本

$ docker -v
Docker version 20.10.0, build 7287ab3

3、启动 docker

$ sudo systemctl start docker

关闭 docker

$ sudo systemctl stop docker

重启 docker

$ sudo systemctl restart docker

4. 镜像 Image

**Docker 把应用程序及其依赖,打包在 image 文件里面。**只有通过这个文件,才能生成 Docker 容器(Container)。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

官方有个镜像库 Docker Hub,很多环境镜像都可以从上面拉取。

4.1 查看镜像

$ docker images

或者

$ docker image ls

刚安装完 docker,是没有任何镜像的

$ docker image ls
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

查看全部镜像 id

$ docker images -q
# 
$ docker image ls -q

4.2 下载镜像

这里我们尝试从官方库下载一个 nginx 镜像,镜像有点类似与 npm 全局依赖,拉取后,后面所有需要使用的 nginx 的镜像都可以依赖此 nginx,不用再重新下载,刚开始学习时,我还以为每个使用到 nginx 的镜像都要重新下载呢。

下载 nginx 镜像 hub.docker.com/_/nginx

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
6ec7b7d162b2: Pull complete 
cb420a90068e: Pull complete 
2766c0bf2b07: Pull complete 
e05167b6a99d: Pull complete 
70ac9d795e79: Pull complete 
Digest: sha256:4cf620a5c81390ee209398ecc18e5fb9dd0f5155cd82adcbae532fec94006fb9
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        latest    ae2feff98a0c   13 hours ago   133MB

docker images 查看刚刚安装的 nginx 镜像,有 5 个title,分别为镜像名称,标签,id,创建时间,大小,其中 TAG 标签默认为 latest 最新版,如果下载指定版本,可以 : 后跟版本号

$ docker pull nginx:1.19

4.3 删除镜像

删除镜像可以使用如下命令

$ docker rmi [image]

或者

$ docker image rm [image]

[image] 可以是镜像名称+标签,也可以是镜像 id,ru

$ docker rmi nginx:latest
$ docker rmi ae2feff98a0c

删除所有镜像

$ docker rmi $(docker images -q)

删除所有 none 镜像

后面有些操作会重复创建相同的镜像,原本的镜像就会被覆盖变为 ,可以批量删除

$ docker rmi $(docker images | grep "none" | awk '{print $3}')

4.4 制作镜像

上面我们下载了 nginx 镜像,但是要想运行我们自己的项目,我们还要制作自己项目的镜像,然后来生成容器才能运行项目。

制作镜像需要借助 Dockerfile 文件,以本项目 admin 后台界面为例(也可以任何 html 文件),因为其打包后只需使用到 nginx 即可访问。

先在 admin 下运行命令 npm run build 打包生成 build 文件夹,下面包好 index.html 文件,在 admin/docker 文件夹下创建 Dockerfile 文件,内容如下

FROM nginx

COPY ../build /usr/share/nginx/html

EXPOSE 80

将 build,docker 两个文件夹放在服务器同一目录下,如 /dockerProject/admin

├─admin
  └─build
    └─index.html
  └─docker
    └─Dockerfile

在 docker 目录下运行命令

$ docker build ./ -t admin:v1

Sending build context to Docker daemon  4.096kB
Step 1/3 : FROM nginx
 ---> ae2feff98a0c
Step 2/3 : COPY ../build /usr/share/nginx/html
COPY failed: forbidden path outside the build context: ../build ()

./ 基于当前目录为构建上下文, -t 指定制作的镜像名称。

可以看到上面报错了,

The path must be inside the context of the build; you cannot ADD ../something/something, because the first step of a docker build is to send the context directory (and subdirectories) to the docker daemon.

上面大意是确定构建上下文后,中间的一些文件操作就只能在当前上下文之间进行,有两种方式解决

  1. Dockfile 与 build 同目录

    ├─admin
      └─build
        └─index.html
      └─Dockerfile
    

    Dockerfile:

    FROM nginx
    
    COPY ./build /usr/share/nginx/html
    
    EXPOSE 80
    

    admin 目录下执行命令

    $ docker build ./ -t admin:v1
    
    Sending build context to Docker daemon  3.094MB
    Step 1/3 : FROM nginx
     ---> ae2feff98a0c
    Step 2/3 : COPY ./build /usr/share/nginx/html
     ---> Using cache
     ---> 0e54c36f5d9a
    Step 3/3 : EXPOSE 80
     ---> Using cache
     ---> 60db346d30e3
    Successfully built 60db346d30e3
    Successfully tagged admin:v1
    
  2. 依然将 Dokcerfile 放入 docker 中统一管理

    ├─admin
      └─build
        └─index.html
      └─docker
        └─Dockerfile
    

    Dockerfile:

    FROM nginx
    
    COPY ./build /usr/share/nginx/html
    
    EXPOSE 80
    

    admin 目录下执行命令

    $ docker build -f docker/Dockerfile ./ -t admin:v1
    
    Sending build context to Docker daemon  3.094MB
    Step 1/3 : FROM nginx
     ---> ae2feff98a0c
    Step 2/3 : COPY ./build /usr/share/nginx/html
     ---> Using cache
     ---> 0e54c36f5d9a
    Step 3/3 : EXPOSE 80
     ---> Using cache
     ---> 60db346d30e3
    Successfully built 60db346d30e3
    Successfully tagged admin:v1
    

    注意这里的 ./build 路径。-f (-file)指定一个 Dockfile 文件,./ 以当前路径为构建上下文,所以 build 路径还是 ./build

上面使用到了 Dockerfile 文件,因为内容比较少,这里先不介绍,后面部署 Next.js 时在稍作说明。

5. 容器 Container

上面生成了 admin:v1 镜像,我们查看一下

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
admin        v1        60db346d30e3   51 minutes ago   136MB
nginx        latest    ae2feff98a0c   14 hours ago     133MB

可以看到多了 admin:v1 镜像,且在上面构建镜像时步骤 Step 1 ,速度很快,直接使用了之前下载的 nginx 镜像,如果之前没下载,这里就会去下载。

项目运行在容器内,我们需要通过一个镜像创建一个容器。

5.1 查看容器

$ docker container ls
CONTAINER ID   IMAGE      COMMAND       CREATED          STATUS          PORTS          NAMES

$ docker ps
CONTAINER ID   IMAGE      COMMAND       CREATED          STATUS          PORTS          NAMES	

这两个命令只显示正在运行的容器,报错停止的都不会显示,加上 -a (--all) 参可以显示全部

$ docker container ls -a
$ docker ps -a

查看所有容器 id

$ docker ps -aq

5.2 生成容器

这里我们通过 admin:v1 来生成一个容器

$ docker create -p 9001:80 --name admin admin:v1
  • -p:端口映射,宿主机(服务器) : 容器,9001:80 代表宿主机 9000 端口可以访问到容器的 80 端口
  • --name:生成的容器名称,唯一值
  • admin:v1:使用的镜像,标签 :v1 默认为 :latest

还有很多参数,可自行了解 docs.docker.com/engine/refe…

生成容器后,咱们来看看

$ docker ps -a
CONTAINER ID   IMAGE      COMMAND                  CREATED         STATUS    PORTS     NAMES
8d755bab5c73   admin:v1   "/docker-entrypoint.…"   5 minutes ago   Created             admin

可以看到容器已经生成,但还没有运行,所有使用 docker ps 是看不到的

运行容器:docker start [container iD],【】里面可以使用容器 ID,也可以使用容器名称,都是唯一的

$ docker start admin

$ docker ps -a
CONTAINER ID   IMAGE      COMMAND                CREATED         STATUS         PORTS                 NAMES
8d755bab5c73   admin:v1   "/docker-entrypoint.…"   8 minutes ago   Up 3 seconds  0.0.0.0:9001->80/tcp admin

容器已经运行,此时通过服务器ip + 9001 端口(Mac、Windows 直接 localhost:9001)即可访问到容器内部。

以上生成容器,运行容器也可以一条命令

$ docker run -p 9001:80 --name admin admin:v1

5.3 删除容器

删除容器可以使用如下命令

$ docker rm admin # id 或 name

如果容器在运行中,要先停止容器

$ docker stop admin

或者强制删除

$ docker rm -f admin

停止所有容器

$ docker stop $(docker ps -aq)

删除所有容器

$ docker rm $(docker ps -aq)

停止并删除所有容器

$ docker stop $(docker ps -aq) & docker rm $(docker ps -aq)

5.4 容器日志

运行容器时如果失败,可以查看日志定位错位

$ docker logs admin

5.5 进入容器内部

容器就像一个文件系统,我们也可以进去查看里面的文件,使用以下命令进入容器内部

$ docker exec -it admin /bin/sh
  • -i 参数让容器的标准输入持续打开,--interactive
  • -t 参数让 Docker 分配一个伪终端,并绑定到容器的标准输入上, --tty
  • admin: 容器 id 或名字

进入容器内部后,可以使用 Linux 命令访问内部文件

$ ls
bin  boot  dev	docker-entrypoint.d  docker-entrypoint.sh  etc	home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

$ cd usr/share/nginx/html
$ ls
50x.html  asset-manifest.json  favicon.ico  index.html	manifest.json  robots.txt  static

进入 nginx 默认 html 目录 usr/share/nginx/html,可以看到我们通过 Dockfile 拷贝过来的文件

6. docker-compose

通过上面可以发现每次制作镜像,生成容器,运行容器,都要输入很多命令,实在是很不方便,如果只要一个简单的命令就能完成就好了,docker-compose 就可以实现,当然,这只是它很小的一部分功能。

官方简介如下:

Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过Compose,您可以使用 YAML 文件来配置应用程序的服务。然后,使用一个命令,就可以从配置中创建并启动所有服务。

使用Compose基本上是一个三步过程:

  • 使用 Dockerfile 定义应用程序的环境,以便可以在任何地方复制它。
  • 在 docker-compose.yml 中定义组成您的应用程序的服务,以便它们可以在隔离的环境中一起运行。
  • 运行 docker-compose up,然后 Compose 启动并运行整个应用程序。

6.1 安装 docker-compose

参考官方文档 Install Docker Compose ,这里简单介绍 Linux 安装

  1. 运行命令

    sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
    

    若是安装慢,可以用 daocloud 下载

    sudo curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
    
  2. 添加可执行权限

    sudo chmod +x /usr/local/bin/docker-compose
    
  3. 检查是否安装完成

    docker-compose --version
    

6.2 docker-compose.yml

docker-compose.yml 是 docker-compose 运行时使用文件,里面配置了镜像和容器一些参数,这里来实现上面创建镜像,生成容器,运行容器。

version: '3'
services:
  admin:
    build: 
      context: ../
      dockerfile: ./docker/Dockerfile
    image: admin:v1
    ports: 
      - 9001:80
    container_name: admin

配置参数有很多 docs.docker.com/compose/com…,官网可以详解,这里以及后面只说说用到的一些配置。

  • varsion:可选 1,2,2.x,3.x
  • services:服务组
    • admin:服务名称,唯一,多个 docker-compose.yml 有相同名称的,下面的容器会覆盖
      • build:构建参数,如果 docker-compose.yml、Dockfile 都和 built 文件加目录,可直接 ./ ,当前构建上下文,当前 Dockerfile;如果 docker-compose.yml、Dockfile 都放在 docker 文件夹下,则需指定构建上下文 context 和 dokcerfile
      • context:构建上下文
      • dockerfile:指定 dockerfile 路径
    • image:指定使用的镜像,如果镜像存在,会直接使用镜像,否则的话通过上面的 dockerfile 构建
    • ports:端口映射,可多个。文件中 - 就代表参数是数组形式,可以多个
    • container_name:容器名字,若不指定,这默认为 当前目录_admin_index (admin:服务名,index:数字,累加,一个服务可以有可以容器,不同 docker-compose.yml 里有相同服务)

将 docker-compose.yml 放入 docker 目录下

├─admin
  └─build
    └─index.html
  └─docker
    └─Dockerfile
    └─docker-compose.yml

在 docker 目录下运行

$ docker-compose up -d --build
  • -d:代表在后台守护运行,不加 -d 的话,会显示构建过程,最后完成只能 Ctrl + C 退出,容器也就停止了,要再去启动容器
  • --build:表示每次构建都重新执行一遍 Dockerfile 生成镜像(会重新安装 npm 包),不加的话如果镜像存在的话,就不会再执行 Dockerfile,一般是 Dockerfile 有变动时加上 --build

docker-compose 与 build 同目录

├─admin
  └─build
    └─index.html
  └─Dockerfile
  └─docker-compose.yml

则,docker-compose.yml

version: '3'
services:
  admin:
    build: ./
    image: admin:v1
    ports: 
      - 9001:80
    container_name: admin

在 build 目录下运行

$ docker-compose up -d --build

字数超了,未完待续,部署篇~