Flarum 是一个简洁的轻论坛程序,交互体验做的十分不错,也有良好的插件扩展机制。接触过的人可能知道,它目前还在 beta,在功能更新和迭代方面不算稳定,部署、修改与定制功能更是一件麻烦的事情。
在 2018 年,我基于它构建了 0xFFFF 社区。经过两年的不断推翻与修改,慢慢沉淀下了一套适合持续迭代的 Flarum 部署与开发迭代方案。
这里主要介绍 Flarum 在服务器和本地开发环境的部署方案。本文假定读者对 Linux 命令行操作、Docker 与 Docker Compose 有基本的了解。相关文件均已开源在 GitHub: zgq354/flarum-docker-env。
在 Linux 折腾 LAMP/LNMP 的同学可能经常被各种环境配置的细节问题折磨,诸如 Nginx 配置、“伪静态”(URL Rewrite)、各种文件权限、所有者问题等等。好不容易配置好了,过一两个月可能已经完全忘记,在未来需要修改或更新之时,如西西弗斯受罚一般,重重复复做着相似的事。
基于 Docker,只需要一系列配置文件,就可以从各种各样的针对手动配置解放出来,通过 Git 管理配置的历史版本。可以随时切换环境配置,而不担心因时间的流逝忘记当初是怎么搞的。
接下来会介绍这个方案的细节,若只想把项目跑起来,可以直接跳到本文的 “使用” 小节。
官方 安装文档 对环境的要求:
本质上来说是一个基于 LAMP/LNMP 架构的应用,所以我们只需要准备三个东西:Web 服务器、PHP 和 数据库,这里用到三个应用容器:
再考虑到数据库管理、还有 HTTPS 证书签发的问题,我们再加上这俩:
在申请到 Let's Encrypt 证书之前,为了完整提供 HTTPS,Nginx 需默认提供使用自签名证书的选项。PHP-FPM 需要安装各种 PHP 扩展,所以 Nginx 与 PHP-FPM 会在基础镜像之上再做一些自定义修改。
为了开发迭代的方便,我们把网站主体文件放在宿主机,然后通过 Volume 的方式绑定 Docker 容器,这一点接下来会提到。
Docker 容器在设计用途上不考虑状态的持久化,每次更新配置,都会通过重新创建新的容器替换原本的容器,原本容器会被销毁。为了数据的持久化,Docker 提供了 Volume 的机制,将 Volume 挂载到容器文件系统的指定路径,写入的数据会通过 Volume 保留。我们把宿主机的特定路径作为 Volume,实现容器内目录和宿主机的映射。需持久化的有:
/var/lib/mysql
)本着 Docker 容器产生的文件都归于一处的原则,我们把相关的文件都归在宿主机下的 ./data
之下。网站主体代码也通过 Volume 挂载,这里放在 ./www
之下,整体目录结构安排如下:
.
├── data
│ ├── db-data # MySQL 数据文件
│ ├── logs # 日志文件
│ └── ssl # ssl 证书相关配置
├── docker-compose.yml
├── nginx # Nginx 镜像相关文件
│ ├── Dockerfile
│ ├── conf # Nginx 配置
│ └── start.sh
├── php-fpm # php-fpm 镜像相关
│ └── Dockerfile
└── www # 站点相关文件
本节将展开介绍各个容器的配置细节,包括 MySQL、Nginx、php-fpm、phpMyAdmin 以及 acme.sh 的证书申请机制。
MySQL 容器直接用官方镜像,通过 .env
设置环境变量,加载 MySQL 初始化的连接密码等。
services:
database:
image: mysql:5.7
restart: always
container_name: site-db
expose:
- 3306
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
volumes:
- ./data/db-data:/var/lib/mysql
Nginx 采用了基于 alpine 的镜像,体积较小。在配置上,大体参考了 Nginx 在发行版中的目录结构,并参考了 Debian 的 nginx 包的目录安排,再考虑 Nginx 镜像内部的结构,绑定了三个路径。
- ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf/conf.d:/etc/nginx/conf.d
- ./nginx/conf/snippets:/etc/nginx/snippets
各个路径的作用:
对于 Web 站点的文件,我们把容器内部 /www/flarum
绑定到本地的 ./www/flarum
。
nginx.conf 参考:
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$http_x_forwarded_for - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log;
sendfile on;
keepalive_timeout 65;
client_max_body_size 20M;
include conf.d/*.conf;
}
SSL 相关参数,毕竟你不是安全人员,自己配置并不稳妥,所以还是用 Mozilla 提供的工具 生成吧。
snippets/ssl-params.conf 参考:
# generated 2020-05-21, Mozilla Guideline v5.4, nginx 1.17.7, OpenSSL 1.1.1d, intermediate configuration, no HSTS, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&hsts=false&ocsp=false&guideline=5.4
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/ssl/dhparam.pem;
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
其中有一个比较关键的 DH 参数,也用 Mozilla 推荐的,我们把这个逻辑加到 Dockerfile。
curl https://ssl-config.mozilla.org/ffdhe2048.txt > /etc/ssl/dhparam.pem
站点相关配置,SSL 证书默认放在 /etc/ssl/certs/
的以域名命名的目录下,参考以下配置,这里证书相关的参数,引用了 snippets/ssl-params.conf
。
conf.d/flarum.conf
的配置参考如下:
server {
listen 80;
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/example.com/full.pem;
ssl_certificate_key /etc/ssl/certs/example.com/key.pem;
include snippets/ssl-params.conf;
# should be changed
server_name example.com;
root /www/flarum/public;
index index.php index.html;
server_tokens off;
access_log /var/log/nginx/flarum-access.log;
error_log /var/log/nginx/flarum-error.log;
# for let's encrypt
location /.well-known/ {
alias /.well-known/;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm-service:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Pass requests that don't refer directly to files in the filesystem to index.php
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# The following directives are based on best practices from H5BP Nginx Server Configs
# https://github.com/h5bp/server-configs-nginx
# Expire rules for static content
location ~* \.(?:manifest|appcache|html?|xml|json)$ {
add_header Cache-Control "max-age=0";
}
location ~* \.(?:rss|atom)$ {
add_header Cache-Control "max-age=3600";
}
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|mp4|ogg|ogv|webm|htc)$ {
add_header Cache-Control "max-age=2592000";
access_log off;
}
location ~* \.(?:css|js)$ {
add_header Cache-Control "max-age=31536000";
access_log off;
}
location ~* \.(?:ttf|ttc|otf|eot|woff|woff2)$ {
add_header Cache-Control "max-age=2592000";
access_log off;
}
# Gzip compression
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/vnd.api+json
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
}
在 Dockerfile 的配置上,为了避免进程无法停止、僵尸进程等问题,容器加入 dumb-init 作为入口程序。
考虑到证书可能不存在的情况,修改启动脚本加入检测证书是否存在的机制。若证书不存在,就调用 OpenSSL 自签一个证书,避免启动失败(但这个证书也不会被客户端信任),具体的域名则通过环境变量传入。
启动脚本 start.sh:
#!/bin/sh -
CERT_DOMAIN=${DOMAIN:-example.com}
if [[ ! -e /etc/ssl/certs/$CERT_DOMAIN/key.pem ]]; then
mkdir -p /etc/ssl/certs/$CERT_DOMAIN
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/certs/$CERT_DOMAIN/key.pem -out /etc/ssl/certs/$CERT_DOMAIN/full.pem \
-subj "/C=CN/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=$CERT_DOMAIN"
fi
nginx -g "daemon off;"
也就是说,这里预置了一个自签名的 ssl 证书,若不向 Let's Encrypt 申请证书,你对这一系列容器的 HTTPS 请求是不受浏览器信任的。
php-fpm 镜像较为简单,直接配置 Dockerfile,在 php:7.4-fpm-alpine
镜像的基础上再加上 gd
、pdo_mysql
、exif
扩展(缺啥补啥)。
还需要考虑 Docker 内用户的 UID 与宿主机用户的 UID 的对应关系,涉及到写入权限的问题。(Docker Volume 的文件所有者的 UID 与宿主机是同步的,可能同一 UID 对应不同的用户名)。
Dockerfile 如下:
FROM php:7.4-fpm-alpine
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && \
echo "Asia/Shanghai" > /etc/timezone
RUN apk add \
freetype \
freetype-dev \
libpng \
libpng-dev \
oniguruma-dev \
libjpeg-turbo \
libjpeg-turbo-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& apk del \
freetype-dev \
libpng-dev \
libjpeg-turbo-dev
RUN docker-php-ext-install pdo_mysql opcache exif
RUN apk --no-cache add shadow \
&& usermod -u 1000 www-data \
&& groupmod -g 1000 www-data \
&& rm /var/cache/apk/*
ENTRYPOINT ["docker-php-entrypoint"]
STOPSIGNAL SIGQUIT
EXPOSE 9000
CMD ["php-fpm"]
引入 phpmyadmin/phpmyadmin:fpm-alpine
镜像,镜像内的文件都在 /var/www/html
,这里我们将 phpMyAdmin 内的 /var/www/html
通过 Volume 映射到 Nginx 的 /www/pma
目录下,这样 Nginx 遇到静态文件请求可以直接通过 /www/pma
访问到,遇到动态文件请求时,则转发给 phpMyAdmin 的容器。
location ~ \.php$ {
try_files $uri /index.php$is_args$args;
fastcgi_pass pma-service:9000;
fastcgi_hide_header X-Powered-By;
# 传给 phpMyAdmin 容器的 php-fpm 的路径 (/var/www/html)
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
include fastcgi_params;
}
如上,在写处理 .php
后缀的 location 的转发配置时需要留意 /www/pma
与 /var/www/html
的差异。这时候我们需要引入 fastcgi_params
文件的预置参数,然后硬编码 SCRIPT_FILENAME
。
完整配置参考:conf.d/pma.conf
这里申请签发证书的部分,我们采用 acme.sh 的 Docker 方案,acme.sh 容器以守护进程的形式运行。
所有的证书相关文件都放在了容器的 /acme.sh
目录中,这里我们把它映射到 ./data/ssl/acmeout
里(具体参考 docker-compose.yml
的配置)。
Let's Encrypt 签发证书有多种验证方式,acme.sh 均有封装。若不希望配置 DNS,可以使用 HTTP 的方式验证,本方案将 acme.sh 容器的 /.well-known
映射到了宿主机的 ./data/ssl/.well-known
,Nginx 把 ./data/ssl/.well-known
映射到了 /.well-known
。
通过 alias 指令实现访问验证文件的效果,如 flarum.conf
中的例子:
# for let's encrypt
location /.well-known/ {
alias /.well-known/;
}
然后我们可以用 docker exec
,采用 HTTP 验证的途径来执行申请命令,稍等片刻即可申请好:
docker exec acme.sh --issue -d example.com -w /
申请好的证书需执行 acme.sh 的 deploy 部署到 nginx 中,用环境变量加载参数,同样以 example.com 为例。
docker exec \
-e DEPLOY_DOCKER_CONTAINER_LABEL=sh.acme.autoload.domain=example.com \
-e DEPLOY_DOCKER_CONTAINER_KEY_FILE=/etc/ssl/example.com/key.pem \
-e DEPLOY_DOCKER_CONTAINER_CERT_FILE="/etc/ssl/example.com/cert.pem" \
-e DEPLOY_DOCKER_CONTAINER_CA_FILE="/etc/ssl/example.com/ca.pem" \
-e DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/ssl/example.com/full.pem" \
-e DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="kill 1" \
acme.sh --deploy -d example.com --deploy-hook docker
然后 acme.sh 的守护进程将会定期检查,在证书快过期的时候自动执行续期逻辑。在执行完续期逻辑后,会在标记了 sh.acme.autoload.domain=example.com
的标签的 nginx 容器执行 kill 1
,干掉这个容器的进程,自动重启容器,实现证书的重新加载。
version: "3.6"
services:
database:
image: mysql:5.7
restart: always
container_name: site-db
expose:
- 3306
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
volumes:
- ./data/db-data:/var/lib/mysql
nginx:
image: nginx-flarum
build:
context: ./nginx
args:
- DOMAIN=${DOMAIN}
container_name: site-nginx
restart: always
ports:
- 80:80
- 443:443
volumes:
- ./data/logs:/var/log/nginx
- ./data/ssl/.well-known:/.well-known
- ./data/ssl/certs:/etc/ssl/certs
- ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/conf/conf.d:/etc/nginx/conf.d
- ./nginx/conf/snippets:/etc/nginx/snippets
- ./www/flarum:/www/flarum
- pma-root:/www/pma # phpMyAdmin
environment:
- DOMAIN=${DOMAIN}
extra_hosts:
- "localhost:127.0.0.1"
labels:
- sh.acme.autoload.domain=${DOMAIN}
healthcheck:
test: ["CMD-SHELL", "wget -q --spider --proxy off http://localhost/get-health || exit 1"]
interval: 5s
retries: 12
logging:
driver: "json-file"
options:
max-size: "100m"
acme.sh:
image: neilpang/acme.sh
container_name: acme.sh
command: daemon
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data/ssl/acmeout:/acme.sh
- ./data/ssl/.well-known:/.well-known
environment:
- DEPLOY_DOCKER_CONTAINER_LABEL=sh.acme.autoload.domain=${DOMAIN}
- DEPLOY_DOCKER_CONTAINER_KEY_FILE=/etc/ssl/certs/${DOMAIN}/key.pem
- DEPLOY_DOCKER_CONTAINER_CERT_FILE="/etc/ssl/certs/${DOMAIN}/cert.pem"
- DEPLOY_DOCKER_CONTAINER_CA_FILE="/etc/ssl/certs/${DOMAIN}/ca.pem"
- DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/etc/ssl/certs/${DOMAIN}/full.pem"
- DEPLOY_DOCKER_CONTAINER_RELOAD_CMD="kill 1"
php-fpm-service:
image: php-fpm-flarum
build: ./php-fpm
container_name: site-php-fpm
restart: always
expose:
- 9000
volumes:
- ./data/logs:/var/log
- ./www/flarum:/www/flarum
healthcheck:
test: ["CMD-SHELL", "pidof php-fpm"]
interval: 5s
retries: 12
logging:
driver: "json-file"
options:
max-size: "100m"
pma-service:
image: phpmyadmin/phpmyadmin:fpm-alpine
container_name: site-pma
restart: always
environment:
- PMA_HOST=site-db
volumes:
- pma-root:/var/www/html
volumes:
pma-root:
在开始使用本方案的环境之前,你需要在宿主机本地先把 Flarum 站点的文件准备好。
首先安装 PHP 包管理器 Composer:
wget -O composer-setup.php https://getcomposer.org/installer
php composer-setup.php --install-dir=bin --filename=composer
设置国内镜像(避免加载过慢,这里可以用阿里云的镜像)
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
这里我们假设站点文件都放在 /var/www/flarum
中(假设你有 /var/www
的所有者,若不是,可 sudo chown <你的用户名>:<你的用户名> /var/www
),执行安装。
cd /var/www/
mkdir flarum && cd flarum
composer create-project flarum/flarum . --stability=beta
等 composer 跑完,安装 Flarum 需要的文件已经准备好了。
无论是线上部署还是本地开发,套路都很一致。
cd /var/www
git clone https://github.com/zgq354/flarum-docker-env.git
cd flarum-docker-env
www/flarum
里面执行 composer create-project flarum/flarum . --stability=beta
加入安装文件)ln -s /var/www/flarum www/flarum
.env
文件,可参考 .env-example
cp .env-example .env
vim .env
DB_PASS,DB_ROOT_PASS 需改成实际想要的密码,Flarum:
DOMAIN=example.com
DB_NAME=flarum_db
DB_USER=flarum_db_user
DB_PASS=xxxxx
DB_ROOT_PASS=xxxxx
pma.conf
、flarum.conf
里面的 server_name
配置为对应的域名。docker-compose up -d
然后把域名解析至服务器所在 IP,就能打开安装界面了,安装时需注意,MySQL Host 应为 MySQL Docker 容器对应的 site-db
。
没有现成的域名?没关系,你可以参考接下来的本地环境的方案来将任意域名指向服务器的 IP。
完成以上步骤后,若需要跑在线上环境,还需按照前文 acme.sh 的部分的方式,申请 Let's Encrypt 认证的 ssl 证书。
本地环境开发,推荐使用 LightProxy 作为开发环境调试的代理工具,LightProxy 是开源抓包工具 whistle 的桌面版封装,可以用类似 hosts 的语法指定域名和 IP 的对应关系。
example.com 127.0.0.1
若在本地部署,按 127.0.0.1
的方式就可以在本地访问,开发环境与生产环境保持同一域名。
限于篇幅,关于本地开发环境的搭建、调试、版本管理等方案,我们下一篇文章再具体介绍。
参考:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有