Docker 部署 React 全棧應用

前言

以前使用 Vue 全家桶開發了我的博客,並部署在阿里雲服務器上,最近在學習 React,因而使用 React 開發重構了本身的博客。css

主要技術棧以下:html

  • 前臺頁面:Next.js 搭建服務端渲染頁面,利於 SEO
  • 後臺管理界面:create-react-app 快速搭建
  • 服務接口:Egg.js + Mysql
  • 部署:Linux + Docker

React 搭建博客先後臺部分,這裏不會細講,只會說說中間遇到的一些問題和一些解決方法,具體開發教程可參考 React Hooks+Egg.js實戰視頻教程-技術胖Blog開發前端

部署部分這裏會是重點講解,由於也是第一次接觸 Docker,這裏只記錄本身的學習心得,有不對的地方還請多多指教。vue

剛好本次項目裏前臺頁面是 node 運行,後臺界面是靜態 HTML,服務接口須要鏈接 Mysql,我以爲 Docker 來部署這幾種狀況也是比較全面的例子了,能夠給後來同窗做爲參考,內容比較囉嗦,但願能幫助後來的同窗少走一點坑,由於有些是本身的理解,可能會有錯誤,還請你們指正,互相學習。node

項目地址

源碼地址:https://github.com/Moon-Futur...mysql

clone 下來參照目錄哦~react

1、React 篇

博客前臺使用 Next.js 服務端渲染框架搭建,後臺管理界面使用 create-react-app 腳手架搭建,服務接口使用 Egg 框架(基於 Koa)。後臺管理和服務接口沒什麼好說的,就是一些 React 基礎知識,這裏主要說下 Next.js 中遇到的一些問題。linux

項目目錄:

blog:前臺界面,Next.jsios

admin:後臺管理界面,create-react-app 腳手架搭建nginx

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-motionhttps://www.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,語法也差很少,安裝起來省心多了。

2、Docker 篇

1. 什麼是 Docker

Docker 是一個開源的應用容器引擎,可讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,而後發佈到任何流行的 Linux 機器上,也能夠實現虛擬化。

容器是徹底使用沙箱機制,相互之間不會有任何接口(相似 iPhone 的 app),更重要的是容器性能開銷極低。

2. 爲何要使用 Docker

對我而言,由於如今使用的是阿里雲服務器,部署了好幾個項目,若是服務器到期後,更換服務器的話,就須要將全部項目所有遷移到新服務器,每一個項目又要去依次安裝依賴,運行,nginx 配置等等,想一想都頭大。而使用 Docker 後,將單個項目與其依賴打包成鏡像,鏡像能夠在任何 Linux 中生產一個容器,遷移部署起來就方便多了。

其餘而已,使用 Docker 可讓開發環境、測試環境、生產環境一致,而且每一個容器都是一個服務,也方便後端實現微服務架構。

3. 安裝

Docker 安裝最好是參照官方文檔,避免出現版本更新問題。https://docs.docker.com/engine/install/ 英文吃力的,這兩推薦一款神奇詞典 歐陸詞典,哪裏不會點哪裏,誰用誰說好。

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

一、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

二、安裝 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

三、啓動 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 鏡像 https://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 鏡像

後面有些操做會重複建立相同的鏡像,本來的鏡像就會被覆蓋變爲 <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

還有不少參數,可自行了解 https://docs.docker.com/engine/reference/commandline/create/#options

生成容器後,我們來看看

$ 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

配置參數有不少 https://docs.docker.com/compose/compose-file/,官網能夠詳解,這裏以及後面只說說用到的一些配置。

  • 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

3、部署篇

上面簡單介紹了 docker 的一些用法,借用靜態 HTML 文件與 nignx 鏡像建立運行了一個容器,可是其遠遠不止這些,下面就經過部署本博客來做爲例子再探探裏面的一些知識點。

源碼地址:https://github.com/Moon-Futur...,可下載下來看着目錄更清晰。

爲了統一維護 docker 文件,如下將 docker 相關文件都放在各自目錄下 docker 文件下,因此要特別主題構建上下文(context)的肯定。

1. 部署前臺 blog

端口映射 9000:9000,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號

在 blog 目錄下建立 docker 目錄,docker 目錄下建立三個文件

  • .dockerignore:拷貝文件忽略列表
  • Dockefile
  • docker-compose.yml

.dockerignore

node_modules
.next

Dockefile

# node 鏡像
# apline 版本的node會小不少
FROM node:12-alpine

# 在容器中建立目錄
RUN mkdir -p /usr/src/app

# 指定工做空間,後面的指令都會在當前目錄下執行
WORKDIR /usr/src/app

# 拷貝 package.json
COPY package.json /usr/src/app

# 安裝依賴
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷貝其餘全部文件到容器(除了 .dockerignore 中的目錄和文件)
COPY . /usr/src/app

# build
RUN npm run build

# 暴露端口 9000
EXPOSE 9000

# 運行容器時執行命令,每一個 Dokcerfile 只能有一個 CMD 命令,多個的話只有最後一個會執行
CMD [ "npm", "start" ]

Docker 鏡像是分層的,下面這些知識點很是重要:

  • Dockerfile 中的每一個指令都會建立一個新的鏡像層,每一個 RUN 都是一個指令 https://docs.docker.com/engin...
  • 鏡像層將被緩存和複用
  • 當 Dockerfile 的指令修改了,複製的文件變化了,或者構建鏡像時指定的變量不一樣了,對應的鏡像層緩存就會失效
  • 某一層的鏡像緩存失效以後,它以後的鏡像層緩存都會失效
  • 鏡像層是不可變的,若是咱們再某一層中添加一個文件,而後在下一層中刪除它,則鏡像中依然會包含該文件(只是這個文件在 Docker 容器中不可見了)。

因此咱們先拷貝 package.json,而後 RUN npm i 安裝依賴,造成一個鏡像層,再拷貝其餘全部文件,造成一個鏡像層,以後若是代碼有所變更,可是 package.json 沒有變更,再次執行時,就不會再安裝依賴了,能夠節省不少時間。package.json 有變更,纔會從新執行 RUN run i 安裝依賴。

假如生成了鏡像 imageA,此時要刪除 imageA,從新生成,記住先生成新的鏡像 imageB,這樣纔會複用 npm 包,若是先刪除了 imageA,再新生成 imageB,則又會從新安裝依賴。

在 blog 目錄下運行如下命令能夠生成鏡像 react_blog:blog

$ docker build -f docker/Dockerfile . -t react_blog:blog

第一次運行安裝依賴時有點慢(有個 sharp 特別慢...),剛開始我使用 node-sass 時,安裝老是報錯,後來索性就換成了 less,省心。若是想用 yarn 安裝的話,這裏 Dockerfile 裏 npm 相關的命令也能夠換成對於的 yarn 命令。

漫長的等待後終於 build 成功,下面一些信息就是 npm run build 生成的文件

image-20201217110814789

看看生成的鏡像

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED         SIZE
react_blog   blog        fef06dfed97f   3 minutes ago   329MB
nginx        latest      ae2feff98a0c   31 hours ago    133MB
node         12-alpine   844f8bb6a3f8   3 weeks ago     89.7MB

而後來生成並運行容器

$ docker run -itd -p 9000:9000 --name react_blog_blog react_blog:blog

這裏參數再說明一下:

  • -i 參數讓容器的標準輸入持續打開,--interactive
  • -t 參數讓 Docker 分配一個僞終端,並綁定到容器的標準輸入上, --tty
  • -d 參數讓容器在後臺,以守護進程的方式執行,--detach(Run container in background and print container ID)
  • --name 參數指定容器惟一名稱,若不指定,則隨機一個名稱

-it 通常同時加上,-d 參數若是不加的話,運行容器成功時,會進入一個終端命令界面,要想退出的話只能 Ctrl + C,退出以後容器也就退出了,docker ps -a 能夠看到容器狀態是 Exited (0) ,可使用 docker start container 再次開啓。加上 -d 的話容器就會直接在後臺運行,通常的話就加上 -d。你們能夠試試,以後再刪除容器就能夠了。

image-20201217112605886

以上容器運行成功的話,在瀏覽器經過 服務器ip:9000 就能夠訪問到頁面啦,Mac 或者 Windows 本地的話 localhost:9000 就能夠訪問啦。

docker-compose.yml

version: '3'
services:
  web:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:blog
    ports:
      - 9000:9000
    container_name: react_blog_blog

上面我們經過 docker builddocker run 等命令先生成容器,再生成並運行容器,是否是有點繁瑣,命令很差記,輸入也麻煩,這裏咱們就能夠利用 docker-compose 來簡化執行命令。

咱們看一下文件內容:

  • web:服務名
  • build:構建相關,後面執行 docker-compose 命令路徑要和 docker-compose.yml 同一路徑,因此這裏 context 構建上下文選擇上一層源碼目錄,dockerfile 就是當前目錄裏的 Dockerfile
  • image:鏡像名,若是有就直接使用,沒有就經過上面的 Dockerfile 生成
  • ports:端口映射
  • container_name:容器名稱,惟一。若不寫,則爲 當前目錄_服務名_index,index 數字(從1累加),若這裏爲 docker_web_1

能夠把上面用 Dockerfile 生成的容器刪了 docker rm -f react_blog_blog,用 docker-compose up 生成試試

在 docker 目錄下執行命令

$ docker-compose up -d

要想從新生成鏡像能夠 docker-compose up -d --build

以上便把 blog 前端頁面部署好了,如今只是單獨部署學習,後面會刪了和後臺與接口一塊兒部署。

2. 部署後臺 admin

端口映射 9001:9001,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號

如今來單獨部署 admin,在 Docker 篇時,咱們已經使用到 admin 來簡單部署學習製做鏡像和生成容器,這裏依然先在 admin 目錄下生成生成環境靜態文件

$ npm run build

在 admin 下建立 docker 目錄用來存放 docker 相關文件,docker 目錄下建立如下文件:

Dockerfile

FROM nginx

# 刪除 Nginx 的默認配置
RUN rm /etc/nginx/conf.d/default.conf

EXPOSE 80

注意這裏和上面的一些區別,

  • 這裏把 nginx 默認的配置刪除了,以後咱們本身配置一個
  • 沒有 COPY 靜態文件到容器,在 docker-compose.yml 經過掛在的方式實現

docker-compose.yml

version: '3'
services:
  admin:
    build: 
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:admin
    ports: 
      - 9001:80
    volumes: 
      - ../build:/www
      - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
    container_name: react_blog_admin

這裏多了 volumes (卷) 項,參數是數組,對應 宿主機文件:容器內文件

  • ../build:/www,build 內的文件掛在到容器 /www 目錄下
  • ./nginx.conf:/etc/nginx/conf.d/nginx.conf,nginx.conf 掛載到容器 /etc/nginx/conf.d/nginx.conf 這個文件

這樣作的好處是,當宿主機上的文件變更後,容器內的文件也會自動變更,相應的容器內文件變更,宿主機文件也會變更。這樣以後源代碼變更,從新打包生成 build 後,只須要放到服務器對應目錄下,容器類 /www 下的類容就會是最新的,而不須要一次次的去執行 Dockerfile 拷貝 build 文件到容器內,數據庫的數據一般也是這樣保存在宿主機內,而防止容器刪除時丟失數據。

同理 nginx.conf 配置文件也是同樣,不過改動 nginx 配置文件後,要重啓如下容器才生效 docker restart container

來運行容器吧,在 docker 目錄下執行命令

$ docker-compose up -d

查看容器是否運行成功

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND             CREATED         STATUS          PORTS            NAMES
7db8ce1c6814   react_blog:admin   "/docker-entrypoint.…"   16 minutes ago   Up 16 minutes   0.0.0.0:9001->80/tcp   react_blog_admin

運行失敗的能夠 docker logs container 查看日誌

運行成功的話,在瀏覽器經過 服務器ip:9001 就能夠訪問到頁面啦,Mac 或者 Windows 本地的話 localhost:9001 就能夠訪問啦。

nginx.conf

server {
  listen 80;
  sendfile on;
  sendfile_max_chunk 1M;
  tcp_nopush on;
  gzip_static on;

  location / {
    root /www;
    index index.html;
  }
}

root 記得和上面掛在目錄相同

3. 部署服務接口 service + Mysql

端口映射 9002:9002,服務器端口:容器端口,如果線上服務器,要先在安全組裏開通對應的端口號

如今咱們來部署服務接口,在 service 目錄下建立 docker 目錄,docker 目錄下建立如下文件:

.dockerignore

node_modules
.github
article

article 目錄用來存放博客內容文件

Dockerfile

FROM node:alpine

# 配置環境變量
ENV NODE_ENV production

# 這個是容器中的文件目錄
RUN mkdir -p /usr/src/app 

# 設置工做目錄
WORKDIR /usr/src/app

# 拷貝package.json文件到工做目錄
# !!重要:package.json須要單獨添加。
# Docker在構建鏡像的時候,是一層一層構建的,僅當這一層有變化時,從新構建對應的層。
# 若是package.json和源代碼一塊兒添加到鏡像,則每次修改源碼都須要從新安裝npm模塊,這樣木有必要。
# 因此,正確的順序是: 添加package.json;安裝npm模塊;添加源代碼。
COPY package.json /usr/src/app/package.json

# 安裝npm依賴(使用淘寶的鏡像源)
# 若是使用的境外服務器,無需使用淘寶的鏡像源,即改成`RUN npm i`。
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷貝全部源代碼到工做目
COPY . /usr/src/app

# 暴露容器端口
EXPOSE 9002

CMD npm start

docker-compose.yml

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    depends_on:
      - db
    environment:
      MYSQL_HOST: localhost
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ../article:/usr/src/app/article
    container_name: react_blog_service
  db:
    image: mysql
    # volumes:
    #    - /db_data:/var/lib/mysql
    ports:
      - 33061:3306
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 8023
      # MYSQL_USER: root
      MYSQL_PASSWORD: 8023
      MYSQL_DATABASE: react_blog
    container_name: react_blog_mysql

注意這裏有運行了兩個服務 service、db

service 服務是後端接口:

  • deponds_on:運行時會先運行 deponds_on 列表裏的服務,防止依賴項還沒運行,本身會報錯
  • command:從 MySQL8.0 開始,默認的加密規則使用的是 caching_sha2_password,此命令能夠更改加密規則。不加可能會報錯 Client does not support authentication protocol requested by server; consider upgrading MySQL client
  • environment:環境變量,這裏會傳入代碼中,在代碼 /config/secret.js(secret-temp.js) 裏面能夠會使用到

    /**

*/

module.exports = {

// mysql 鏈接配置
mysql: {
  host: process.env.MYSQL_HOST || 'localhost',
  port: process.env.MYSQL_PORT || '3306',
  user: process.env.MYSQL_USER || 'xxx',
  password: process.env.MYSQL_PASSWORD || 'xxx',
  database: process.env.MYSQL_DATABASE || 'xxxxxx',
},
// jwt
tokenConfig: {
  privateKey: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
},

}

- volumes:這裏我把文章寫入宿主機了,掛載到容器裏

**db** 服務是 Mysql 數據庫:

- volumes:數據設置存儲在宿主機

- ports:端口映射,宿主機經過 33061 端口能夠訪問容器內部 Mysql,咱們以後就能夠經過 Navicat 或其餘數據庫可視化工具來鏈接
- environment:配置數據庫
- MYSQL_ROOT_PASSWORD 必需要帶上,設置 ROOT 帳號的密碼
- MYSQL_USER 容器登陸 MySQL 用戶名,**注意**,這裏若是是 root 會報錯 `ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ` ,根據 https://github.com/docker-library/mysql/issues/129 可知,已經存在一個 root 用戶,沒法再建立,因此這個能夠不帶,就默認 root 用戶登陸,若是帶的話就不要是 root,會新建一個帳戶
- MYSQL_PASSWORD 容器登陸 Mysql 密碼,對用戶名 MYSQL_USER,若是是 ROOT,密碼就是 MYSQL_ROOT_PASSWORD,若是是其餘,就是設置新密碼
- MYSQL_DATABASE 建立一個 react_blog 數據庫,也能夠不填,後面再進入容器或者 Navicat 建立,可是這裏由於後端代碼要鏈接到 react_blog 數據庫,不建立的會鏈接會保存,因此仍是加上。(實在不想加也能夠後見建立好數據庫後,才運行兩個容器)



在 service/docker 目錄下執行命令

$ docker-compose up -d

運行成功的話,看看 images 和 container

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
react_blog service 89139d833458 About an hour ago 150MB
react_blog admin 1b5d6946f1fe 32 hours ago 133MB
react_blog blog fef06dfed97f 35 hours ago 329MB
nginx latest ae2feff98a0c 2 days ago 133MB
mysql latest ab2f358b8612 6 days ago 545MB
node 12-alpine 844f8bb6a3f8 3 weeks ago 89.7MB

能夠看到多了 Mysql 和 react_blog:blog 鏡像

$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5878940d7626 react_blog:blog "docker..." 5 seconds ago Up 4 seconds 0.0.0.0:9000->9000/tcp react_blog_blog
3bff0060de19 react_blog:admin "/docker…" 3 minutes ago Up 18 seconds 0.0.0.0:9001->80/tcp react_blog_admin
d8a899232e8c react_blog:service "docker…" About a Exited (1) 5 minutes ago react_blog_service
a9da07ff5cae mysql "docker…" About an hr 33060/tcp, 0.0.0.0:33061->3306/tcp react_blog_mysql

能夠看到多了 react_blog_service 和 react_blog_mysql 容器,其中 react_blog_service 容器運行失敗了,顯示沒事失敗的先別高興,我們來看看日誌

$ docker logs react_blog_service

...
errno: "ECONNREFUSED"
code: "ECONNREFUSED"
syscall: "connect"
address: "127.0.0.1"
port: 3306
fatal: true
name: "ECONNREFUSEDError"
pid: 47
hostname: d8a899232e8c
...

能夠看出是數據庫鏈接失敗了,在上面 **docker-compose.yml** 中咱們定義的環境變量 `MYSQL_HOST=localhost` 傳給後端代碼來鏈接數據庫,每一個容器都至關一一個獨立的個體,localhost 是 react_blog_service 本身的 ip (127.0.0.1),固然是訪問不到 react_blog_mysql,這個問題咱們在下一節再來解決,先來講說 Mysql。

上面能夠看到 Mysql 容器已經成功運行,咱們能夠進入容器內部鏈接 Mysql,還記得怎麼進入容器嗎

$ docker exec -it react_blog_mysql /bin/sh

$ ls
bin boot dev docker-entrypoint-initdb.d entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.22 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

能夠看到順利鏈接 Mysql,輸入 `exit` 能夠退出容器。咱們也可使用可視化工具來鏈接,我這裏使用 Navicat 來鏈接

![image-20201218224818914](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-03.png)

![image-20201218225257487](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-04.png)



注意這裏的端口 33061,上面咱們經過端口映射,經過宿主機端口 33061 能夠訪問到 Mysql 容器內端口 3306,因此就鏈接上啦。

> 在這個過程當中,個人服務器(宿主機)上的 Mysql 出現了問題,鏈接時報錯 `2013 lost connection to mysql server at 'reading initial communication packet'`,我也不知道是什麼緣由引發的,解決方式是運行命令 `systemctl start mysqld.service` 啓動 Mysql 服務,也不知是哪裏影響到了,不事後面我會直接鏈接宿主機 Mysql,不使用容器,這樣能夠和其餘項目統一管理數據,我任務比較方便,且數據也較安全。



### 4. 容器互聯

上面留了一個問題,service 鏈接數據庫失敗,如今咱們來嘗試解決。參考 [Docker 築夢師系列(一):實現容器互聯](https://tuture.co/2020/01/12/cd44c84/)

#### 4.1 Network 類型

Network,顧名思義就是 「網絡」,可以讓不一樣的容器之間相互通訊。首先有必要要列舉一下 Docker Network 的五種驅動模式(driver):

- `bridge`:默認的驅動模式,即 「網橋」,一般用於**單機**(更準確地說,是單個 Docker 守護進程)
- `overlay`:Overlay 網絡可以鏈接多個 Docker 守護進程,一般用於**集羣**,後續講 Docker Swarm 的文章會重點講解
- `host`:直接使用主機(也就是運行 Docker 的機器)網絡,僅適用於 Docker 17.06+ 的集羣服務
- `macvlan`:Macvlan 網絡經過爲每一個容器分配一個 MAC 地址,使其可以被顯示爲一臺物理設備,適用於但願直連到物理網絡的應用程序(例如嵌入式系統、物聯網等等)
- `none`:禁用此容器的全部網絡

默認狀況下,建立的容器都在 bridge 網絡下,以下如所示,各個容器經過 dokcer0 可鏈接到宿主機HOST,而且各自分配到 IP,這種狀況下,容器間互相訪問須要輸入對方的 IP 地址去鏈接。

![image-20201220153944034](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-05.png)



查看 network 列表

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
5ec462838a1c host host local
c726e6887f10 none null local

這裏有 4 的 network,默認原本只有 3 個,沒有 docker_default,我也是寫到這裏才發現建立了一個 docker_default 網絡,查找官網([Networking in Compose](https://docs.docker.com/compose/networking/#:~:text=By%20default%20Compose%20sets%20up,identical%20to%20the%20container%20name.))才發現,經過 docker-compose 來生成運行容器時,若是沒指定 network,會自動建立一個 network,包含當前 docker-compose.yml 下的全部容器,network 名字默認爲 **`目錄_default`** ,這裏目錄就是 `docker` 剛好咱們這個幾個 docker-compose.yml 都是放在 docker 目錄下,因此建立的幾個容器都是在  docker_default 網絡裏。能夠一下命令查看網絡詳細信息

$ docker network inspect docker_default

[

{
    "Name": "docker_default",
    "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
    "Created": "2020-12-16T11:03:37.2152073+08:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
        "Driver": "default",
        "Options": null,
        "Config": [
            {
                "Subnet": "172.24.0.0/16",
                "Gateway": "172.24.0.1"
            }
        ]
    },
    "Internal": false,
    "Attachable": true,
    "Ingress": false,
    "ConfigFrom": {
        "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
        "23891d43187e046eea25936dc0ab703964cc6c7213bb150ae9529da3e2e57662": {
            "Name": "react_blog_mysql",
            "EndpointID": "649857f928e0444500cfd296035869678bf26162d429a4499b262776b2a1d264",
            "MacAddress": "02:42:ac:18:00:03",
            "IPv4Address": "172.24.0.3/16",
            "IPv6Address": ""
        },
        "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
            "Name": "react_blog_admin",
            "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
            "MacAddress": "02:42:ac:18:00:02",
            "IPv4Address": "172.24.0.2/16",
            "IPv6Address": ""
        },
        "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
            "Name": "react_blog_blog",
            "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
            "MacAddress": "02:42:ac:18:00:04",
            "IPv4Address": "172.24.0.4/16",
            "IPv6Address": ""
        }
    },
    "Options": {},
    "Labels": {
        "com.docker.compose.network": "default",
        "com.docker.compose.project": "docker",
        "com.docker.compose.version": "1.25.1"
    }
}

]

能夠看到 docker_default 網關地址爲 `172.24.0.1` ,其餘幾個容器 IP 分別爲 `172.24.0.3`,`172.24.0.2`,`172.24.0.4`,因此這裏的狀況是這樣的

![image-20201220154038077](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-06.png)

上面說了默認網絡 bridge 下容器見訪問只能輸入 **IP 地址**來鏈接,而自定義的網絡還能夠經過**容器名**來鏈接

> On user-defined networks like `alpine-net`, containers can not only communicate by IP address, but can also resolve a container name to an IP address. This capability is called **automatic service discovery**. 
>
> [Networking with standalone containers](https://docs.docker.com/network/network-tutorial-standalone/)

這就能夠避免每次生成容器 IP 會變的問題了。知道了這些,咱們在 service 接口裏就可已經過 react_blog_mysql 來鏈接 react_blog_mysql 容器了,service/docker/docker-compose.yml 修改以下:

version: '3'
services:
service:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
restart: on-failure
depends_on:
  - db
environment:
  MYSQL_HOST: react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接
  MYSQL_USER: root
  MYSQL_PASSWORD: 8023
volumes:
  - ./article:/usr/src/app/article
container_name: react_blog_service

db:

image: mysql
ports:
  - 33061:3306
restart: on-failure
command: --default-authentication-plugin=mysql_native_password
environment:
  MYSQL_ROOT_PASSWORD: 8023
  MYSQL_PASSWORD: 8023
  MYSQL_DATABASE: react_blog
container_name: react_blog_mysql
在此運行命令

$ docker-compose up -d --build

![image-20201219011335466](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-07.png)

能夠看到服務容器已正常運行,`docker logs react_blog_service` 查看日誌也沒有報錯,說明已經鏈接數數據庫,在代碼你我加了一個 get 測試接口,在瀏覽器輸入 `IP:9002/api/test/get` 或者 `localhost:9002/api/test/get`,會返回一個 json 對象

{"message":"Hello You Got It"}

**這裏我試了 N 久,一直有問題,**

- Mysql 建立失敗,environment 我加了一個 MYSQL_USER: root,結果一直報錯  `ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ` ,根據 https://github.com/docker-library/mysql/issues/129 可知,已經存在一個 root 用戶,沒法再建立,因此這個能夠不帶,就默認 root 用戶登陸,若是帶的話就不要是 root,會新建一個帳戶。這裏直接去掉 MYSQL_USER,使用 root 登陸

- service 建立失敗,日誌報錯沒鏈接上 Mysql,我試試了很久,最後發現重啓一下 service `docker start react_blog_service` 就能夠了,因此我以爲應該是 Mysql 建立好後,數據口等一些配置還沒搞好,因此 service 還鏈接不上,就一直報錯,等一會從新運行 service 就行了,因此這裏加上了 restart 參數,報錯就從新啓動,這樣就不用本身去重啓了,等一會,看日誌沒問題,就是鏈接成功了。

  明明使用了 depends_on,爲何還會有這種問題呢,我也不太清楚,不過官網有這段示例:

version: "3.9"
services:

web:
  build: .
  depends_on:
    - db
    - redis
redis:
  image: redis
db:
  image: postgres
> `depends_on` does not wait for `db` and `redis` to be 「ready」 before starting `web` - only until they have been started. If you need to wait for a service to be ready, see [Controlling startup order](https://docs.docker.com/compose/startup-order/) for more on this problem and strategies for solving it.
>
> Depends_on 在啓動 Web 以前不會等待 db 和 Redis 處於「就緒」狀態-僅在它們啓動以前。
>
> 應該就這個緣由了~



咱們再來看看 docker_default 網絡

$ docker network inspect docker_default

[

{
    "Name": "docker_default",
    "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
    "Created": "2020-12-16T11:03:37.2152073+08:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
        "Driver": "default",
        "Options": null,
        "Config": [
            {
                "Subnet": "172.24.0.0/16",
                "Gateway": "172.24.0.1"
            }
        ]
    },
    "Internal": false,
    "Attachable": true,
    "Ingress": false,
    "ConfigFrom": {
        "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
        "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
            "Name": "react_blog_admin",
            "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
            "MacAddress": "02:42:ac:18:00:02",
            "IPv4Address": "172.24.0.2/16",
            "IPv6Address": ""
        },
        "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
            "Name": "react_blog_blog",
            "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
            "MacAddress": "02:42:ac:18:00:04",
            "IPv4Address": "172.24.0.4/16",
            "IPv6Address": ""
        },
        "83005eec8d50071a6c23a2be4af8552983c09c532e937f04d79f02f8eb68acc9": {
            "Name": "react_blog_mysql",
            "EndpointID": "265ed7793c98287a05ccf8997e81671287a02ee8ea464984996083a34abe10dd",
            "MacAddress": "02:42:ac:18:00:03",
            "IPv4Address": "172.24.0.3/16",
            "IPv6Address": ""
        },
        "937339a37ce726e704ec21b31b4028a97967a00de01438557e5a60d8538a51c8": {
            "Name": "react_blog_service",
            "EndpointID": "934d26f32a2b23e2cb4691020cb93d26c97b9647108047b492c3f7dd2be6faef",
            "MacAddress": "02:42:ac:18:00:05",
            "IPv4Address": "172.24.0.5/16",
            "IPv6Address": ""
        }
    },
    "Options": {},
    "Labels": {
        "com.docker.compose.network": "default",
        "com.docker.compose.project": "docker",
        "com.docker.compose.version": "1.25.1"
    }
}

]

能夠看到 react_blog_service 也已正常加入網絡,IP 爲 172.24.0.5



#### 4.2 自定義 Network

docker_default 網絡是根據目錄來建立的,恰巧咱們這幾個項目 docker-compose.yml 文件都放在 docker 目錄下,因此都在一個網絡,若是名稱變了就不在一個網絡,而且以後項目可能還會有 docker 目錄,所有都在一個網絡也是不太好的,因此這裏咱們來自定義本次項目的網絡。

***blog/docker/docker-compose.yml***

version: '3'
services:
blog:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:blog
ports:
  - 9000:9000
networks: 
  - react_blog
container_name: react_blog_blog

networks:
react_blog:

***admin/docker/docker-compose.yml***

version: '3'
services:
admin:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:admin
ports:
  - 9001:80
volumes:
  - ../build:/www
  - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
  - react_blog
container_name: react_blog_admin

networks:
react_blog:

***service/docker/docker-compose.yml***

version: '3'
services:
service:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
depends_on:
  - db
environment:
  - MYSQL_HOST=react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接
  - MYSQL_USER=root
  - MYSQL_PASSWORD=8023
volumes:
  - ./article:/usr/src/app/article
networks:
  - react_blog
container_name: react_blog_service

db:

image: mysql
ports:
  - 33061:3306
command: --default-authentication-plugin=mysql_native_password
environment:
  - MYSQL_ROOT_PASSWORD=8023
  - MYSQL_USER=root
  - MYSQL_PASSWORD=8023
  - MYSQL_DATABASE=react_blog
networks:
  - react_blog
container_name: react_blog_mysql

networks:
react_blog:

- 與services 同級的 networks:建立一個新的 network,這裏生成的 network 最終名稱也會加上目錄名,docker_react_blog。
- 服務內部的 networks:加入哪些網絡,參數帶 「-」 說明是數組,能夠加入多個網絡,這裏咱們所有加入 react_blog,不分先後端了

**注意:**

這樣在 dockor-compose.yml 裏生成的 network 都會加上當前目錄名,若想不帶,能夠本身先生成一個

$ docker network create my_net

而後在 dockor-compose.yml 裏

version: '3'
services:
service:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
depends_on:
  - db
environment:
  - MYSQL_HOST=react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接
  - MYSQL_USER=root
  - MYSQL_PASSWORD=8023
volumes:
  - ./article:/usr/src/app/article
networks:
  - my_net
container_name: react_blog_service

db:

image: mysql
ports:
  - 33061:3306
command: --default-authentication-plugin=mysql_native_password
environment:
  - MYSQL_ROOT_PASSWORD=8023
  - MYSQL_USER=root
  - MYSQL_PASSWORD=8023
  - MYSQL_DATABASE=react_blog
networks:
  - my_net
container_name: react_blog_mysql

networks:
my_net:

external: true
加個 external 參數則使用已經建立的 network(my_net),不會再去建立或加上目錄名。



咱們再來從新建立容器,先刪除所有容器

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

在進入各個目錄分別執行 `docker-compose up -d`,在運行第一個時會看到 `Creating network "docker_react_blog" with the default driver` 這句話,說明建立了一個新的 network,咱們來看看

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a75e040b03ed bridge bridge local
13545e6a3970 docker_default bridge local
e1ceb437a4fd docker_react_blog bridge local
5ec462838a1c host host local
c726e6887f10 none null local

$ docker network inspect docker_react_blog
[

{
    "Name": "docker_react_blog",
    "Id": "e1ceb437a4fdc5de91e51ff8831e21b565c92754159ad7057de36758e548a92f",
    "Created": "2020-12-19T01:39:02.201644444+08:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
        "Driver": "default",
        "Options": null,
        "Config": [
            {
                "Subnet": "172.18.0.0/16",
                "Gateway": "172.18.0.1"
            }
        ]
    },
    "Internal": false,
    "Attachable": true,
    "Ingress": false,
    "ConfigFrom": {
        "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
        "00da404f6f050b9b2f20e39bbb136fef614e8dfee85ec31bd6000bfd59cc2dab": {
            "Name": "react_blog_mysql",
            "EndpointID": "1cb966cc731eca3e9721e6d3edcfcac6152b66051faa934557f567e9e36c75c6",
            "MacAddress": "02:42:ac:12:00:04",
            "IPv4Address": "172.18.0.4/16",
            "IPv6Address": ""
        },
        "ad1480e48e8e7ed160b1d4bcf7eed77d74505aea7581d48d8931206772b5d805": {
            "Name": "react_blog_service",
            "EndpointID": "8866c3457382d6baa945da09aef40da54c7dfdea0f393485001c35bb37d201a0",
            "MacAddress": "02:42:ac:12:00:05",
            "IPv4Address": "172.18.0.5/16",
            "IPv6Address": ""
        },
        "b518d40b5021d3fdec7b7e62fbaa47b8a705a38346ccba2b9814174e46b67cd0": {
            "Name": "react_blog_admin",
            "EndpointID": "9a58ff20dc57d4d1fa6af83482051a68e80e22a5e37cf8e0cb3570b78102f107",
            "MacAddress": "02:42:ac:12:00:03",
            "IPv4Address": "172.18.0.3/16",
            "IPv6Address": ""
        },
        "db0050257a8e8a0fa430ea04b009ae819dbf04ef001cf1027ec2b5565403b48e": {
            "Name": "react_blog_blog",
            "EndpointID": "664794ed292871bc7fd8e1c4eaa56f682a6be5d653209f84158f3334a4f30660",
            "MacAddress": "02:42:ac:12:00:02",
            "IPv4Address": "172.18.0.2/16",
            "IPv6Address": ""
        }
    },
    "Options": {},
    "Labels": {
        "com.docker.compose.network": "react_blog",
        "com.docker.compose.project": "docker",
        "com.docker.compose.version": "1.25.1"
    }
}

]

#### 4.3 調用接口

如今還有一個問題,咱們在代碼中調用接口形式是 `http://localhost:9002/api/xxx` ,在 react_blog_blog 容器中調用接口 localhost 是自己本身,沒有調到 react_blog_service 裏面的接口。



**針對 admin**

在代碼中,咱們這樣來調接口

const HOST = process.env.NODE_ENV === 'development' ? 'http://localhost:9002' : ''

const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
addArticle: HOST + '/api/addArticle',
delArticle: HOST + '/api/delArticle',

getTagList: HOST + '/api/getTagList',
addTag: HOST + '/api/addTag',
delTag: HOST + '/api/delTag',

register: HOST + '/api/register',
login: HOST + '/api/login',
}

export default API

![image-20201219160058561](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-08.png)



會發現接口 404,咱們經過 nginx 來代理接口請求

***admin/docker/nginx.conf***

server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;

location /api {

proxy_pass http://react_blog_service:9002;

}

location / {

root /www;
index index.html;

}
}

以 /api 爲開頭的請求,咱們都轉發到 react_blog_service 容器 9002 端口,將 nginx.conf 拖到服務器,由於咱們是將此文件掛載到容器內部的,因此這裏只須要重啓一下容器

$ docker restart react_blog_admin

再看看請求接口,能夠看到請求 200 成功,返回數據,若是返回 500,說明數據庫還沒建表,將目錄下 react_blog.sql 導入數據庫就能夠了。

![image-20201219160458357](https://cloud-images-1255423800.cos.ap-guangzhou.myqcloud.com/Docker-09.png)



**針對 blog**

開始我覺得經過環境變量(Next 中要存儲在運行時變量裏 [Runtime Configuration](https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration))來傳遞請求 HOST (react_blog_service || localhost) ,但發現 react_blog_service 直接拼在前端接口裏訪問是不可行的(getServerSideProps 可行),因此最後仍是改成 nginx 來代理請求,而且後面咱們確定仍是要經過域名來訪問網站的,因此仍是須要 nginx,那麼咱們就爲前臺頁面來加一個 nginx 容器。

**一、建立環境變量**

***blog/docker/Dockerfile***

node 鏡像

apline 版本的node會小不少

FROM node:12-alpine

在容器中建立目錄

RUN mkdir -p /usr/src/app

指定工做空間,後面的指令都會在當前目錄下執行

WORKDIR /usr/src/app

拷貝 package.json

COPY package.json /usr/src/app

安裝依賴

RUN npm i --production --registry=https://registry.npm.taobao.org

拷貝其餘全部文件到容器(除了 .dockerignore 中的目錄和文件)

COPY . /usr/src/app

build 階段獲取

ENV HOST react_blog_service ## 增長一個環境變量,在 build 階段可獲取到,必定放在 npm run build 前一行

build

RUN npm run build

暴露端口 9000

EXPOSE 9000

運行容器時執行命令,每一個 Dokcerfile 只能有一個 CMD 命令,多個的話只有最後一個會執行

CMD [ "npm", "start" ]

代碼中,設置運行是變量 ***blog/next.config.js***

const withCSS = require('@zeit/next-css')
const withLess = require('@zeit/next-less')
module.exports = () =>
withLess({

...withCSS(),
// 改成 nginx 代理
publicRuntimeConfig: {
  HOST: process.env.HOST || 'localhost', // 若是是 docker build,此處 process.env.HOST,不然就 localhsot,不影響本地運行
},

})

在 ***blog/config/api***

import getConfig from 'next/config'
const { publicRuntimeConfig } = getConfig()

const SSRHOST = http://${publicRuntimeConfig.HOST}:9002
const HOST = http://localhost:9002

export const SSRAPI = {
getArticleList: SSRHOST + '/api/getArticleList',
getArticle: SSRHOST + '/api/getArticle',
}

export const API = {
getArticleList: HOST + '/api/getArticleList',
getArticle: HOST + '/api/getArticle',
}

這裏有點麻煩,我不知道個人理解對不對,但試了多種狀況只有這種本地和 docker 部署才均可以。

- 若是是本地運行(不使用 docker),服務端獲取數據(**getServerSideProps**)和頁面中獲取數據直接使用服務接口地址(localhost:9002)便可

- 若是是 docker 運行,服務端獲取數據(**getServerSideProps**)須要直接帶上服務接口容器地址,沒法經過 nginx 代理,頁面中獲取數據調用接口則職能經過 nginx 代理的方式

**二、nginx 代理**

修改 ***blog/docker/docker-compose.yml***,增長一個 nginx 容器

version: '3'
services:
blog:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:blog
# ports:
#   - 9000:9000
networks:
  - react_blog
container_name: react_blog_blog

nginx:

build:
  context: ../
  dockerfile: ./docker/Dockerfile-nginx
image: react_blog:nginx
ports:
  - 9000:80
volumes:
  - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
  - react_blog
container_name: react_blog_nginx

networks:
react_blog:

***blog/docker/Dockerfile-nginx***

FROM nginx

刪除 Nginx 的默認配置

RUN rm /etc/nginx/conf.d/default.conf

EXPOSE 80

***blog/docker/nginx.conf***

server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;

location /api {

proxy_pass http://react_blog_service:9002;

}

location / {

proxy_pass http://react_blog_blog:9000;

}
}

**三、生成容器**

由於 blog 的內容有變,因此須要從新生成鏡像,使用 `docker-compose up -d --build` 會從新下載 npm node_modules,比較慢,因此仍是先生成鏡像。

在 blog 目錄下執行

$ docker build -f docker/Dockerfile . -t react_blog:blog

在 blog/docker 下執行

$ docker-compose up -d

運行成功的話,再試試接口就能夠獲取數據啦。



### 5. 鏈接宿主機 Mysql

上面遇到一個問題,在上面過程當中,個人服務器(宿主機)上的 Mysql 出現了問題,鏈接時報錯 `2013 lost connection to mysql server at 'reading initial communication packet'`,我也不知道是什麼緣由引發的,解決方式是運行命令 `systemctl start mysqld.service` 啓動 Mysql 服務,也不知是哪理影響到了。由於以前其餘項目都是單獨部署的,沒使用 docker,數據都在宿主機 Mysql 上,因此我仍是跟傾向於統一管理,自適應宿主機一個 Mysql,下面來看看怎麼實現吧。

這裏有兩種方式

**方式一:network_mode: host**

修改 ***service/docker/docker-compose.yml***

version: '3'
services:
service:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
restart: on-failure
# depends_on:
#   - db
environment:
  # MYSQL_HOST: react_blog_mysql # 此處 localhost 換爲 mysql 容器名,在同一個自定義網絡下,變會自動解析爲 IP 鏈接
  MYSQL_USER: root
  MYSQL_PASSWORD: 8023
volumes:
  - ../article:/usr/src/app/article
network_mode: host
# networks:
#   - react_blog
container_name: react_blog_service

# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:

service/docker 下執行命令

$ docker-compose up -d

$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af9e525e7d14 react_blog:service "docker-entrypoint.s…" 28 seconds ago Up 26 seconds react_blog_service

能夠看到 service 運行正常,且沒有端口映射,`docker inspect react_blog_service` 也沒有分配 IP,這種就至關於一個 Node 應用本身鏈接到宿主機 Mysql。可是對於頁面接口請求來講,由於 react_blog_service 已不在 docker_react_blog ,因此就要使用宿主機 IP 地址來訪問了。

***nginx.conf***

server {
listen 80;
sendfile on;
sendfile_max_chunk 1M;
tcp_nopush on;
gzip_static on;

location /api {

# proxy_pass http://react_blog_service:9002;
proxy_pass http://xxx.xx.xxx.x:9002; # xxx.xx.xxx.x 爲宿主機(服務器)IP

}

location / {

proxy_pass http://react_blog_blog:9000;

}
}

服務端渲染接口也是同樣

node 鏡像

apline 版本的node會小不少

FROM node:12-alpine

在容器中建立目錄

RUN mkdir -p /usr/src/app

指定工做空間,後面的指令都會在當前目錄下執行

WORKDIR /usr/src/app

拷貝 package.json

COPY package.json /usr/src/app

安裝依賴

RUN npm i --production --registry=https://registry.npm.taobao.org

拷貝其餘全部文件到容器(除了 .dockerignore 中的目錄和文件)

COPY . /usr/src/app

build 階段獲取,xxx.xx.xxx.x 爲宿主機(服務器)IP

ENV HOST xxx.xx.xxx.x

build

RUN npm run build

暴露端口 9000

EXPOSE 9000

運行容器時執行命令,每一個 Dokcerfile 只能有一個 CMD 命令,多個的話只有最後一個會執行

CMD [ "npm", "start" ]

這種方式是否是很麻煩,還要暴露服務器 IP 地址,因此我選擇方式二



**方式二:**

修改 ***service/docker/docker-compose.yml***

version: '3'
services:
service:

build:
  context: ../
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
restart: on-failure
# depends_on:
#   - db
environment:
  MYSQL_HOST: 172.17.0.1
  MYSQL_USER: root
  MYSQL_PASSWORD: 8023
volumes:
  - ../article:/usr/src/app/article
networks:
  - react_blog
container_name: react_blog_service

# db:
# image: mysql
# ports:
# - 33061:3306
# restart: on-failure
# command: --default-authentication-plugin=mysql_native_password
# environment:
# MYSQL_ROOT_PASSWORD: 8023
# MYSQL_PASSWORD: 8023
# MYSQL_DATABASE: react_blog
# networks:
# - react_blog
# container_name: react_blog_mysql
networks:
react_blog:

這裏 MYSQL_HOST 爲 172.17.0.1,上面也說了,容器能夠經過此 IP 來鏈接到宿主機,因此這就鏈接上宿主機的 Mysql 了,其餘的地方就不須要改了。



### 6. 一個 docker-compoer.yml

前面用了 3 個 docker-compose.yml 來啓動各自的項目,仍是挺繁瑣的,咱們來寫一個彙總的,一個命令運行因此,固然後面某一個項目須要從新跑,也能夠進入各自目錄去運行本身的 docker-compose.yml

在項目根目錄建立 ***docker/docker-compose.yml***,建立 docker 目錄,是爲了建立的 network 和單個項目運行是建立的一致

version: '3'
services:
blog:

build:
  context: ../blog
  dockerfile: ./docker/Dockerfile
image: react_blog:blog
networks:
  - react_blog
container_name: react_blog_blog

nginx:

build:
  context: ../blog
  dockerfile: ./docker/Dockerfile-nginx
image: react_blog:nginx
ports:
  - 9000:80
volumes:
  - ../blog/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
  - react_blog
container_name: react_blog_nginx

admin:

build:
  context: ../admin
  dockerfile: ./docker/Dockerfile
image: react_blog:admin
ports:
  - 9001:80
volumes:
  - ../admin/build:/www
  - ../admin/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
networks:
  - react_blog
container_name: react_blog_admin

service:

build:
  context: ../service
  dockerfile: ./docker/Dockerfile
image: react_blog:service
ports:
  - 9002:9002
restart: on-failure
environment:
  MYSQL_HOST: 172.17.0.1
  MYSQL_USER: root
  MYSQL_PASSWORD: 8023
volumes:
  - ../service/article:/usr/src/app/article
networks:
  - react_blog
container_name: react_blog_service

networks:
react_blog:

中止並刪除以前建立的全部容器

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

進入 /docker 目錄執行,

$ docker-compose up -d
Building nginx
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Running in bb163c42c6b5
Removing intermediate container bb163c42c6b5
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Running in 9b77ebd39952
Removing intermediate container 9b77ebd39952
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:nginx
WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use docker-compose build or docker-compose up --build.
Building admin
Step 1/3 : FROM nginx
---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
---> Using cache
---> 282cb303dddf
Step 3/3 : EXPOSE 80
---> Using cache
---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:admin
WARNING: Image for service admin was built because it did not already exist. To rebuild this image you must use docker-compose build or docker-compose up --build.
Creating react_blog_admin ... done
Creating react_blog_service ... done
Creating react_blog_blog ... done
Creating react_blog_nginx ... done

$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1fbb15abdd30 react_blog:service "docker" 13 seconds ago Up 6 seconds 0.0.0.0:9002->9002/tcp react_blog_service
fbee53e25c3a react_blog:admin "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9001->80/tcp react_blog_admin
70cb25f87d14 react_blog:blog "docker" 13 seconds ago Up 6 seconds 9000/tcp react_blog_blog
aa9fbf2afea4 react_blog:nginx "/docker" 13 seconds ago Up 6 seconds 0.0.0.0:9000->80/tcp react_blog_nginx

運行成功~



### 7. 域名

我如今是經過宿主機的 nginx 來代理域名訪問 IP:9000,而後訪問到 react_blog_nginx 容器,本想是直接在 react_blog_nginx 中作代理,可是試了沒成功。想了想,訪問 react_blog_nginx 是經過端口映射,宿主IP:9000 訪問到的,若是在 react_blog_nginx 內部配置域名,總感受是沒法訪問,這點還沒想過,這幾天再試試。



## 結語

終於寫完了,寫以前已經學習嘗試了很久,覺得頗有把握了,結果在寫的過程當中又遇到一堆問題,一個問題可能都會卡很久天,各類百度,Google,油管都用上啦,總算解決了遇到的全部問題,固然這些問題可能只知足了我如今的部署需求,其中還有不少知識點,沒有接觸到,不過不要緊,我就是想成功部署前端項目就能夠了。
相關文章
相關標籤/搜索