以前使用 Vue 全家桶开发了我的博客,并部署在阿里云服务器上,最近在学习 React,因而使用 React 开发重构了本身的博客。css
主要技术栈以下:html
React 搭建博客先后台部分,这里不会细讲,只会说说中间遇到的一些问题和一些解决方法,具体开发教程可参考 React Hooks+Egg.js实战视频教程-技术胖Blog开发。前端
部署部分这里会是重点讲解,由于也是第一次接触 Docker,这里只记录本身的学习心得,有不对的地方还请多多指教。vue
刚好本次项目里前台页面是 node 运行,后台界面是静态 HTML,服务接口须要链接 Mysql,我以为 Docker 来部署这几种状况也是比较全面的例子了,能够给后来同窗做为参考,内容比较啰嗦,但愿能帮助后来的同窗少走一点坑,由于有些是本身的理解,可能会有错误,还请你们指正,互相学习。node
源码地址:https://github.com/Moon-Futur...mysql
clone 下来参照目录哦~react
博客前台使用 Next.js 服务端渲染框架搭建,后台管理界面使用 create-react-app 脚手架搭建,服务接口使用 Egg 框架(基于 Koa)。后台管理和服务接口没什么好说的,就是一些 React 基础知识,这里主要说下 Next.js 中遇到的一些问题。linux
项目目录:blog:前台界面,Next.jsios
admin:后台管理界面,create-react-app 脚手架搭建nginx
service:先后台服务接口
由于是服务端渲染,因此页面初始数据会在服务器端获取后,渲染页面后返回给前端,这里有两个官方 API,getStaticProps
,getServerSideProps
,从名字能够稍微看出一点区别。(Next.js 9.3 版本以上,使用 getStaticProps
或 getServerSideProps
来替代 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 捕获异常,防止获取数据失败或者后端接口报错,服务端渲染错误返回不了页面。
还有一些数据,咱们并不但愿在服务端获取渲染到页面里,而是但愿页面加载后再操做。
使用 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
第二个参数,表明是否执行的依赖。
使用 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
页面进入、退出动画找到一个比较好用的库 framer-motion, https://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> ) }
在 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 去监听路由切换状态,这里也能够自定义加载状态。
在 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 都引入一下,可是我试了下,仍是不行,不过好在这种状况只在开发模式下,生产模式下没什么问题,因此也就没在折腾了,就这样刷新一下吧。
在 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 效果。
本来项目中使用的是 sass,但在后面 docker 部署安装依赖时,实在时太慢了,还各类报错,以前也是常常遇到,因此索性直接换成了 less,语法也差很少,安装起来省心多了。
Docker 是一个开源的应用容器引擎,可让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,而后发布到任何流行的 Linux 机器上,也能够实现虚拟化。
容器是彻底使用沙箱机制,相互之间不会有任何接口(相似 iPhone 的 app),更重要的是容器性能开销极低。
对我而言,由于如今使用的是阿里云服务器,部署了好几个项目,若是服务器到期后,更换服务器的话,就须要将全部项目所有迁移到新服务器,每一个项目又要去依次安装依赖,运行,nginx 配置等等,想一想都头大。而使用 Docker 后,将单个项目与其依赖打包成镜像,镜像能够在任何 Linux 中生产一个容器,迁移部署起来就方便多了。
其余而已,使用 Docker 可让开发环境、测试环境、生产环境一致,而且每一个容器都是一个服务,也方便后端实现微服务架构。
Docker 安装最好是参照官方文档,避免出现版本更新问题。https://docs.docker.com/engine/install/ 英文吃力的,这两推荐一款神奇词典 欧陆词典,哪里不会点哪里,谁用谁说好。
Mac 和 Windows 都有客户端,能够很简单的下载安装,另外 Window 注意区分专业版、企业版、教育版、家庭版
由于我这里使用的是阿里云 Centos 7 服务器,因此简单介绍一下在 Centos 下的安装。
首先若已经安装过 Docker,想再装最新版,先协助旧版
$ sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
有三种安装方式:
这里选择官方推荐的第一种方式安装 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
Docker 把应用程序及其依赖,打包在 image 文件里面。只有经过这个文件,才能生成 Docker 容器(Container)。image 文件能够看做是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,能够生成多个同时运行的容器实例。
image 文件是通用的,一台机器的 image 文件拷贝到另外一台机器,照样可使用。通常来讲,为了节省时间,咱们应该尽可能使用别人制做好的 image 文件,而不是本身制做。即便要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制做。
官方有个镜像库 Docker Hub,不少环境镜像均可以从上面拉取。
$ docker images
或者
$ docker image ls
刚安装完 docker,是没有任何镜像的
$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE
只查看所有镜像 id
$ docker images -q # 或 $ docker image ls -q
这里咱们尝试从官方库下载一个 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
删除镜像可使用以下命令
$ 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}')
上面咱们下载了 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.
上面大意是肯定构建上下文后,中间的一些文件操做就只能在当前上下文之间进行,有两种方式解决
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
依然将 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 时在稍做说明。
上面生成了 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 镜像,若是以前没下载,这里就会去下载。
项目运行在容器内,咱们须要经过一个镜像建立一个容器。
$ 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
这里咱们经过 admin:v1 来生成一个容器
$ docker create -p 9001:80 --name admin 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
删除容器可使用以下命令
$ 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)
运行容器时若是失败,能够查看日志定位错位
$ docker logs admin
容器就像一个文件系统,咱们也能够进去查看里面的文件,使用如下命令进入容器内部
$ docker exec -it admin /bin/sh
-i
参数让容器的标准输入持续打开,--interactive-t
参数让 Docker 分配一个伪终端,并绑定到容器的标准输入上, --tty进入容器内部后,可使用 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 拷贝过来的文件
经过上面能够发现每次制做镜像,生成容器,运行容器,都要输入不少命令,实在是很不方便,若是只要一个简单的命令就能完成就行了,docker-compose 就能够实现,固然,这只是它很小的一部分功能。
官方简介以下:
Compose 是用于定义和运行多容器 Docker 应用程序的工具。经过Compose,您可使用 YAML 文件来配置应用程序的服务。而后,使用一个命令,就能够从配置中建立并启动全部服务。
使用Compose基本上是一个三步过程:
- 使用 Dockerfile 定义应用程序的环境,以即可以在任何地方复制它。
- 在 docker-compose.yml 中定义组成您的应用程序的服务,以便它们能够在隔离的环境中一块儿运行。
- 运行 docker-compose up,而后 Compose 启动并运行整个应用程序。
参考官方文档 Install Docker Compose ,这里简单介绍 Linux 安装
运行命令
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
添加可执行权限
sudo chmod +x /usr/local/bin/docker-compose
检查是否安装完成
docker-compose --version
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/,官网能够详解,这里以及后面只说说用到的一些配置。
services:服务组
admin:服务名称,惟一,多个 docker-compose.yml 有相同名称的,下面的容器会覆盖
将 docker-compose.yml 放入 docker 目录下
├─admin └─build └─index.html └─docker └─Dockerfile └─docker-compose.yml
在 docker 目录下运行
$ docker-compose up -d --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
上面简单介绍了 docker 的一些用法,借用静态 HTML 文件与 nignx 镜像建立运行了一个容器,可是其远远不止这些,下面就经过部署本博客来做为例子再探探里面的一些知识点。
源码地址:https://github.com/Moon-Futur...,可下载下来看着目录更清晰。
为了统一维护 docker 文件,如下将 docker 相关文件都放在各自目录下 docker 文件下,因此要特别主题构建上下文(context)的肯定。
端口映射 9000:9000,服务器端口:容器端口,如果线上服务器,要先在安全组里开通对应的端口号
在 blog 目录下建立 docker 目录,docker 目录下建立三个文件
.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 生成的文件
看看生成的镜像
$ 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。你们能够试试,以后再删除容器就能够了。
以上容器运行成功的话,在浏览器经过 服务器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 build
,docker run
等命令先生成容器,再生成并运行容器,是否是有点繁琐,命令很差记,输入也麻烦,这里咱们就能够利用 docker-compose 来简化执行命令。
咱们看一下文件内容:
docker-compose
命令路径要和 docker-compose.yml 同一路径,因此这里 context 构建上下文选择上一层源码目录,dockerfile 就是当前目录里的 Dockerfile当前目录_服务名_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 前端页面部署好了,如今只是单独部署学习,后面会删了和后台与接口一块儿部署。
端口映射 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
注意这里和上面的一些区别,
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 下的类容就会是最新的,而不须要一次次的去执行 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 记得和上面挂在目录相同
端口映射 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 服务是后端接口:
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 来链接   注意这里的端口 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 地址去链接。  查看 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`,因此这里的状况是这样的  上面说了默认网络 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
 能够看到服务容器已正常运行,`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
 会发现接口 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 导入数据库就能够了。  **针对 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***
FROM node:12-alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app
RUN npm i --production --registry=https://registry.npm.taobao.org
COPY . /usr/src/app
ENV HOST react_blog_service ## 增长一个环境变量,在 build 阶段可获取到,必定放在 npm run build 前一行
RUN npm run build
EXPOSE 9000
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
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;
}
}
服务端渲染接口也是同样
FROM node:12-alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app
RUN npm i --production --registry=https://registry.npm.taobao.org
COPY . /usr/src/app
ENV HOST xxx.xx.xxx.x
RUN npm run build
EXPOSE 9000
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,油管都用上啦,总算解决了遇到的全部问题,固然这些问题可能只知足了我如今的部署需求,其中还有不少知识点,没有接触到,不过不要紧,我就是想成功部署前端项目就能够了。