LNMP 多用户虚拟主机方案

A. 特点
1. 高效、内存使用少。
2. 权限分离,用户间互不干扰。

B. 应用程序说明
Nginx : 事件驱动的 Web 服务器,采用模块化设计,小巧、高效。
PHP-CGI : PHP的CGI接口版本(本文使用FastCGI高效接口)。

C. 整个架构的简单说明
Nginx 处理所有的 Web 请求,它将 PHP 的请求 Match 出,发送给上游服务器处理,这里的上游服务器就是 PHP-CGI。
PHP-CGI 工作在 FastCGI 模式,它侦听着一个地址端口(或 Unix socket文件,建议组合权限使用 Unix Socket 更安全),Nginx 会连接并发送请求及回收结果并发送给客户浏览器。
Nginx 运行于 www-data 用户环境,这要求 www-data 用户有所有虚拟主机用户的主目录访问权限。每个虚拟主机拥有自己的 PHP-CGI 进程组(PHP-CGI 可工作在多进程模式),运行于自己的用户环境,本方案并没有设计动态的 PHP-CGI 进程管理器用于对资源的负载均衡。
Nginx 使用了 HTTP OwnerMatch 模块,使得它能够控制每个虚拟主机的每个 Location 有哪些用户的文件的访问权限。关于这个模块

D. 以 Ubuntu 10.04 系统为例的配置实例
1. 安装应用程序

sudo apt-get install nginx mysql-server php5-cgi php5-mysql

PS:建议下载补丁版本的 Nginx => https://heiher.info/1755.html

2. 配置 Nginx
指定 Nginx 的进行用户,和工作进程数,其它根据实际需要作出调整。

sudo vim /etc/nginx/nginx.conf
user www-data;       # 指定使用 www-data 执行 Nginx
worker_processes 2;  # 指定 2 个子工作进程

3. 配置虚拟主机用户
a. 创建用户目录

sudo mkdir -p /var/web/username/{config,cert,bin,run}

b. 创建用户与组
本方案使用 sftp 作为用户的文件管理器,虚拟主机用户没有终端。sftp 方案见我的另一日志。

sudo useradd -m -g sftp -s /bin/false username
sudo mkdir -p /home/username/web/{www,logs}
sudo ln -s /home/username /var/web/username/home

c. 虚拟主机配置文件模板
/var/web/username/config/vhost 为虚拟主机配置文件,将软链接到 /etc/nginx/sites-enabled/ 目录中。以下是模板,修改其中的 username 和 server_name 值。

# vhost
# Heiher <admin@heiher.info>
 
# HTTP Server
server {
 
	listen   80; ## listen for ipv4
 
	server_name  localhost;
 
	access_log  /var/web/username/home/web/logs/access.log;
	error_log  /var/web/username/home/web/logs/error.log;
 
	location / {
		root   /var/web/username/home/web/www;
		index  index.html index.htm index.php;
		## Rewrite
		if (!-e $request_filename)
		{
			rewrite ^(.+)$ /index.php?q=$1 last;
		}
		omallow username sftp; # 允许访问隶属于 username:sftp 的文件
		omdeny all;					# 禁止访问其它所有文件
	}
 
	# pass the PHP scripts to FastCGI server listening on socket file
	#
	location ~ \.php$ {
		if (!-e $request_filename) {
			return 404;
		}
		fastcgi_pass   unix:/var/web/username/run/pfw.sock;
		fastcgi_index  index.php;
		fastcgi_param  SCRIPT_FILENAME  /var/web/username/home/web/www/$fastcgi_script_name;
		include fastcgi_params;
	}
 
	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	#
	location ~ /\.ht {
		deny  all;
	}
}
 
# HTTPS Server
server {
 
	listen   443; ## listen for ipv4
 
	server_name  localhost;
 
	ssl  on;
	ssl_certificate  /var/web/username/cert/cert.pem;
	ssl_certificate_key  /var/web/username/cert/cert.key;
 
	ssl_session_timeout  5m;
 
	ssl_protocols  SSLv3 TLSv1;
	ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
	ssl_prefer_server_ciphers   on;
 
	access_log  /var/web/username/home/web/logs/access.log;
	error_log  /var/web/username/home/web/logs/error.log;
 
	location / {
		root   /var/web/username/home/web/www;
		index  index.html index.htm index.php;
		## Rewrite
		if (!-e $request_filename)
		{
			rewrite ^(.+)$ /index.php?q=$1 last;
		}
		omallow username sftp;
		omdeny all;
	}
 
	# pass the PHP scripts to FastCGI server listening on socket file
	#
	location ~ \.php$ {
		if (!-e $request_filename) {
			return 404;
		}
		fastcgi_pass   unix:/var/web/username/run/pfw.sock;
		fastcgi_index  index.php;
		fastcgi_param  SCRIPT_FILENAME  /var/web/username/home/web/www/$fastcgi_script_name;
		fastcgi_param  HTTPS on;
		include fastcgi_params;
	}
 
	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	#
	location ~ /\.ht {
		deny  all;
	}
}

d. php-fcgi 封装器源代码
这里的封装器只是设置了环境变量,这里同样需要修改 username。
保存下面的内容至文件 /var/web/username/bin/php-fcgi-wrapper

#!/bin/sh
# Heiher <admin@heiher.info>
 
USERNAME=username
PHP_CGI_BIN=/usr/bin/php-cgi
PHP_CGI_SOCKET=/var/web/${USERNAME}/run/pfw.sock
PHPRC=/etc/php5/cgi/
PHP_FCGI_MAX_REQUESTS=32768 # 工作进程最大处理请求数 (小内存建议设置为1,适当提高工作进程数)
PHP_FCGI_CHILDREN=4 # PHP-CGI 工作进程数,影响并发。(小内存建议设置为4)
 
export PHPRC
export PHP_FCGI_MAX_REQUESTS
export PHP_FCGI_CHILDREN
 
exec ${PHP_CGI_BIN} -b ${PHP_CGI_SOCKET}
 
exit 0

e. 安装 php-fcgd 服务

sudo vim /etc/init.d/php-fcgid  # 输入以下内容
sudo update-rc.d php-fcgid defaults

脚本内容

#!/bin/sh
# Heiher <admin@heiher.info>
 
SCRIPT_OK=0
SCRIPT_ERROR=1
 
DESCRIPTION="php-fcgi super-duper-control thing"
NAME=php-fgcid
SCRIPT_NAME=$(basename $0)
 
webdir=/var/web
 
log_daemon_msg () {
    echo $@
}
 
log_end_msg () {
    # Dummy function to be replaced by LSB library.
 
    if test "$1" != "0"; then
        echo "Error with $DESCRIPTION: $NAME"
    fi
    return $1
}
 
phpfcgid_start() {
    echo "Starting $NAME."
    for userdir in ${webdir}/*; do
        user=$(basename ${userdir})
        wrapper=${userdir}/bin/php-fcgi-wrapper
 
        if [ -x ${wrapper} ]; then
                su ${user} -c "${wrapper}&"
        fi
    done
}
 
phpfcgid_stop() {
    echo "Stopping $NAME."
    pkill php-cgi
}
 
phpfcgid_status() {
    log_daemon_msg "To be implemented: status"
    log_end_msg $SCRIPT_ERROR
}
 
 
parse_script_option_list () {
 
    case "$1" in
        start)
            log_daemon_msg "Starting $DESCRIPTION" $NAME
            if phpfcgid_start; then
                log_end_msg $SCRIPT_OK
            else
                log_end_msg $SCRIPT_ERROR
            fi
            ;;
        stop)
            log_daemon_msg "Stopping $DESCRIPTION" $NAME
            if phpfcgid_stop; then
                log_end_msg $SCRIPT_OK
            else
                log_end_msg $SCRIPT_ERROR
            fi
            ;;
        restart|force-reload)
            log_daemon_msg "Restarting $DESCRIPTION" $NAME
            if phpfcgid_stop; then
                if phpfcgid_start; then
                    log_end_msg $SCRIPT_OK
                else
                    log_end_msg $SCRIPT_ERROR
                fi
            else
                log_end_msg $SCRIPT_ERROR
            fi
            ;;
        status)
            phpfcgid_status
            ;;
        *)
            cat << EOF >&2
Usage: $SCRIPT_NAME {start|stop|restart|force-reload|status}
EOF
            exit $SCRIPT_ERROR
            ;;
    esac
}
 
parse_script_option_list $@

f. 设置目录权限
这个步骤很关键,必须认真仔细设置,如无特殊需要,不要修改。如果我错了,感谢指正。

sudo chown -R www-data:www-data /var/web/username/config
sudo chmod 0755 /var/web/username/config
sudo chmod 0640 /var/web/username/cofnig/vhost
 
sudo chown -R www-data:www-data /var/web/username/cert
sudo chmod 0750 /var/web/username/cert
sudo chmod 0640 /var/web/username/cert/cert.pem
sudo chmod 0640 /var/web/username/cert/cert.key
 
sudo chown -R root:root /var/web/username/bin
sudo chmod -R 0755 /var/web/username/bin
 
sudo chown username:www-data /var/web/username/run
sudo chmod 0750 /var/web/username/run
 
sudo chown root:root /home/username
sudo chown -R username:www-data /home/username/web
sudo chmod 0755 /home/username
sudo chmod 0750 /home/username/web
 
# 注:/home/username/web/www 目录中的文件和目录全部要求是 username:sftp 隶属,文件权限 0644 目录权限 0755。

g. 启用此用户的虚拟主机

sudo ln -s /var/web/username/config/vhost /etc/nginx/sites-enabled/username
sudo nginx -t # 重启前测试脚本有没有错误
sudo service nginx reload

PS:所有的虚拟用户都隶属于 sftp 用户组,本方案建议使用 SFTP 作为用户的文件传输窗口。在 sshd_config 中对 sftp 组用户做必要的限制,如禁止转发和 Chroot 等等。

Over!

11 thoughts on “LNMP 多用户虚拟主机方案”

  1. 啊啊啊,写读后感来了。

    恕我直言,现在主流的虚拟主机需要给用户提供rewrite支持,
    当然要是能提供对阉割版php.ini的支持更好。

    从补丁和此页面的策略看,貌似无法支持。
    —————————————————————————————
    呃,我再检查了一下,用户的~目录属主已经变成了root:root,
    /home/username只代表一个标志,
    “家目录”意义大变(貌似不能终端登录,家目录意义本就已经变了)。

    惯性思维……各位也仔细看看,/var/web/username/{bin,cert,conf,run}
    这些子目录,权限设置非常微妙!

    还有nginx.conf中应该有一句include /etc/nginx/sites-enabled/*的,
    是整个用户虚拟主机运行的入口,才会运行Mucid兄的说的内涵。

    用户到期貌似就是删除:
    /etc/nginx/sites-enabled/username
    /home/username
    /var/web/username
    即可
    —————————————————————————————
    再啰嗦下,用户rewrite和限制版php.ini支持,不知可有实现方案?

    按博主这样的思路,应该会提供一个用户web表单提交rewrite和php.ini,
    idc的客服提供接到表单后转交给技术支撑检查后再设置……
    或者用户直接联系客服人工改……或者直接不支持。

    还有mysql数据库位置,还有最头疼的资源占用,内存php能设置
    进程最大占用还好说,磁盘quota、带宽、cpu……

    见笑了。

Leave a Reply

Your email address will not be published. Required fields are marked *