标签归档:php

php

Let’s encrypt

最近域名主机双双到期了,原来的服务商建议域名迁出,于是转移到了Godaddy,过程很顺利。主机迁到了linode,一个是因为它便宜,另一个是因为想给自己的网站加个SSL证书。
首先是服务器环境Apache, PHP, MaraiDB(MySQL)的配置。 linode 创建主机很简单,点点就好了,然后可以去启动机器,设置SSH访问。

yum update
yum install httpd php php-cli php-mbstring php-pdo php-mysql php-gd php-tidy

启动Apache

systemctl start httpd.service
systemctl enable httpd.service

然后直接访问你的服务器ip,可以看到默认的欢迎界面

ip addr show eth0 | grep inet | awk '{ print $2; }' | sed 's/\/.*$//'
curl 1.139.x.x

CentOS 7目前默认不提供Mysql Server,而是Mariadb。MySQL的命令行仍然可以使用并兼容, PHP仍然可以使用PDO及MySQL扩展访问它。

yum install mariadb-server mariadb
systemctl start mariadb

然后设置管理员密码及安全设置

mysql_secure_installation

开机启动Mariadb服务

systemctl enable mariadb.service

可以在/var/www/html目录下创建一个测试脚本来验证安装情况

<?php
phpinfo();

WordPress则是从原本的数据库全部导出,文件全部打包回来。
在Linode上创建对应的数据库,用户及导入脚本

MariaDB [(none)]> CREATE database courages_wordpress;

MariaDB [(none)]> CREATE USER courages_wp IDENTIFIED BY '*************';

MariaDB [(none)]> grant all privileges on courages_wordpress.* to courages_wp@localhost identified by '*************';

导入数据库

mysql -uroot -p courages_wordpress < /tmp/wordpress.sql

将文件解压并复制到/var/www/html目录

tar -xzvf backup.tar.gz
cp -R backup/public_html/* /var/www/html/*
chown -R apache:apache /var/www/html

更改Apache设置AllowOverride 为all,以便支持WordPress的链接重定向。

vim /etc/httpd/conf/httpd.conf

<Directory "/var/www/html">
    #
    # Possible values for the Options directive are "None", "All",
    # or any combination of:
    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
    #
    # Note that "MultiViews" must be named *explicitly* --- "Options All"
    # doesn't give it to you.
    #
    # The Options directive is both complicated and important.  Please see
    # http://httpd.apache.org/docs/2.4/mod/core.html#options
    # for more information.
    #
    Options Indexes FollowSymLinks

    #
    # AllowOverride controls what directives may be placed in .htaccess files.
    # It can be "All", "None", or any combination of the keywords:
    #   Options FileInfo AuthConfig Limit
    #
    AllowOverride All

    #
    # Controls who can get stuff from this server.
    #
    Require all granted
</Directory>

重启Apache

systemctl restart httpd.service

在linode的DNS manager那里新增一个新的domain,在服务器列表里面选中对应的服务器就可以了,然后就可以看到对应的域名解析信息。
域名转移会要求一个key,从原注册商那里解锁并获得,在Godday输入Key后,它会发邮件与你确认,然后将DNS域名服务器改为linode的域名服务器就好了。

Let’s Encrypt提供免费90天的SSL证书,如果证书到期了就需要再次更新下。如果你有shell权限,它推荐使用Cerbot来安装和更新证书。CentOS 7 + Apache的安装非常简单。首先安装EPEL源,要不然找不到对应的安装包

yum install epel-release
yum install certbot-apache
certbot --authenticator webroot --installer apache

设置一下域名,网站目录及域名重定向

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer apache
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): <email@example.com>
Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org

-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A

-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: Y
Starting new HTTPS connection (1): supporters.eff.org
No names were found in your configuration files. Please enter in your domain
name(s) (comma and/or space separated)  (Enter 'c' to cancel): courages.us
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for courages.us
Input the webroot for courages.us: (Enter 'c' to cancel): /var/www/html
Waiting for verification...
Cleaning up challenges

We were unable to find a vhost with a ServerName or Address of courages.us.
Which virtual host would you like to choose?
(note: conf files with multiple vhosts are not yet supported)
-------------------------------------------------------------------------------
1: ssl.conf                       |                       | HTTPS | Enabled
-------------------------------------------------------------------------------
Press 1 [enter] to confirm the selection (press 'c' to cancel): 1
Deploying Certificate for courages.us to VirtualHost /etc/httpd/conf.d/ssl.conf

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
-------------------------------------------------------------------------------
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Created redirect file: le-redirect-courages.us.conf
Rollback checkpoint is empty (no changes made?)

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://courages.us

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=courages.us
-------------------------------------------------------------------------------

在浏览器访问一下域名即有小绿钥匙。可以在/etc/httpd/conf.d/ssl.conf查看相应的SSL证书配置

<VirtualHost _default_:443>
ServerName courages.us
SSLCertificateFile /etc/letsencrypt/live/courages.us/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/courages.us/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateChainFile /etc/letsencrypt/live/courages.us/chain.pem
</VirtualHost>

由于证书在90天后即将失效,可以加入crontab自动更新

certbot renew --dry-run
crontab -e

0 0,12 * * * python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew 

参考链接:
How To Install Linux, Apache, MySQL, PHP (LAMP) stack On CentOS 7
How to enable EPEL repository?
How to Secure Your Server
Introduction to FirewallD on CentOS

PHP MongoDB Replica Set应用

MongoDB是个面向文档管理的NoSQL数据库,能够直接存取JSON数据,支持海量数据。公司内部的一个单点登录系统使用MongoDB来存储用户的session,以便在不同应用,服务器之间共享登录信息,解决了服务器切换用户状态丢失(被登出)的问题。单点登录系统前端采用PHP,与LDAP交互认证用户信息,提供登录界面,API等。MongoDB则采用Replica Set模式以便支持高可用。
在CentOS 6.5上安装MongoDB,首先添加源仓库

$ suod vim /etc/yum.repos.d/mongodb.repo
[mongodb]
name=MongoDB Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/x86_64/
gpgcheck=0
enabled=0

安装,启动,连接MongoDB

$ sudo yum --enablerepo=mongodb install mongodb-org
$ sudo /sbin/chkconfig --levels 235 mongod on
$ sudo mongod --port 27017 --dbpath /data/db1
$ mongo --port 27017

创建用户SSOReplUser

> use admin 
> db.createUser( { user: "SSOReplUser", pwd: "<password>", roles: [ { role: "root", db: "admin" } ] });

创建集群要用到的key

$ sudo openssl rand -base64 741 > /home/sso/mongodb-keyfile 
$ sudo chmod 600 /home/sso/mongodb-keyfile

编辑MongoDB配置文件

$ sudo vim /etc/mongod.conf
# Replication Options 
# in replicated mongo databases, specify the replica set name here 
replSet=SSOReplSet 
# maximum size in megabytes for replication operation log 
#oplogSize=1024 
# path to a key file storing authentication info for connections # between replica set members keyFile=/home/sso/mongodb-keyfile

重启mongod服务

$ sudo /etc/init.d/mongod stop   
$ sudo /usr/bin/mongod -f /etc/mongod.conf

使用SSOReplUser登录

> use admin 
> db.auth("SSOReplUser", "<password>");

初始化集群

> rs.initiate()
> rs.conf()   
{
  "_id": "SSOReplSet",
  "version": 1,
  "members": [
    {
      "_id": 1,
      "host": "192.168.33.10:27017"
    }
  ]
}

在其他机器上按照上面步骤安装MongoDB,更改配置,复制对应的mongodb-keyfile并启动。
回到192.168.33.10并添加机器

SSOReplSet:PRIMARY> rs.add("192.168.33.11:27017") 
SSOReplSet:PRIMARY> rs.add("192.168.33.12:27017") 
SSOReplSet:PRIMARY> rs.add("192.168.33.13:27017")
#SSOReplSet:PRIMARY> rs.remove("192.168.33.13:27017")

在从机上认证查看

SSOReplSet:SECONDARY> use admin 
SSOReplSet:SECONDARY> db.auth("SSOReplUser", "<password>");   
SSOReplSet:SECONDARY> rs.status()

MongoDB的Replica set模式最少要求3台机器,以便在PRIMARY机器故障时,能够自我选举出新的PRIMARY,但需要n/2+1的机器投票同意。例如刚才配置了4台机器,那么需要4/2+1=3台机器投票,能够承受一台机器故障。如果是集群有5台机器,则能够承受2台机器故障。因此再配置一台不存储数据的Arbiter机器比较合理。
按照上面的步骤安装,复制mongodb-keyfile文件,但需要更改mongod.conf

$sudo vim /etc/mongod.conf
nojournal=true

然后启动mongod服务并在PRIMARY机器上将这台机器加入机器

SSOReplSet:PRIMARY> use admin   
SSOReplSet:PRIMARY> db.auth("SSOReplUser", "<password>");   
SSOReplSet:PRIMARY> rs.addArb("<ip of arbiter>");

PHP的mongo扩展安装比较简单,下载对应版本,启用即可。然而在应用过程中发现登录有点慢,启用看PHP mongo扩展profiling功能,查看PHP日志

<?php
\MongoLog::setLevel(\MongoLog::ALL);
\MongoLog::setModule(\MongoLog::ALL);
try{
    echo  microtime(true) . PHP_EOL;
    echo "aaa01(primary)" . PHP_EOL;
    $m = new \MongoClient("mongodb://admin:XXXXXXXXX@192.168.33.10:27017/?replicaSet=SSOReplSet ");
    echo  microtime(true) . PHP_EOL;
    echo "aaa01(primay), bbb01(secondary),ccc01(secondary),ddd01(secondary)" . PHP_EOL;
    $m = new \MongoClient("mongodb://admin:XXXXXXXXX@192.168.33.10:27017,192.168.33.11:27017,192.168.33.12:27017,192.168.33.13:27017/?replicaSet=SSOReplSet ");
    echo  microtime(true) . PHP_EOL;
} catch (\MongoConnectionException $e) {
    var_dump($e);
}

发现PHP连接MongoDB集群是这样子的

即:

  • 1)在连接里面配置了几个MongoDB连接服务器,PHP每个都会创建连接去查询
  • 2)从每台服务器上查询出整个集群的服务器列表,再分别ping和连接这些服务器,如果连接不存在或不匹配则创建,无效的则销毁
  • 3)汇总所有服务器返回集群列表
  • 4)选择离自己最近的服务器并使用该与该服务器的连接
  • MongoDB 的写操作默认在Primary节点上操作完成即返回成功;读操作默认是也是从Primary上读取,所以需要去查询服务器。由于SSO应用部署在多个数据中心,网络抖动会造成较大影响,跨数据中心的查询并不会很快,如果每次都去连接并查询整个列表是比较耗时。另外如果在php里面配置的是IP而MongoDB Replica Set里面配置的是域名,则连接名会出现不匹配,而创建新的连接并销毁旧连接,也耗时。如果配置的是域名,则需要DNS解析。由于每台PHP服务器均已配置HA检测,最终每个应用只配置了一台服务器,并统一配置MongoDB集群为IP。而连接最快的是在Primary机器上,可以根据本机是否Primary来做HA:

    #!/usr/bin/env bash
    count=`ps -fe |grep "mongod" | grep -v "grep" | wc -l`
    FILE="/home/scripts-bits/master.conf"
    SERVER=`hostname`
    
    if [ $count -lt 1 ]; then
        rm -f $FILE
    else
        PRIMAY=`/usr/bin/mongo ${SERVER}:27017 --quiet --eval 'printjson(db.isMaster().ismaster);'`
        if [ "$PRIMAY" == "true" ]; then
        	if [ ! -f "$FILE" ]; then
        		touch "$FILE"
        	fi
        	REMOVE=`/usr/bin/mongo ${SERVER}:27017/admin --quiet /home/scripts-bits/mongo_status.js`
        fi
    fi
    

    删除故障节点(not reachable/healthy)的脚本mongo_status.js:

    db.auth('admin','<password>');
    conf=rs.status();
    members=conf["members"];
    for(i in members){
    	if(members[i]["state"] == 8){
    		rs.remove(members[i]["name"]);
    	}
    }
    

    这中间也出现过因为网络/机器故障,导致PHP等待连接超时的情况的,将该机器从集群中移除即可。然而当服务器是启动的(可以到达)情况下,MongoDB故障,则不会超时。可以更改connectTimeoutMS,以便减少等待。
    MongoDB的日志默认都是记录在/var/log/mongodb/下面并且会越来越大。创建Python脚本来定时清除它:

    #!/bin/env python
    import commands
    import datetime,time
    
    def rotate_log(path, expire = 30):
        str_now = time.strftime("%Y-%m-%d")
        dat_now = time.strptime(str_now, "%Y-%m-%d")
        array_dat_now = datetime.datetime(dat_now[0], dat_now[1], dat_now[2])
        lns = commands.getoutput("/bin/ls --full-time %s|awk '{print $6, $9}'" % path)
        for ln in lns.split('\n'):
            ws = ln.split()
            if len(ws) != 2:
                continue
            ws1 = time.strptime(ws[0], "%Y-%m-%d")
            ws2 = datetime.datetime(ws1[0], ws1[1], ws1[2])
            if (array_dat_now - ws2).days > expire:
                v_del = commands.getoutput("/bin/rm -rf %s/%s" % (path, ws[1]))
    
    
    def rotate_mongo():
        # get mongo pid
        mongo_pid = commands.getoutput("/sbin/pidof mongod")
        #print mongo_pid
        # send Sig to mongo
        if mongo_pid != '':
            cmd = "/bin/kill -USR1 %s" % (mongo_pid)
            # print cmd
            mongo_rotate = commands.getoutput(cmd)
        else:
            print "mongod is not running..."
    
    if __name__ == "__main__":
        log_path = "/var/log/mongodb/"
        expire = 30
        rotate_mongo()
        rotate_log(log_path, expire)
    
    

    加入到crontab里面去执行

    10 1 * * * /usr/bin/python /home/sso/mongo_rotate.py > /dev/null 2>&1
    

    参考连接
    mongodb与mysql相比的优缺点
    PHP MongoDB 复制集合
    MongoDB Replication
    MongoDB Enable Auth
    MongoDB Rotate Log Files
    Blocking connect() leads to cumulative timeouts for multiple inaccessible servers
    How Raft consensus algorithm will make replication even better in MongoDB 3.2
    Write Concern for Replica Sets
    MongoDB Read Preference

    日志系统设计

    日志系统是项目开发/运维当中非常重要的一部分,提供产品使用情况,跟踪调试信息等。以前都是各个项目各自实现日志记录,比如PHP、JAVA各自实现一套,如果要跨项目/服务器进行查询/跟踪/统计,则比较麻烦,比如Web后台–>业务模块–>基础组件,客户端–>公共接口–>业务模块等等。
    目前的项目都是将日志写入本地,首先需要定义统一的规范:

    格式
    DateTime | ServerIP | ClientIP | PID | RequestID|  Type | Level | Message|  Code
    
    解释
    DateTime:记录日志的当前时间戳
    ServerIP:当前服务器IP
    ClientIP:客户端IP
    PID:进程ID
    RequestID:交易号,即请求的唯一标识,使用uniqid(+UserCode)或UUID或,一次请求会有多条日志,,用于关联本次请求内的相关日志
    Type:日志类型,比如统计,操作(审计),业务等
    Level:日志等级
    Message:日志内容,当为值为数组或者对象时转为JSON
    Type为RUNTIME时,表示运行日志,属性:自定义
    Type为HTTP时,表示来源请求,属性:Url,Method(Get/Post),Params,RemoteIP,UserAgent,ReferUrl[,Action,Method]
    Type为REST时,表示外部调用,属性:Type(Http/Https),Url,Port,RequestParams,Response,RunTime;
    Type为SQL时,表示SQL执行,属性:Sql,RunTime
    Code:标识码,记录错误码、响应码、统计码、版本、自定义信息,方便统计
    

    这里的RequestID由入口处自动产生,用于标识一次请求,关联所产生的所有日志,需要在各个项目之间传递。为了减少日志,Level通常为Info级别,避免产生过多日志。为了方便调试追踪,RequestID和Level也可以在其他参数中指明,比如HTTP头里面附加。
    然后是日志收集:客户端收走日志系统,发送给日志系统服务端。
    然后分析处理呈现:服务端将接收到的日志,发给处理其他组件分析处理,提供Web界面的查询系统。研发人员,可以错误信息,定位问题;获悉程序运行情况进行调优;大数据分析日志,得出产品使用情况;运维平台则可以进行业务报警。

    日志产生由个语言依照规范自行实现,收集、保持则由FlumeKafka实现。Flume是一个分布式的日志收集、合并、移动系统,能够监控文件变化,将变化部分传输出去。Kafka是一个分布式的发布/订阅的消息流平台,包括Broker,Consumer,Producer都支持分布式,依赖Zookeeper实现。

    在PHP上面的实现,一开始使用log4php,看起来很美好,但是性能很差,对业务影响较大。决定再次简化,砍掉不必要的东西(Socket,邮件等等),在C语言开发的PHP日志扩展SeasLog基础上在做开发,将日志文件保存在本地。为了减少日志所占用内存,每超过一定的大小的日志即进行保存,否则在最后进行保存,利用了Nginx的fastcgi_finish_request特性。生产上发现,每天产生的文件日志太大了,需要控制日志信息大小、等级,并及时清理。
    对于Web后台,还结合FirePHP,将日志直接输出到浏览器,方便边运行变调试。

    参考链接:
    最佳日志实践
    Optimal Logging
    Flume+Kafka收集Docker容器内分布式日志应用实践
    基于Flume的美团日志收集系统(一)架构和设计
    Twitter是如何构建高性能分布式日志的
    开源日志系统比较
    Kafka剖析(一):Kafka背景及架构介绍
    基于Flume的野狗实时日志系统的演进和优化
    EVODelavega/phpkafka
    有赞统一日志平台初探
    RabbitMQ和kafka从几个角度简单的对比
    Flume-ng的原理和使用
    利用flume+kafka+storm+mysql构建大数据实时系统

    使用Docker Compose管理Docker容器

    上一篇创建了一个PHP的运行环境,现在需要一个MySQL服务,直接运行:

    root@thinkpad:~# docker run --name rocket-mysql -v /home/rocketfish/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
    

    就自动下载并得到一个mysql版本为5.7的服务镜像。
    这时候,要运行一个Web服务我们需要运行两次的docker run才可以,如果还有更多的Web容器或其他的服务容器呢?
    Docker官方提倡一个容器仅提供一个服务,多个服务/容器可以使用docker-compose来管理。
    docker-compose本身是一个Python写的工具,可以直接通过pip安装:

    root@thinkpad:~# sudo pip install --upgrade pip
    

    如果你本地并没有Python环境,也可以采用docker-compose的docker镜像来运行:

    root@thinkpad:/home/compose-web# curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
    root@thinkpad:/home/compose-web# docker-compose --version
    #开始下载镜像,建议用第一种
    

    查看帮助

    root@thinkpad:~# docker-compose -h
    Define and run multi-container applications with Docker.
    
    Usage:
      docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
      docker-compose -h|--help
    
    Options:
      -f, --file FILE             Specify an alternate compose file (default: docker-compose.yml)
      -p, --project-name NAME     Specify an alternate project name (default: directory name)
      --verbose                   Show more output
      -v, --version               Print version and exit
      -H, --host HOST             Daemon socket to connect to
    
      --tls                       Use TLS; implied by --tlsverify
      --tlscacert CA_PATH         Trust certs signed only by this CA
      --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
      --tlskey TLS_KEY_PATH       Path to TLS key file
      --tlsverify                 Use TLS and verify the remote
      --skip-hostname-check       Don't check the daemon's hostname against the name specified
                                  in the client certificate (for example if your docker host
                                  is an IP address)
    
    Commands:
      build              Build or rebuild services
      bundle             Generate a Docker bundle from the Compose file
      config             Validate and view the compose file
      create             Create services
      down               Stop and remove containers, networks, images, and volumes
      events             Receive real time events from containers
      exec               Execute a command in a running container
      help               Get help on a command
      kill               Kill containers
      logs               View output from containers
      pause              Pause services
      port               Print the public port for a port binding
      ps                 List containers
      pull               Pulls service images
      push               Push service images
      restart            Restart services
      rm                 Remove stopped containers
      run                Run a one-off command
      scale              Set number of containers for a service
      start              Start services
      stop               Stop services
      unpause            Unpause services
      up                 Create and start containers
      version            Show the Docker-Compose version information
    

    这些命令包括了创建/启动/停止/暂停/继续运行容器。
    首先要创建docker-compose.yml,这是一个YAML格式的文档

    mkdir docker
    cd docker
    mkdir web
    mkdir db
    vim docker-compose.yml
    

    内容如下:

    version: '2'
    services:
      db:
        image: mysql:5.7
        ports:
        - "3306:3306"
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: web
          MYSQL_USER: root
          MYSQL_PASSWORD: rootweb
        volumes:
        - ./db/data:/var/lib/mysql
      web:
        depends_on:
          - db
        image: nginx-php-fpm:phalcon
        ports:
        - "80:80"
        restart: always
        environment:
          WEB_DB_HOST: db:3306
          WEB_DB_PASSWORD: root
        volumes:
        - ./web/html:/var/www/html/
        links:
        - db
    

    这里的environment定义了环境变量,比如host的MAC,就可以在docker容器的系统变量里面得到,各个编程语言都有获取系统环境变量的方法。这些变量也可以组织起来放在文件里面加载进去,参考这里
    这里的volumes定义了要映射进容器的文件或文件夹或者数据容器,可以参考这里。注意,多个容器共享同一个目录,会出现写冲突,比如MySQL,多个实例,数据目录需要分开。所以设计好你的程序,哪一部分是需要只读的(可以共享),哪一部分是需要写的;写的部分是否可以放到各自临时目录,或者其他公共服务里面。
    运行并查看

    root@thinkpad:/home/compose-web# docker-compose up -d
    Creating network "composeweb_default" with the default driver
    Creating composeweb_db_1
    Creating composeweb_web_1
    root@thinkpad:/home/compose-web# docker-compose ps
          Name                   Command             State              Ports            
    ------------------------------------------------------------------------------------
    composeweb_db_1    docker-entrypoint.sh mysqld   Up      0.0.0.0:3306->3306/tcp      
    composeweb_web_1   /start.sh                     Up      443/tcp, 0.0.0.0:80->80/tcp 
    #也可以使用原来的命令
    root@thinkpad:/home/compose-web# docker ps
    CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                         NAMES
    efbdaf257748        nginx-php-fpm:phalcon   "/start.sh"              13 seconds ago      Up 11 seconds       0.0.0.0:80->80/tcp, 443/tcp   composeweb_web_1
    a6935d20911e        mysql:5.7               "docker-entrypoint.sh"   14 seconds ago      Up 13 seconds       0.0.0.0:3306->3306/tcp        composeweb_db_1
    
    

    docker-compose up -d ,将会在后台启动并运行所有的容器。
    docker-compose仅仅提供了对多个docker服务的管理,仍然可以在这些容器上运行原来的docker命令。检查下web容器的IP:

    root@thinkpad:/home/compose-web# docker inspect composeweb_web_1 | grep IPAddress
                "SecondaryIPAddresses": null,
                "IPAddress": "",
                        "IPAddress": "172.18.0.3",
    

    然后访问:http://172.18.0.3/ 就可以看到web页面了。
    如果想要停止服务,可以用docker-compose stop:

    root@thinkpad:/home/compose-web# docker-compose stop db
    Stopping composeweb_db_1 ... done
    root@thinkpad:/home/compose-web# docker-compose stop web
    Stopping composeweb_web_1 ... done
    

    再次启动,得到相同名称的服务:

    root@thinkpad:/home/compose-web# docker-compose up -d
    Starting composeweb_db_1
    Starting composeweb_web_1
    

    各个容器的名称也可以在配置文件里面通过参数container_name来指定。
    docker-compose的配置还有很多其他的参数,可以参考这里。比如刚才我们通过docker inspect来查找容器IP,也可以配置成静态IP:

    version: '2'
    
    services:
      app:
        image: nginx-php-fpm:phalcon
        networks:
          app_net:
            ipv4_address: 172.18.0.10
            ipv6_address: 2001:3984:3989::10
    
    networks:
      app_net:
        driver: bridge
        driver_opts:
          com.docker.network.enable_ipv6: "true"
        ipam:
          driver: default
          config:
          - subnet: 172.18.0.0/24
            gateway: 172.18.0.1
          - subnet: 2001:3984:3989::/64
            gateway: 2001:3984:3989::1
    

    也可以自定义网络
    刚才我们的配置文件里面直接指定了镜像,也可以指定Dockerfile镜像构建,假如Dockerfile在web目录下面:

    build:
      context: ./web
    

    或者直接指明Dockerfile:

    build:
      context: .
      dockerfile: Dockerfile-alternate
    

    实际上,docker-compose的配置已经覆盖了Dockerfile的配置项,也可以直接用于构建多个docker服务,比如指定运行的命令

    command: [/bin/bash]
    

    这里的入口程序,如果不能持续运行的话,运行完成后,docker就会退出。所以如果你需要可以后台运行的docker容器,那么入口程序,就必须保持一个程序在前台运行,不退出,比如运行crontab的镜像,入口程序可以是:

    cron && bash
    

    cron这个程序是在后台运行的,如果不配合前台程序bash,容器执行玩cron就会退出了。其他运行crontab的容器,大都是强制一个程序在前台不退出,比如这个

    cron && tail -f /var/log/cron.log
    

    这里需要注意一下,通过docker设置的environment values在容器内的cli下面执行时取不到的,比如printenv,使用cron后台运行时取不到外部设置的变量,但是手动执行时又可以。这些外部环境变量只能在ENTRYPOINT或者CMD所执行的shell里面去设置,比如在CMD执行start.sh:

    printenv | grep -E "^HOST" > /root/env.conf && cron && bash
    

    Docker compose还可以跟Docker Swarm结合。

    参考链接:
    Install Docker Compose
    Quickstart: Docker Compose and WordPress
    YAML 模板文件
    Introduction to Docker Compose Tool for Multi-Container Applications
    Dockerfile基本结构
    How to Get Environment Variables Passed Through docker-compose to the Containers
    Access environment variables from crontab into a docker container
    How can I access Docker set Environment Variables From a Cron Job
    Dockerfile里指定执行命令用ENTRYPOING和用CMD有何不同?
    What is the difference between CMD and ENTRYPOINT in a Dockerfile?
    Docker difference between run, cmd, entrypoint commands

    基于Docker的Nginx + PHP-FPM + Phalcon镜像

    上一篇简单介绍了Docker的安装,运行,这一篇来构建一个基于Nginx和PHP-FPM的Phalcon镜像。在官方找了以下,单独的Nginx和PHP镜像更加流行,混合的反倒不是很受欢迎。其实官方并不提倡在一个容器里面运行多个服务,最好是一个容器只对外提供一个服务:一个容器启动时仅仅运行一个命令(其实里面可以包含多个),也方便部署扩展升级。多个服务之间可以使用Docker Compose来管理。但是Docker并不阻止创建包含多个服务器的镜像,为了方便,所以我们仍然可以自己构建。
    构建镜像可以有好几种方式,比如基于Alpine Linuxphusion/baseimage-docker构建,或者基于Ubuntu,CentOS等构建,又或者在PHP,Nginx的基础镜像上构建。注意:如果要采用Ubuntu或者CentOS构建,可能需要一些额外的工作,以便保持镜像轻量稳定运行。
    这里采用已有的richarvey/nginx-php-fpm来构建,它是一个基于Nginx官方镜像来构建的。
    Github上拉取相关文件从Dockerfile构建:

    $ sudo git clone https://github.com/ngineered/nginx-php-fpm
    $ sudo docker build -t nginx-php-fpm:latest .
    

    关于Dockerfile的相关解释,可以参考这里。当然也可以直接拉取镜像使用

    $ sudo docker pull richarvey/nginx-php-fpm
    # 也可以直接运行,会自动拉取
    #$ sudo docker run -d richarvey/nginx-php-fpm
    

    查看本地的镜像,连单独的nginx也来了:

    root@thinkpad:~# docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    nginx-php-fpm       latest              4fc9ac9f2945        7 hours ago         228.5 MB
    nginx               mainline-alpine     00bc1e841a8f        5 days ago          54.21 MB
    

    这里的mainline-alpine是指基于Alpine Linux构建的。Alpine Linux是一个仅有5M大小的linux系统,采用apk add/search来安装/查找相应软件,有许多镜像都是基于它构建的,官方PHP镜像也有基于它构建的Docker镜像。
    然后运行nginx-php-fpm:

    root@thinkpad:~# docker run --name web -d richarvey/nginx-php-fpm
    

    docker inspect命令用来查看容器的相关信息,查看下分配的IP:

    root@thinkpad:~# docker inspect web | grep IPAddress
                "SecondaryIPAddresses": null,
                "IPAddress": "172.17.0.2",
                        "IPAddress": "172.17.0.2",
    

    然后在浏览器里面访问:http://172.17.0.2/就可以看到phpinfo的页面。到这里,Nginx + PHP的web容器就已经运行起来了,对应的Nginx和PHP进程可以在宿主机器上直接查看:

    root@thinkpad:~# ps aux | grep nginx
    root     18167  0.0  0.0  13696  4300 pts/6    S    01:47   0:00 nginx: master process /usr/sbin/nginx
    systemd+ 18168  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
    systemd+ 18169  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
    systemd+ 18170  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
    systemd+ 18171  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
    systemd+ 18172  0.0  0.0  14144  1868 pts/6    S    01:47   0:00 nginx: worker process
    root     18190  0.0  0.0  21292  1012 pts/18   S+   01:47   0:00 grep --color=auto nginx
    root@thinkpad:~# ps aux | grep php-fpm
    root     18166  0.0  0.2 167880 23364 pts/6    S    01:47   0:00 php-fpm: master process (/etc/php5/php-fpm.conf)
    systemd+ 18173  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
    systemd+ 18174  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
    systemd+ 18175  0.0  0.1 167880  8620 pts/6    S    01:47   0:00 php-fpm: pool www
    root     18192  0.0  0.0  21292  1032 pts/18   S+   01:47   0:00 grep --color=auto php-fpm
    

    接下来要为这个容器添加Phalcon扩展。首先要进入容器里面,使用docker attach命令进入:

    root@thinkpad:~# docker attach web
    
    
    
    

    结果在这里等了半天进不去。。。。查看下当前镜像入口程序:

    root@thinkpad:~# docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    94176348a939        nginx-php-fpm       "/start.sh"         6 seconds ago       Up 5 seconds        80/tcp, 443/tcp     web
    

    这个容器启动的时候运行的是start.sh这个脚本,这个脚本运行了Supervisor工具。于是重新启动容器,运行/bin/bash

    #终止容器运行
    root@thinkpad:~# docker stop web
    web
    #删除容器
    root@thinkpad:~# docker rm web
    web
    #重新运行
    root@thinkpad:~# docker run --name web -d -t -i nginx-php-fpm /bin/bash
    ea21e10df702644a83ed75930b30c7764a786c4feabdf17cd868f86640137c47
    root@thinkpad:~# docker ps
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
    ea21e10df702        nginx-php-fpm       "/bin/bash"         6 seconds ago       Up 5 seconds        80/tcp, 443/tcp     web
    root@thinkpad:~# docker attach web
    #进来了
    bash-4.3# ls
    bin       etc       lib       media     proc      run       srv       sys       usr
    dev       home      linuxrc   mnt       root      sbin      start.sh  tmp       var
    

    就可以进去了。
    先安装编译相关工具包:

    bash-4.3# apk --no-cache add php5-dev
    bash-4.3# apk --no-cache add gcc
    bash-4.3# apk --no-cache add make
    bash-4.3# apk --no-cache add autoconf
    bash-4.3# apk --no-cache add libc-dev
    fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
    fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
    (1/2) Installing musl-dev (1.1.14-r12)
    (2/2) Installing libc-dev (0.7-r0)
    OK: 334 MiB in 106 packages
    

    编译安装Phalcon:

    bash-4.3# cd /home
    bash-4.3# git clone --depth=1 git://github.com/phalcon/cphalcon.git
    bash-4.3# cd cphalcon/build
    bash-4.3# ./install
    bash-4.3# ls -la /usr/lib/php5/modules/ | grep phalcon
    -rwxr-xr-x    1 root     root       5045264 Sep 28 17:34 phalcon.so
    

    更改PHP扩展的配置:

    bash-4.3# cd /etc/php5/conf.d/
    bash-4.3# vi phalcon.ini
    #添加以下内容
    #extension=phalcon.so
    
    #检查扩展是否加载成功
    bash-4.3# php -i | grep phalcon
    /etc/php5/conf.d/phalcon.ini,
    phalcon
    phalcon => enabled
    phalcon.db.escape_identifiers => On => On
    phalcon.db.force_casting => Off => Off
    phalcon.orm.cast_on_hydrate => Off => Off
    phalcon.orm.column_renaming => On => On
    phalcon.orm.enable_implicit_joins => On => On
    phalcon.orm.enable_literals => On => On
    phalcon.orm.events => On => On
    phalcon.orm.exception_on_failed_save => Off => Off
    phalcon.orm.ignore_unknown_columns => Off => Off
    phalcon.orm.late_state_binding => Off => Off
    phalcon.orm.not_null_validations => On => On
    phalcon.orm.virtual_foreign_keys => On => On
    OLDPWD => /home/cphalcon/build
    _SERVER["OLDPWD"] => /home/cphalcon/build
    _ENV["OLDPWD"] => /home/cphalcon/build
    

    加载成功了,需要保持本次镜像变更。首先退出容器:

    bash-4.3# cd /home
    #删除各种不必要的东西,比如gcc
    bash-4.3# rm -rf cphalcon/
    bash-4.3# exit
    exit
    

    然后查看版本并提交变更:

    root@thinkpad:~# docker ps -l
    CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
    ea21e10df702        nginx-php-fpm       "/bin/bash"         31 minutes ago      Exited (0) 6 seconds ago                       web
    root@thinkpad:~# docker commit ea2 nginx-php-fpm:phalcon
    sha256:bb388df328ecc33fac02dba69759d5c992a145f650a0e5b20ca29a4b122fa933
    

    docker commit命令可以用来提交变更,ea2是container id的前三位,也可以写全;然后跟的是要提交的镜像。这里提交到phalcon这个标签下,以便与原来的区分开。查看所有镜像,发现有两个不同的标签:

    root@thinkpad:~# docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    nginx-php-fpm       phalcon             bb388df328ec        11 seconds ago      364.4 MB
    nginx-php-fpm       latest              4fc9ac9f2945        4 hours ago         228.5 MB
    

    采用新镜像来运行,这里要讲程序运行入口改回/start.sh,以便能正常启动Nginx和PHP-FPM:

    root@thinkpad:~# docker rm web
    web
    root@thinkpad:~# docker run --name web -d -t -i nginx-php-fpm:phalcon /start.sh
    deecb19467cda2676b24248e3f55970a2481255c6022a80ffbf5087792ccb559
    root@thinkpad:~# docker ps
    CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS               NAMES
    deecb19467cd        nginx-php-fpm:phalcon   "/start.sh"         4 seconds ago       Up 3 seconds        80/tcp, 443/tcp     web
    

    入口程序改变了,需要再提交一次变更:

    root@thinkpad:~# docker stop web
    web
    root@thinkpad:~# docker ps -l
    CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS                       PORTS               NAMES
    c7600e62733d        nginx-php-fpm:phalcon   "/start.sh"         34 seconds ago      Exited (137) 8 seconds ago                       web
    root@thinkpad:~# docker commit c76 nginx-php-fpm:phalcon
    sha256:1c97ee169a551dd8441f42b40beafd102c71f3e887e2317dc11ce0ef136ceaf0
    

    运行最终的镜像:

    root@thinkpad:~# docker rm web
    web
    root@thinkpad:~# docker run --name web -d -t -i nginx-php-fpm:phalcon
    cb5b0c9e55913a538539e46c53ac7905b21def84a05eb00ef81c4b500853576c
    root@thinkpad:~# docker ps
    CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS               NAMES
    cb5b0c9e5591        nginx-php-fpm:phalcon   "/start.sh"         4 seconds ago       Up 3 seconds        80/tcp, 443/tcp     web
    

    访问http://172.17.0.2/,便可以在页面找到phalcon扩展。
    通常我们会将程序和数据分开,挂载外部文件目录到容器里面去:

    root@thinkpad:~# docker stop web
    web
    root@thinkpad:~# docker rm web
    web
    root@thinkpad:~# docker run --name web -d -t -i -v /home/docker/nginx-php-fpm/src:/var/www/html/ nginx-php-fpm:phalcon
    ffd64793fe8e7a2a95b68f514e221b7ec3b6cadfe668c016f55a7bb6d48bc702
    

    -v参数可以用来挂载目录或者文件,可以又多个-v参数。
    刚才容器里面做的那些已经添加到Dockerfile里面去,你直接使用它来构建。
    至此Nginx + PHP-FPM + Phalcon镜像构建完成,介绍绍了如何进入容器,提交变更,网络访问和文件挂载。

    参考链接:
    A minimal Ubuntu base image modified for Docker-friendliness
    eboraas/phalcon
    基于Docker的PHP开发环境
    Docker for PHP Developers
    Docker在PHP项目开发环境中的应用
    使用 Supervisor 来管理进程
    PHP C扩展框架Phalcon
    Alpine Linux,一个只有5M的Docker镜像