Docker+PHP+Redis+WebSocket+Nginx+https 构建纯容器化前后端应用

PHP

FROM php:7.4.3-apache

EXPOSE 80
EXPOSE 443

RUN curl -L -o /tmp/redis.tar.gz https://github.com/phpredis/phpredis/archive/5.2.0.tar.gz \
    && tar xfz /tmp/redis.tar.gz \
    && rm -r /tmp/redis.tar.gz \
    && mkdir -p /usr/src/php/ext \
    && mv phpredis-5.2.0 /usr/src/php/ext/redis \
    && docker-php-ext-install redis

RUN apt-get update -y && \
    apt-get install -y libmcrypt-dev && \
    pecl install mcrypt-1.0.3 && \
    docker-php-ext-enable mcrypt

RUN mkdir -p /var/www/html/

RUN chmod -R 755 /var/www/html

上述是构建PHP镜像的Dockerfile。第一个RUN是为了安装Redis扩展,第二个RUN是为了安装mcrypt以及mcrypt扩展。第三个和第四个RUN是为了给Apache整个目录的权限。接着执行命令,构建镜像(my-cdc),创建容器(mycdc),并启动容器:

docker build --tag my-cdc .
docker run --name mycdc --restart=always -v $(pwd)/src:/var/www/html -d my-cdc

踩坑:首先是官方的PHP镜像肯定是不带Redis扩展的,需要通过第一条RUN在构建自己的镜像的时候安装扩展,安装扩展的时候,安装脚本会自动安装基础Ubuntu镜像缺失的依赖。实测如果基础镜像选择Alpine版本,也会通过apk add命令自动安装缺失的依赖。其次是因为项目里面用到了mcrypt,但是这个扩展在PHP7.1的时候被弃用,在PHP7.2的时候被移除,所以只能通过第二条RUN安装。尝试过使用7.0、7.1版本的PHP官方镜像,竟然发现也没有mcrypt扩展,不知为何,故直接使用最新版PHP镜像,并使用上述方法安装mcrypt扩展。

注意:启动容器的时候,是通过-v将php源码的目录$(pwd)/src映射到Apache默认的源码目录/var/www/html中。这样我们修改php代码之后可是实时生效,无需重新构建镜像和创建容器。如果没有这样做,而是在Dockerfile中通过COPY src/ /var/www/html/的方式将php源码复制进基础镜像的目录并构建自己的镜像,则每次更新php源码后,都需要重新构建镜像,非常麻烦。

Redis

docker pull redis
docker run --name my-redis --net=container:mycdc -d redis

直接通过上述命令,创建创建容器(my-redis),并启动容器。

注意:--net=container:mycdc的作用是让Redis的容器与PHP的容器处于同一个网桥下,这样php可以直接通过127.0.0.1:6379访问Redis。

WebSocket+Nginx+https

php代码中除了访问后端API,还通过WebSocket连接后端,实时更新数据。而现在部署前端的时候肯定使用的是https,如果依然使用ws协议,那么握手的时候走的是http,在Chrome中会出现网页混合错误,无法连接WebSocket。所以就需要将WebSocket通过Nginx反向代理,从ws协议升级为wss。

location /wss/{
    # switch off logging
    access_log off;

    # redirect all HTTP traffic to socket_server:3051
    proxy_pass http://socket_server:3051;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # WebSocket support (nginx 1.4)
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

在nginx.conf中,ws原域名的443端口节点下,新增一个location,将所有向ws原域名/wss/路径的443端口请求转发到socket_server:3051。其中socket_server为WebSocket服务的容器的名称,3051为WebSocket端口号。于是,原来的ws地址为:ws://host:3051,修改之后wss地址为:wss://host/wss/。注意wss不带端口,因为我们使用默认的443端口。

接下来就是配置Nginx对前端php的反向代理了

server {
    listen       443 ssl;
    server_name  xxx.com;

    ssl_certificate      cert/xxx.pem;
    ssl_certificate_key  cert/xxx.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;


    location / {
        index  index.php
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://mycdc:80/;
    }
}   

server {
    listen       80;
    server_name  xxx.com;
    return 301 https://xxx.com;
}

创建一个对前端域名(xxx.com)以及443端口监听的server,配置ssl,注意通过proxy_pass反向代理到前面创建的php容器的80端口。同时为了能在通过http访问前端的时候自动跳转到https,可以添加一个对前端域名以及80端口监听的server,并跳转到https。

接下来就是构建Nginx镜像(my-nginx),创建容器(mynginx),并启动容器:

docker build --tag my-nginx .
docker run --name mynginx --restart=always -p 443:443 -p 80:80 --link mycdc --link socket_server -v $(pwd)/logs:/etc/nginx/logs -d my-nginx

注意:启动Nginx容器的时候,通过–link,将前面构建的PHP容器以及WebSocket容器链接起来,这样nginx.conf中对WebSocket以及前端PHP的反向代理才能生效。

如上我们实现了将PHP、Redis、WebSocket、Nginx全部容器化的前后端构建。

不过有一个可以提升的地方,就是将Nginx置于容器中之后,无法获取到访问者真实的远程IP,导致反向代理后的各个服务,也无法得到访问者真实IP。除非Nginx容器使用–net=host模式,来跟宿主机使用相同的网段,而不自己创建一个虚拟网桥。但是这样的话,就不能在启动Nginx容器的时候,使用–link链接容器,来实现端口隔离,所有要通过Nginx反向代理的容器都会将端口直接暴露在宿主机,从而可能造成端口冲突,还有安全风险,还不好管理。Docker的issue中也很多人在等待这个问题的解决方案,不过几年过去了,依然没有好的解决方案。据说–net=host模式还只在Linux下生效,Windows和Mac下无效,不过这个说法我没有去证实。

注意:由于Nginx反向代理的作用,被Nginx代理后的WebSocket容器无法直接得到用户远程真实ip(WebApi容器也是,只不过本文不对WebApi作说明)。但是上述nginx.conf规则中有一条proxy_set_header X-Real-IP $remote_addr;,也就是说用户的真实ip会在ws握手的时候以Header的形式通过x-real-ip传进来,我们只需要在监听握手的时候,提取x-real-ip这个Header的value,就可以得到用户远程真实ip。WebApi同理,在监听Request的时候,提取Header中的x-real-ip即可。

Share

You may also like...

发表评论

电子邮件地址不会被公开。 必填项已用*标注