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
即可。