---
title: 基于 Docker 的 Flarum 轻论坛部署方案
date: 2020-06-19T23:33:00+08:00
tags:
  - Docker
  - Flarum
  - 技术

---

> 2023.05.11 更新：
本方案的复杂度仍较高，不建议使用，本文仅作存档
目前的最新方案已有更方便的 沉淀改动、打包部署 流程
可以参考：[https://github.com/0xffff-one/flarum-0x](https://github.com/0xffff-one/flarum-0x)
> 

Flarum 是一个简洁的轻论坛程序，交互体验做的十分不错，也有良好的插件扩展机制。接触过的人可能知道，它目前还在 beta，在功能更新和迭代方面不算稳定，部署、修改与定制功能更是一件麻烦的事情。

在 2018 年，我基于它构建了 [0xFFFF 社区](https://0xffff.one/)。经过两年的不断推翻与修改，慢慢沉淀下了一套适合持续迭代的 Flarum 部署与开发迭代方案。

这里主要介绍 Flarum 在服务器和本地开发环境的部署方案。本文假定读者对 Linux 命令行操作、Docker 与 Docker Compose 有基本的了解。相关文件均已开源在 [GitHub: zgq354/flarum-docker-env](https://github.com/zgq354/flarum-docker-env)。

## Why Docker

在 Linux 折腾 LAMP/LNMP 的同学可能经常被各种环境配置的细节问题折磨，诸如 Nginx 配置、“伪静态”（URL Rewrite）、各种文件权限、所有者问题等等。好不容易配置好了，过一两个月可能已经完全忘记，在未来需要修改或更新之时，如西西弗斯受罚一般，重重复复做着相似的事。

基于 Docker，只需要一系列配置文件，就可以从各种各样的针对手动配置解放出来，通过 Git 管理配置的历史版本。可以随时切换环境配置，而不担心因时间的流逝忘记当初是怎么搞的。

接下来会介绍这个方案的细节，若只想把项目跑起来，可以直接跳到本文的 “使用” 小节。

## 镜像的选择

官方 [安装文档](https://flarum.org/docs/install.html#server-requirements) 对环境的要求：

- Apache (with mod_rewrite enabled) or Nginx
- PHP 7.2.9+ with the following extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
- MySQL 5.6+ or MariaDB 10.0.5+
- SSH (command-line) access to run Composer

本质上来说是一个基于 LAMP/LNMP 架构的应用，所以我们只需要准备三个东西：Web 服务器、PHP 和 数据库，这里用到三个应用容器：

- Nginx：Web 服务器，负责输出静态文件、将需要 PHP 处理的请求通过 [FastCGI 协议](https://en.wikipedia.org/wiki/FastCGI) 转发给 PHP-FPM
- PHP-FPM：PHP 的 FastCGI 进程管理器，接收 Web 服务器的 FastCGI 请求，执行对应的 PHP 脚本
- MySQL 5.7：网站专用数据库

再考虑到数据库管理、还有 HTTPS 证书签发的问题，我们再加上这俩：

- [phpMyAdmin 的 PHP-FPM 容器](https://hub.docker.com/r/phpmyadmin/phpmyadmin)
- [acme.sh](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)：一键向 Let's Encrypt 申请证书

在申请到 Let's Encrypt 证书之前，为了完整提供 HTTPS，Nginx 需默认提供使用自签名证书的选项。PHP-FPM 需要安装各种 PHP 扩展，所以 Nginx 与 PHP-FPM 会在基础镜像之上再做一些自定义修改。

为了开发迭代的方便，我们把网站主体文件放在宿主机，然后通过 Volume 的方式绑定 Docker 容器，这一点接下来会提到。

## 目录结构

Docker 容器在设计用途上不考虑状态的持久化，每次更新配置，都会通过重新创建新的容器替换原本的容器，原本容器会被销毁。为了数据的持久化，Docker 提供了 Volume 的机制，将 Volume 挂载到容器文件系统的指定路径，写入的数据会通过 Volume 保留。我们把宿主机的特定路径作为 Volume，实现容器内目录和宿主机的映射。需持久化的有：

1. 数据库数据的文件（MySQL 一般在 `/var/lib/mysql`）
2. Nginx 的 Web 访问日志、配置文件
3. 证书签发相关文件

本着 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

MySQL 容器直接用官方镜像，通过 `.env` 设置环境变量，加载 MySQL 初始化的连接密码等。

```yaml
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

Nginx 采用了基于 alpine 的镜像，体积较小。在配置上，大体参考了 Nginx 在发行版中的目录结构，并参考了 Debian 的 [nginx 包的目录安排](https://salsa.debian.org/nginx-team/nginx/-/tree/master/debian/conf)，再考虑 Nginx 镜像内部的结构，绑定了三个路径。

```
 - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf
 - ./nginx/conf/conf.d:/etc/nginx/conf.d
 - ./nginx/conf/snippets:/etc/nginx/snippets
```

各个路径的作用：

- nginx.conf：覆盖原始的配置文件
- conf.d：Nginx HTTP 服务与站点相关的配置，会被 nginx.conf include 进去
- snippets：各种代码段，这里放了一个 SSL 相关的配置

对于 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 提供的工具](https://ssl-config.mozilla.org/) 生成吧。

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。

```bash
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](https://www.infoq.cn/article/2016/01/dumb-init-Docker) 作为入口程序。

考虑到证书可能不存在的情况，修改启动脚本加入检测证书是否存在的机制。若证书不存在，就调用 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

php-fpm 镜像较为简单，直接配置 Dockerfile，在 `php:7.4-fpm-alpine` 镜像的基础上再加上 `gd`、`pdo_mysql`、`exif` 扩展（缺啥补啥）。

还需要考虑 Docker 内用户的 UID 与宿主机用户的 UID 的对应关系，涉及到写入权限的问题。（Docker Volume 的文件所有者的 UID 与宿主机是同步的，可能同一 UID 对应不同的用户名）。

Dockerfile 如下：

```docker
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/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](https://github.com/zgq354/flarum-docker-env/blob/66c98e3dde3b95fca8551a19844c78178332e9d1/nginx/conf/conf.d/pma.conf)

### Let's Encrypt 证书申请

这里申请签发证书的部分，我们采用 [acme.sh](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker) 的 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 验证的途径来执行申请命令，稍等片刻即可申请好：

```bash
docker exec acme.sh --issue -d example.com -w /
```

申请好的证书需执行 acme.sh 的 deploy 部署到 nginx 中，用环境变量加载参数，同样以 example.com 为例。

```bash
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`，干掉这个容器的进程，自动重启容器，实现证书的重新加载。

### 完整 docker-compose.yaml

```yaml
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 文件

在开始使用本方案的环境之前，你需要在宿主机本地先把 Flarum 站点的文件准备好。

首先安装 PHP 包管理器 Composer：

```bash
wget -O composer-setup.php https://getcomposer.org/installer
php composer-setup.php --install-dir=bin --filename=composer
```

设置国内镜像（避免加载过慢，这里可以用阿里云的镜像）

```bash
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
```

这里我们假设站点文件都放在 `/var/www/flarum` 中（假设你有 `/var/www` 的所有者，若不是，可 `sudo chown <你的用户名>:<你的用户名> /var/www` ），执行安装。

```bash
cd /var/www/
mkdir flarum && cd flarum
composer create-project flarum/flarum . --stability=beta
```

等 composer 跑完，安装 Flarum 需要的文件已经准备好了。

### 部署

无论是线上部署还是本地开发，套路都很一致。

1. 首先在宿主机安装 [Docker CE](https://docs.docker.com/engine/install/) 与 [Docker Compose](https://docs.docker.com/compose/install/)
2. 克隆项目代码（你也可以用这个 Template 创建自己的项目，再克隆，这样可以自己更新）

```bash
cd /var/www
git clone https://github.com/zgq354/flarum-docker-env.git
cd flarum-docker-env
```

1. 创建符号链接（若不想创建符号链接，也可以在 `www/flarum` 里面执行 `composer create-project flarum/flarum . --stability=beta` 加入安装文件）

```bash
ln -s /var/www/flarum www/flarum
```

1. 创建环境变量配置 `.env` 文件，可参考 `.env-example`

```bash
cp .env-example .env
vim .env
```

DB_PASS，DB_ROOT_PASS 需改成实际想要的密码，Flarum：

```bash
DOMAIN=example.com

DB_NAME=flarum_db
DB_USER=flarum_db_user
DB_PASS=xxxxx
DB_ROOT_PASS=xxxxx
```

1. 修改 nginx 配置，把 `pma.conf`、`flarum.conf` 里面的 `server_name` 配置为对应的域名。
2. 启动

```bash
docker-compose up -d
```

然后把域名解析至服务器所在 IP，就能打开安装界面了，安装时需注意，MySQL Host 应为 MySQL Docker 容器对应的 `site-db`。

![2291687024.jpg](https://cdn.zgq.me/assets/posts/2020/flarum-docker-deployment/2291687024.jpg)

[**安装界面**](https://cdn.zgq.me/assets/posts/2020/flarum-docker-deployment/2291687024.jpg)

没有现成的域名？没关系，你可以参考接下来的本地环境的方案来将任意域名指向服务器的 IP。

完成以上步骤后，若需要跑在线上环境，还需按照前文 acme.sh 的部分的方式，申请 Let's Encrypt 认证的 ssl 证书。

### 本地环境

本地环境开发，推荐使用 [LightProxy](https://github.com/alibaba/lightproxy) 作为开发环境调试的代理工具，LightProxy 是开源抓包工具 [whistle](https://github.com/avwo/whistle) 的桌面版封装，可以用类似 hosts 的语法指定域名和 IP 的对应关系。

```
example.com 127.0.0.1
```

若在本地部署，按 `127.0.0.1` 的方式就可以在本地访问，开发环境与生产环境保持同一域名。

限于篇幅，关于本地开发环境的搭建、调试、版本管理等方案，我们下一篇文章再具体介绍。

## 最后

使用有任何问题，可在文末留言，或在 [项目 issue](https://github.com/zgq354/flarum-docker-env/issues) 提出，也欢迎加入 [0xFFFF 社区交流群](https://0xffff.one/p/2-0xffff-intro)，一起玩耍！

参考：

- [使用 Docker 和 Traefik 搭建 Flarum 轻论坛应用 - 苏洋博客](https://soulteary.com/2019/07/16/building-a-flarum-forum-app-with-docker-and-traefik.html)
- [0xffff-one/0xffff-env](https://github.com/0xffff-one/0xffff-env)
