yanchang
yanchang
发布于 2026-06-20 / 7 阅读
0
0

在复杂家宽架构下部署项目管理系统 Plane

碎碎念

昨晚的话,开会,但是发现现在的话团队其实是有很大的问题的,就是每个人都不知道要做什么,这个问题其实比较牙疼的,首先是工作节点不清晰,虽然说这个团队只有几个人但是几个人之间工作不够明确,分工不明确,而且现在手头上的活越来越多,工作越来越杂,所以现在只能尝试找个工具,促进一下多个人的协作吧

为了更好地进行项目管理,我决定在我家里的服务器上部署一个开源项目管理工具:Plane

这本该是一个简单的 docker compose up -d 就能解决的问题,但由于我特殊的网络架构,硬是生出了一堆极其隐蔽的网络和协议坑。

我的服务器访问链路如下: 用户浏览器 ➜ 腾讯云 EdgeOne (边缘加速节点,暴露 443 端口) ➜家庭宽带路由器 (做端口映射) ➜ 宿主机 Nginx (监听特定的 **** 回源端口) ➜ Docker 内部网络 (Plane 代理容器 **** 端口) ➜ Plane 后端微服务群。

1. 下载最新版部署脚本

依次运行以下命令新建文件夹并下载最新的 setup.sh

Bash

# 创建一个专门的文件夹并进入
mkdir -p /opt/plane
cd /opt/plane

# 下载最新的官方部署脚本
curl -fsSL -o setup.sh https://github.com/makeplane/plane/releases/latest/download/setup.sh

# 赋予执行权限
chmod +x setup.sh

2. 初始化项目(生成配置)

运行刚才下载的脚本:

Bash

./setup.sh

这时候终端会弹出一个菜单,让你选择数字:

Select a Action you want to perform:

  1. Install (x86_64)

  2. Start ...

输入 1 并回车。 这步不会马上启动容器,而是会在当前目录拉取最新的 docker-compose.yaml.env(也就是前面说过的关键环境变量文件)。

3. 配置环境变量(非常关键!)

安装完成后,当前目录会多出一个 plane-app 文件夹(或者直接在当前目录下生成了 .env)。 使用文本编辑器打开 .env 文件:

Bash

nano plane.env  # 或者 vim plane.env
APP_DOMAIN=plane.yccs.cc
APP_RELEASE=v1.3.1

WEB_REPLICAS=1
SPACE_REPLICAS=1
ADMIN_REPLICAS=1
API_REPLICAS=1
WORKER_REPLICAS=1
BEAT_WORKER_REPLICAS=1
LIVE_REPLICAS=1

LISTEN_HTTP_PORT=****
LISTEN_HTTPS_PORT=

WEB_URL=https://${APP_DOMAIN}
DEBUG=0
CORS_ALLOWED_ORIGINS=https://${APP_DOMAIN}
API_BASE_URL=http://api:8000

#DB SETTINGS
PGHOST=plane-db
PGDATABASE=plane
POSTGRES_USER=plane
POSTGRES_PASSWORD=plane
POSTGRES_DB=plane
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data
DATABASE_URL=

# REDIS SETTINGS
REDIS_HOST=plane-redis
REDIS_PORT=6379
REDIS_URL=

# RabbitMQ Settings
RABBITMQ_HOST=plane-mq
RABBITMQ_PORT=5672
RABBITMQ_USER=plane
RABBITMQ_PASSWORD=plane
RABBITMQ_VHOST=plane
AMQP_URL=

# If SSL Cert to be generated, set CERT_EMAIl="email <EMAIL_ADDRESS>"
CERT_ACME_CA=https://acme-v02.api.letsencrypt.org/directory
TRUSTED_PROXIES=0.0.0.0/0
SITE_ADDRESS=:80
CERT_EMAIL=



# For DNS Challenge based certificate generation, set the CERT_ACME_DNS, CERT_EMAIL
# CERT_ACME_DNS="acme_dns <CERT_DNS_PROVIDER> <CERT_DNS_PROVIDER_API_KEY>"
CERT_ACME_DNS=


# Secret Key
SECRET_KEY=60gp****************************

# DATA STORE SETTINGS
USE_MINIO=1
AWS_REGION=
AWS_ACCESS_KEY_ID=access-key
AWS_SECRET_ACCESS_KEY=secret-key
AWS_S3_ENDPOINT_URL=http://plane-minio:9000
AWS_S3_BUCKET_NAME=uploads
FILE_SIZE_LIMIT=524288000

# Gunicorn Workers
GUNICORN_WORKERS=3

# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
# DOCKER_PLATFORM=linux/amd64

# Force HTTPS for handling SSL Termination
MINIO_ENDPOINT_SSL=0

# API key rate limit
API_KEY_RATE_LIMIT=60/minute

# Live server environment variables
# WARNING: You must set a secure value for LIVE_SERVER_SECRET_KEY in production environments.
LIVE_SERVER_SECRET_KEY=

# Webhook IP allowlist — comma-separated IPs or CIDR ranges allowed as webhook targets
# even if they resolve to private networks (e.g. "10.0.0.0/8,192.168.1.0/24,172.16.0.5")
WEBHOOK_ALLOWED_IPS=

# Webhook hostname allowlist — comma-separated hostnames that bypass the private-IP
# SSRF check. Useful for trusted internal services whose container/service IPs are
# dynamic (e.g. "silo,silo.namespace.svc.cluster.local")
WEBHOOK_ALLOWED_HOSTS=
DOCKERHUB_USER=makeplane
PULL_POLICY=if_not_present
CUSTOM_BUILD=false


4. 正式启动服务

环境变量改好之后,再次运行管理脚本:

Bash

./setup.sh

这次在弹出的菜单中,输入 2 (Start) 并回车

系统会自动开始 docker-compose up -d,从远端拉取 PostgreSQL、Redis、Minio 以及 Plane 的主程序镜像。由于镜像较多,这步大概需要几分钟。

当看到终端提示 "Plane is running"(或者所有容器状态为 Started),就大功告成了。

5. 访问与配置 Nginx

一般来说家庭配置的服务,外部访问都应该是有一个统一的访问端口的,所以在这里配置的nginx的文件为

如果你刚才把 .env 里的 NGINX_PORT 改成了 ****,那么你的本地 Nginx 反向代理配置(plane.conf)应该是这样的:

Nginx

server {
    listen **** ssl;
    server_name plane.*****.****;

    ssl_certificate     /etc/nginx/ssl/www.yanchang.pem;
    ssl_certificate_key /etc/nginx/ssl/www.yanchang.key;

    # 允许无限大小的文件上传(针对任务附件)
    client_max_body_size 0;

    location / {
        proxy_pass http://127.0.0.1:****;

        # 不要用 $host,直接强制写死 CDN 域名
        proxy_set_header Host plane.yccs.cc;
        # 强制统一 Origin,彻底扼杀后端的 CORS 跨域拦截
        proxy_set_header Origin https://plane.yccs.cc;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 支持 WebSocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_request_buffering off;
        proxy_buffering off;

        proxy_connect_timeout 300;
        proxy_send_timeout 300;
        proxy_read_timeout 300;
        send_timeout 300;
    }
}

然后做出应用即可

sudo ln -s /etc/nginx/sites-available/plane.conf /etc/nginx/sites-enabled/

sudo nginx -t      # 测试配置是否正确

sudo nginx -s reload # 重启 Nginx

注意Plane 的 env 配置必须对应

6. 配置系统服务

还是创建系统配置文件

/etc/systemd/system/plane.service

添加以下内容

[Unit]
Description=Plane Project Management Service
Requires=docker.service
After=docker.service network-online.target

[Service]
Type=simple
Restart=always
RestartSec=10
WorkingDirectory=/opt/plane/plane-app

# 【核心修改】强制指定 -f docker-compose.yaml
ExecStartPre=-/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env down
ExecStart=/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env up
ExecStop=/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env down

[Install]
WantedBy=multi-user.target

然后启动系统服务

sudo systemctl daemon-reload

sudo systemctl enable plane

sudo systemctl start plane

sudo systemctl status plane

7. 边缘加速计算

还是老样子即可

坑点一:图片上传报错 ERR_CONNECTION_CLOSED

在完成基础的 docker 部署并配置好 Nginx 反向代理后,页面访问一切正常。但当我在新建项目尝试上传一张风景图作为封面时,界面直接抛出 Failed to upload cover image

打开浏览器 F12 开发者工具,发现网络面板里有一个向 /uploads 发起 POST 请求的记录,全都是报错:net::ERR_CONNECTION_CLOSED

一开始我以为是 Nginx 的 client_max_body_size 限制或者 Docker 目录权限问题,但在反复检查配置和容器日志后,发现后端 API 容器甚至返回了 200 OK 并生成了一个直传链接。

真正的死结在于“预签名 URL 陷阱”: Plane 的图片上传强依赖于内置的 MinIO (S3) 服务。前端会先向后端请求一个直传 URL,由于我之前在 .env 中把 APP_DOMAIN 写成了内部的回源域名,导致后端生成了一个指向内网域名的 HTTPS 链接。前端拿到这个链接后,傻乎乎地去撞击我宽带根本没有开放的 443 端口,TCP 握手瞬间失败,直接报 Connection Closed。

为了不折腾 EdgeOne 的回源设置,我选择直接在 Nginx 层面实施“降维打击”。打开 /etc/nginx/sites-available/plane.conf,将核心的 location / 配置修改如下:

Nginx

location / {
    proxy_pass http://127.0.0.1:****;
    # 不管边缘节点传过来的是啥,强制写死为对外的 CDN 域名
    proxy_set_header Host plane.yccs.cc;
    proxy_set_header Origin https://plane.yccs.cc;

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Plane 强依赖 WebSocket,必须加上
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    
    proxy_request_buffering off;
    proxy_buffering off;
}

重载 Nginx 后,后端乖乖生成了基于 CDN 域名的直传链接,图片瞬间上传成功!同时,这一招也顺带秒杀了 Django 后端因为来源域名不匹配而触发的 CSRF (403) 跨域拦截问题。

坑点二:Systemd 接管后突发 502 Bad Gateway

通常来说部署服务的话,是需要将服务设置为系统服务,方便实用systemctl管理,于是我写了一个标准的 Systemd 服务脚本准备让系统接管 Plane 的自启:

Ini, TOML

[Unit]
Description=Plane Service
Requires=docker.service
After=docker.service

[Service]
Type=simple
WorkingDirectory=/opt/plane/plane-app
ExecStart=/usr/bin/docker compose up
...

结果用 systemctl start plane 启动后,网页直接崩盘,满屏的 502 Bad Gateway nginx/1.24.0 (Ubuntu)

敲下 docker compose ps 后,我发现事情并不简单:原本负责接管 **** 端口的 proxy 容器离奇失踪了,而 **** 端口居然被负责纯前端渲染的 web 容器抢占。纯前端当然不懂得怎么分发后端的流量,Nginx 一把流量打过来,当场暴毙。

为啥会少一个容器? 翻看 Plane 官方的 setup.sh 源码才恍然大悟:

Bash

$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d

原来官方强制指定了读取 docker-compose.yamlplane.env。而我在 systemd 里只写了 docker compose up,Docker 默认像开盲盒一样读到了我目录下残留的旧版 .yml 文件(且丢失了环境变量),拉起了一个残缺不全的基础架构。

修改 /etc/systemd/system/plane.service,老老实实把官方参数补齐:

Ini, TOML

# 启动前先清理
ExecStartPre=-/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env down
# 前台拉起,便于 systemd 收集日志
ExecStart=/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env up
ExecStop=/usr/bin/docker compose -f docker-compose.yaml --env-file plane.env down

清理残局后重新 systemctl daemon-reloadstart,502 烟消云散,服务稳如泰山。

总结

这次 Plane 的部署踩坑,本质上是一场在复杂代理环境、Docker 虚拟网络、以及严格加密协议三者交织下的排雷行动。

  1. 遇事不决改 Nginx 头:当遇到 CSRF 拦截或预签名 URL 生成错误时,强行覆盖 Host 和 Origin 能解决 80% 的烦恼。

  2. Systemd 不相信盲盒:让系统守护 Docker 进程时,文件路径和 --env-file 参数一个字母都不能省。

  3. SMTP 连不上别总怪防火墙:当 465 报错时,换 587+TLS 往往能有奇效。


评论