用Docker搭建LNMP

Posted justlikeheaven

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Docker搭建LNMP相关的知识,希望对你有一定的参考价值。

程序员经常会说的一句话:在我的机器上是正常的,肯定是你的机器有问题。因此,Docker诞生了,它把应用所需要的一切东西都打包,从而可以很方便地进行部署。

Docker 的主要用途,目前有三大类:

  1. 提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
  2. 提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
  3. 组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

下面开始来体验Docker,操作系统为win10,Docker的安装请参考官方文档,这里略过。

Hello World

官方提供了一个程序员最喜欢的hello world镜像,我们就以此来开始Docker之旅,感受Docker的强大之处吧:

docker run hello-world

如果你看到输出Hello from Docker!,恭喜你,Docker安装成功了。

image和container

镜像和容器可以理解为面向对象里面的类和对象。

上面的例子是直接拿官方的镜像来跑,我们也可以在官方镜像的基础上加工,来构建新的镜像,当然也可以不基于任何镜像来构建。

所有内容(代码、软件、环境等)都可以通过Dockerfile配置来构建image,通过image可以运行具体的container实例。

查看镜像列表:docker image ls

查看容器列表:docker container ls

如果要查看完整的列表,在ls后面加个-a选项就好了:docker container ls -a

装个Linux

下面加点难度,装个Linux,alpine体积小,很多开源镜像也是以它为基础(比如nginx),很适合用来体验:

docker run -it --rm alpine

发现run后面多了几个选项,其中-it其实是-i -t的缩写,即interactive terminal,交互式终端的意思,不加这个我们是进不到容器的交互式终端的。

加了--rm,表示在容器退出后会自动删除容器,我们这里是为了体验,希望用完之后不要占用磁盘空间,所以加了删除选项,注意这里不是删除镜像,删除的是容器。

现在我们进入到alpine系统的终端了,输入命令查看系统版本试试:

cat /etc/os-release

体验完毕,这时候只要输入exit按下回车就可以退出容器了。

如果想运行容器的时候直接输出系统版本,可以这么玩:

docker run --rm alpine cat /etc/os-release

在镜像名后面直接跟上你想运行的命令,输出系统版本信息以后容器就自动关闭并删除了。

运行php

好了,现在Linux系统装好了,直觉告诉我,如果要跑PHP,我们就直接进到刚刚那个alpine容器里面安装就好了,那么不好意思,你的姿势错了。先不解释,先看看正确姿势:

我们先在CLI模式下体验PHP,在这之前,我们可以看看官方的php-cli:7.2镜像的Dockerfile是怎么定义的:

FROM debian:stretch-slim
...此处省略若干行...

FROM指定基于哪个镜像,也可以不基于任何镜像:FROM scratch,比如官方的alpine的Dockerfile

FROM scratch
ADD alpine-minirootfs-3.10.0-x86_64.tar.gz /
CMD ["/bin/sh"]

由此可见,PHP镜像是建立在debian这个镜像的基础上的,所以我们不需要像在虚拟机上面装PHP一样,得先用iso安装一个Linux操作系统。

项目位置在D:\www\php-cli-demo,建立好文件夹以后,在项目下面新建一个index.php文件:

<?php
echo phpversion();

再新建一个Dockerfile文件:

FROM php:7.2-cli
WORKDIR /var/www/php-cli-demo
COPY . .
CMD ["php", "index.php"]

WORKDIR是指定容器的工作目录,我们需要把当前项目的代码拷贝到容器的操作系统所在的目录里,所以我们使用COPY,第一个点的意思是当前目录,第二个点的意思是容器的当前目录,意思就是把我win10系统下D:\www\php-cli-demo这个文件夹下的所有文件全部拷贝到容器的Debian系统下的当前目录(已通过工作目录指定为/var/www/php-cli-demo)下。

CMD就是容器运行起来以后所要执行的命令了,这里我们希望直接运行这个php文件。

在当前文件夹下,按住Shift键,然后按下鼠标右键,出现右键菜单,这时候出现其中一项“在此处打开Powershell窗口(s)”,这时候我们只需要按下s就可以进入命令行并且直接定位到当前文件夹了。

然后我们根据刚才的Dockerfile配置文件来建立镜像:

docker build -t php-cli-demo .

t是tag,打标签的意思。

最后那个点,指定了要打包的是哪个目录,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件,然后Docker引擎在解析上面的COPY指令时,就能知道要COPY哪些文件了,所以这个COPY的第一个参数的路径是在打包路径下展开的,是相对路径。

Docker默认会去找Dockerfile,也可以是别的文件名,例如你的Dockerfile叫build.conf,可以通过-f参数指定:

docker build -t php-cli-demo -f build.conf .

好了,镜像构建成功了,现在马上来跑一下吧:

docker run --rm php-cli-demo

可以看到正确输出了PHP的版本号为:7.2.19。

build的时候把项目代码一块打包进image镜像文件了,那如果我要运行另外一个项目呢?要重新build一遍吗?其实这个image是可以复用的,只不过在运行的时候指定项目位置就行了:

docker run --rm -v $PWD:/var/www/php-cli-demo php-cli-demo

v是volume,数据卷的意思,$PWD是宿主机当前目录,冒号后面是容器目录,意思就是把当前目录挂载到容器的/var/www/php-cli-demo目录。

不过不建议这么做,因为镜像是一层一层做缓存的,所以重复build不会占用太多空间,而且如果镜像做了改动,会影响到另外一个容器,所以还是单独build比较好。感兴趣的朋友可以自己做一下测试,然后用docker system df查看空间占用情况。

如果像上面这种在基础镜像上,没有涉及到任何加工的,只需要把项目映射到容器就行了,可以直接这么玩:

docker run --rm -v $PWD:/var/www/demo -w /var/www/demo php:7.2-cli php index.php

现在,回过头来分析刚才的问题,为什么不建议在alpine容器里面直接安装PHP。

镜像构建时,会一层一层地构建,前一层是后一层的基础,这样可以达到复用的效果。

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。

镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层。

容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。

所以,你在alpine容器里面直接安装PHP,如果不小心把容器删掉了,下一次再跑的时候,PHP就丢了。而且这是一个黑箱操作,别人并不知道这个容器里面有什么,所以最好还是通过Dockerfile构建镜像,然后再根据镜像来跑容器咯。

搭建LNMP

好了,上面的都搞明白之后我们就来跑个真正的PHP项目吧。我们需要用到PHP-FPM,Nginx,mysql。项目结构是这么安排的:

  • docker-php-demo
    • app
      • index.html
      • index.php
    • mysql
      • Dockerfile
    • nginx
      • conf.d
        • site.conf
    • php
      • Dockerfile

我们先把Nginx装起来,跑个静态页面试试。直接跑官方镜像:

docker run --name nginx -d --rm nginx:1.16

打开浏览器输入localhost,就可以看到Welcome to nginx!的静态页了。这是Nginx的默认页面,我们需要加一个专门针对本次项目的配置。进入Nginx容器,查看配置文件路径:

docker exec -it nginx bash
nginx -V

看到配置文件路径为:

--conf-path=/etc/nginx/nginx.conf

查看配置文件:

cat /etc/nginx/nginx.conf

可以看到默认的配置还会去加载其他配置文件:

include /etc/nginx/conf.d/*.conf;

所以我们只需要把项目的配置通过-v映射到容器的conf.d目录下就可以了。把当前的Nginx容器停掉:

docker stop nginx

然后编写site.conf:

server 
    listen 80;
    server_name local.docker-php-demo.com;
    root /var/www/docker-php-demo;

在app目录放入index.html:

Hello Docker

因为Nginx本身我们不做加工,所以不用Dockerfile,直接用官方镜像就好了,然后分别把项目和Nginx网站配置都映射过去,在项目根目录下执行:

docker run --name dpd-nginx -d -v $PWD/app:/var/www/docker-php-demo -v $PWD/nginx/conf.d:/etc/nginx/conf.d -p 80:80 nginx:1.16

最后在本机配置hosts就大功告成啦,Win + R 打开运行窗口,输入drivers,打开etc/hosts文件加入一行域名配置:

127.0.0.1 local.docker-php-demo.com

浏览器打开local.docker-php-demo.com,到这里,我们就完成了让Nginx跑静态页面的配置了。我们的项目是PHP,把fpm装起来,然后再让Nginx连上就行了,在php目录下编写Dockerfile:

FROM php:7.2-fpm
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini     && docker-php-ext-install pdo_mysql

上面我们使用了镜像提供的安装扩展的快捷操作方法来安装pdo_mysql扩展。

在app目录放入index.php:

<?php
phpinfo();

构建运行:

cd php
docker build -t dpd-php .
cd ..
docker run --name dpd-php -d -v $PWD/app:/var/www/docker-php-demo -p 9000:9000 dpd-php

把php和nginx通过网卡连接起来:

docker network create --driver bridge dpd # 新建一个桥接网卡,名字叫dpd
docker network connect dpd dpd-php
docker network connect dpd dpd-nginx
docker network ls # 列出docker当前有哪些网卡
docker network inspect dpd # 查看dpd网卡详情

修改Nginx配置,把PHP脚本转发给fpm,fpm地址通过容器名dpd-php就可以识别:

server 
    listen 80;
    server_name local.docker-php-demo.com;
    root /var/www/docker-php-demo;
    
    location ~ \.php$ 
        fastcgi_pass dpd-php:9000;
        include fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
    

让Nginx重新加载配置文件:

docker exec -it dpd-nginx nginx -s reload

到这里,我们就把Nginx 和 php-fpm搭起来了。下面我们再来搭建Mysql,在mysql目录下新建test.sql,存放表结构和基础数据:

CREATE TABLE `user` (
    `id` int(10) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `name` varchar(20) NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户';

INSERT INTO `user` VALUES (null, 'a1');

Dockerfile:

FROM mysql:5.7
ENV MYSQL_ROOT_PASSWORD=123 MYSQL_DATABASE=test
COPY ./test.sql /var/data/test.sql

构建并启动容器,这里直接用--network参数把容器加入dpd网络:

cd mysql
docker build -t dpd-mysql .
docker run --name dpd-mysql -d --network dpd -p 3306:3306 dpd-mysql

进入容器把表和测试数据导入进去:

docker exec -it dpd-mysql bash
mysql -uroot -p
mysql> use test;
mysql> source /var/data/test.sql

修改index.php:

<?php
$dsn = 'mysql:dbname=test;host=dpd-mysql';
$user = 'root';
$password = '123';

try 
    $dbh = new PDO($dsn, $user, $password);
    $sql = 'SELECT * FROM user WHERE id=?';
    $sth = $dbh->prepare($sql);
    $sth->execute(array(1));
    $result = $sth->fetch(PDO::FETCH_ASSOC);
    var_dump($result);
 catch (PDOException $e) 
    echo 'Error: ' . $e->getMessage();

浏览器输入http://local.docker-php-demo.com/index.php,PHP成功地从数据库获取到了用户数据:

array(2)  ["id"]=> string(1) "1" ["name"]=> string(2) "a1" 

参考资料

以上是关于用Docker搭建LNMP的主要内容,如果未能解决你的问题,请参考以下文章

ubuntu系统用docker搭建wordpress

用docker搭建测试环境--docker的基本操作

用docker搭建最简单的应用

用Docker搭建WordPress博客

Docker搭建Nginx

用docker快速搭建flarum论坛