把项目管理搬回自己服务器:Windshift部署与避坑

简介

什么是 Windshift?

Windshift 是一个开源的自托管工作管理平台,目标是把 Jira 的深度保留下来,同时去掉 Jira 的沉重。它用一个 Go 二进制文件打包了 Svelte 前端,默认使用内嵌 SQLite,也可以换成 PostgreSQL。

对于受够了 Jira 每年涨价、插件生态层层收费,又不想退到 Trello 那种只能看板的团队,Windshift 提供了一个折中方案:工作项、Epic、Sprint、自定义工作流、自动化、服务台、SLA、知识库、测试管理,全部内置。

主要特点

  • Jira 级的规划能力 :看板、Backlog、迭代、Epic 与子任务、层级树、路线图、仪表盘一应俱全,支持工作项类型的自由配置
  • 每个工作区独立配置:工作项类型、自定义字段、界面、状态、工作流、优先级、权限都按工作区隔离,互不干扰
  • JQL 风格查询语言 :内置 Windshift Query Language,过滤条件可以保存为共享集合(Collection),驱动看板、列表、日历、路线图
  • 事件驱动自动化 :规则可以在事件发生时改字段、发通知、创建工作项,不需要再买 Marketplace 上的自动化插件
  • 服务台一体化 :SLA 指标与目标、队列视图、预设回复、审批、客户门户全部内置,无需单独部署 Jira Service Management
  • 替代周边工具 :知识库页面顶掉 Confluence,测试用例/测试运行顶掉 Xray、Zephyr,资产 CMDB 顶掉 Insight,时间跟踪也是原生的
  • Jira 导入器 :内置迁移工具,支持 Jira Cloud 与 Data Center 的项目、评论、附件、worklog、看板、Sprint、自定义字段导入
  • 九种语言 :含简体中文、阿拉伯语(完整 RTL 镜像布局)、日韩俄德法西葡
  • 单二进制、无外部依赖 :不需要 Java,不需要 Elasticsearch 集群,约 70 MB 一个文件,树莓派也能跑
  • 开源免费 :基于 AGPL-3.0 协议开源,无用户数限制,无需 License Key

应用场景

  • 团队从 Jira 迁移:受够了按人头计费与插件订阅,用内置导入器把项目整体搬过来,数据留在自己服务器上
  • 中小团队自建项目管理 :五到五十人的团队,不想维护 Confluence + Jira + Xray + Insight 这一套,一个容器全解决
  • 外包与客户支持 :客户门户 + SLA + 队列,对外提供工单入口,对内用同一套工作项跟踪进度
  • 研发流程闭环 :关联 GitHub、GitLab、Gitea、Forgejo,分支和 PR 直接绑到工作项上,提交记录自动回流
  • 内网/合规环境:完全离线可用,数据不出机房,适合对数据主权有要求的场景

Windshift 是一个把 Jira 核心能力和周边插件压缩进单个容器的自托管方案,适合想拿回数据控制权、又不愿意在功能上做减法的团队。

安装

官方镜像只发布在 ghcr.io,群晖的 Docker 套件搜不到,需要用命令行拉取。

本文写作时,最新稳定版本为 v0.9.1(2026-10-21 发布),老苏选择了最新的 main

准备工作

通过 SSH 登录到您的群晖,执行下面的命令,先创建数据目录,并生成一个固定的密钥:

bash 复制代码
# 新建文件夹 windshift 和 子目录
mkdir -p /volume1/docker/windshift/{data,pgdata}

# 进入 windshift 目录
cd /volume1/docker/windshift

# 生成 SSO_SECRET,只生成一次,之后保持不变
openssl rand -hex 32

SSO_SECRET 用于签名会话 Cookie 和加密 SSO 凭据。一旦更换,所有人会被踢下线,已保存的 SSO/集成凭据也会失效。

env.txt

配置文件主要就下面几个参数需要调整

  • SERVER_IP:服务器 IP
  • PORT:主机端口
  • SSO_SECRET:上一步中 openssl rand -hex 32 生成
  • POSTGRES_PASSWORD:数据库密码
ini 复制代码
# =============================================================================
# Windshift 环境变量配置 ------ 纯 HTTP + 服务器 IP 直连版
# =============================================================================
#
# 快速开始:
#   1. cp .env.example .env
#   2. 编辑 .env,至少填 SERVER_IP、SSO_SECRET、POSTGRES_PASSWORD
#   3. docker compose up -d
#   4. 浏览器访问 http://<SERVER_IP>:8080
#
# 本文件与同目录 docker-compose.yml 的 ${变量} 一一对应,改这里即生效。
# 写死在 compose 里的固定值见文末「附录:固定配置」。
#
# 重要提示:
#   - 这是明文 HTTP 部署,会话和数据不加密,仅适用于可信内网或试用环境。
#   - 修改 POSTGRES_* 后旧数据卷不会迁移,如需重建请先备份再删除卷。
# =============================================================================


# -----------------------------------------------------------------------------
# 1. 访问地址(必填)
# -----------------------------------------------------------------------------
# 服务器 IP 或内网域名。必须是浏览器能直接访问到的地址,
# 它决定登录回调、邮件链接、日历订阅里的域名,填错会导致登录后反复跳转。
SERVER_IP=192.168.0.197

# 对外暴露的端口,默认 8080。改了这里,访问地址同步变成 http://SERVER_IP:新端口
PORT=8256


# -----------------------------------------------------------------------------
# 2. 安全密钥(必填)
# -----------------------------------------------------------------------------
# 会话签名与 SSO 凭据加密密钥。生成方式:
#   openssl rand -hex 32
# 生成一次后固定不变。更换会导致所有人被踢下线,且已加密的 SSO/集成凭据失效。
SSO_SECRET=606588438a0363a8ed5611d9ea5fd8de667befbfd44186e7a6ea7a36ae55e104


# -----------------------------------------------------------------------------
# 3. 数据库(PostgreSQL,必填)
# -----------------------------------------------------------------------------
POSTGRES_USER=windshift

# 请改为强密码。密码含 @ : / ? # 等字符也安全,连接串会自动转义。
POSTGRES_PASSWORD=change-me-please

POSTGRES_DB=windshift

# 数据库连接 TLS 模式:disable / allow / prefer / require / verify-ca / verify-full
# 同机容器内网通信保持 disable;指向远程或托管 PostgreSQL 时改 require 或更严格。
POSTGRES_SSLMODE=disable

# 连接池预算(一般无需修改):
#   副本数 × (MAX_READ_CONNS + 5) + POSTGRES_CONNECTION_HEADROOM ≤ PostgreSQL 的 max_connections
# PostgreSQL 默认 max_connections=100,当前配置 1×(30+5)+10=45,安全。
MAX_READ_CONNS=30
POSTGRES_REPLICA_COUNT=1
POSTGRES_CONNECTION_HEADROOM=10


# -----------------------------------------------------------------------------
# 4. 内存预算(可选,两个值需成对调整)
# -----------------------------------------------------------------------------
# 进程内部内存预算,单位 MiB。运行时堆上限和缓存分配由它推导。
WINDSHIFT_MEMORY_LIMIT_MB=2048

# 容器内存硬限额,必须 ≥ 上面的进程预算,否则进程会先被 OOM Killer 杀掉。
WINDSHIFT_CONTAINER_MEMORY_LIMIT=2g


# -----------------------------------------------------------------------------
# 5. 会话与出网策略(可选)
# -----------------------------------------------------------------------------
# 会话 IP 变化时的处理方式:
#   log    - 记录日志但放行,并把新 IP 更新为绑定点(默认,推荐先用这个观察)
#   strict - 直接拒绝,要求重新登录
#   off    - 完全不比较客户端 IP
SESSION_IP_BINDING=log

# true 允许服务端 HTTP 客户端访问回环/内网地址。
# 需要连内网 Gitea/Jira、宿主机本地 LLM(如 http://172.17.0.1:11434/v1)时保持 true。
# 设为 false 会屏蔽所有私网目标,上述集成全部失效(含 OIDC、SMTP、webhook)。
ALLOW_LOCAL_CONNECTIONS=true

# true 表示对出站 TLS 跳过证书校验(SMTP / IMAP / LDAP / HTTPS 集成)。
# 仅当目标使用自签证书时开启;它会对整个进程关闭服务器身份校验。
TLS_SKIP_VERIFY=false


# -----------------------------------------------------------------------------
# 6. 日志(可选)
# -----------------------------------------------------------------------------
# 级别:debug / info / warn / error
LOG_LEVEL=info

# 格式:json / text / logfmt
LOG_FORMAT=json


# -----------------------------------------------------------------------------
# 7. 附件存储(可选,默认本地磁盘)
# -----------------------------------------------------------------------------
# local 表示存到 /data/attachments(windshift-data 卷内);
# 改为 s3 则使用下面的 S3 兼容对象存储。
STORAGE_BACKEND=local

# 以下仅在 STORAGE_BACKEND=s3 时生效。
# AWS S3:S3_ENDPOINT 留空、S3_ACCESS_KEY_ID/S3_SECRET_ACCESS_KEY 都留空,
#         走 AWS 官方凭据链(环境变量、SSO、实例角色)。
# MinIO / RustFS / Wasabi 等自建:填 S3_ENDPOINT,通常还需 S3_FORCE_PATH_STYLE=true。
S3_ENDPOINT=
S3_BUCKET=
S3_REGION=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_FORCE_PATH_STYLE=false
S3_PREFIX=


# -----------------------------------------------------------------------------
# 8. Web Push 推送(可选)
# -----------------------------------------------------------------------------
# 三项留空即可:首次启动会自动生成 VAPID 密钥对并存入数据库,推送直接可用。
# 仅当你想自行管理或轮换密钥时才填写(密钥对必须长期稳定,轮换会使所有订阅失效)。
# 自行生成:npx web-push generate-vapid-keys
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=

# VAPID 的 sub 联系方式,mailto: 或 https: 开头;留空则使用 BASE_URL。
VAPID_SUBJECT=


# -----------------------------------------------------------------------------
# 9. 监控(可选)
# -----------------------------------------------------------------------------
# 设置后 /metrics 需携带 "Authorization: Bearer <token>";留空则公开,
# 此时应在网络层限制访问。
WINDSHIFT_METRICS_TOKEN=


# =============================================================================
# 附录:固定配置(在 docker-compose.yml 中写死,改本文件无效)
# =============================================================================
#
# BASE_URL=http://<SERVER_IP>:<PORT>
#   由 SERVER_IP 与 PORT 自动拼装。若要换域名或加子路径,改 compose 里的这一行。
#
# ALLOW_INSECURE_HTTP=true
#   允许浏览器从非 localhost 的明文 HTTP 源做带凭据请求。
#   不开启时容器能正常启动,但日志报 "Failed to create CORS middleware ... insecure origin
#   patterns",之后浏览器所有请求返回 CORS_CONFIG_ERROR。
#
# USE_PROXY=false
#   无反向代理时必须为 false。为 true 会信任 X-Forwarded-Proto / X-Forwarded-For,
#   可被伪造,导致 IP 限流与安全日志失真。
#
# PORT=8080(容器内)
#   容器内监听端口,固定值;对外端口由上面第 1 节的 PORT 决定。
#
# DB_TYPE=postgres / POSTGRES_HOST=db / POSTGRES_PORT=5432
#   使用 compose 内置的 db 服务,主机名固定为服务名 db。
#
# ATTACHMENT_PATH=/data/attachments / PLUGIN_DIR=/data/plugins
#   位于 windshift-data 卷内,随卷持久化。
#
# CODING_AGENT_ENABLED=true
#   开启 coding-agent 编排面(只排队与分发,实际执行在远程 runner 上)。
#
# --- 如果要换成 HTTPS(推荐正式使用)---
# 1. 在 compose 中把 BASE_URL 改为 https://域名
# 2. 删除 ALLOW_INSECURE_HTTP=true(或显式设 false)
# 3. 在 Windshift 前面加 Caddy/Nginx 终止 TLS,并把 USE_PROXY 改为 true
# 4. 加反代后不要再对外暴露 8080,只允许反代访问
# 5. 如需 passkey 登录,额外设置 WEBAUTHN_RP_ID=域名(裸 IP 不是合法 RP ID)
# =============================================================================

记得保存时要用 UTF-8 编码

docker-compose.yml

将下面的内容保存为 docker-compose.yml 文件:

yaml 复制代码
# Windshift --- 纯 HTTP + 服务器 IP 部署(内网/测试用)
# 用法:
#   1. 复制 .env.example 为 .env,改 SERVER_IP / SSO_SECRET
#   2. docker compose up -d
#   3. 浏览器访问 http://<SERVER_IP>:8080
services:
  windshift:
    image: ghcr.io/windshiftapp/windshift:main
    container_name: windshift-web
    pull_policy: always
    restart: unless-stopped
    # 直连 IP:8080,必须显式暴露端口(原 compose-main.yml 是给 Dokploy/反代用的,没有 ports)
    ports:
      - "${PORT:-8080}:8080"
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    # 必须:scratch 镜像内没有 /tmp,大附件上传和 coding-agent 都依赖它
    tmpfs:
      - /tmp:exec,size=64M
    environment:
      # 关键三条:http + IP
      - BASE_URL=http://${SERVER_IP}:${PORT}
      - ALLOW_INSECURE_HTTP=true      # 允许非 localhost 的明文 HTTP 源(否则 CORS 中间件创建失败)
      - USE_PROXY=false               # 没有反代就必须是 false,否则会信任伪造的 X-Forwarded-*
      - PORT=8080
      - SSO_SECRET=${SSO_SECRET}
      - SESSION_IP_BINDING=${SESSION_IP_BINDING:-log}
      - ATTACHMENT_PATH=/data/attachments
      - STORAGE_BACKEND=${STORAGE_BACKEND:-local}
      - PLUGIN_DIR=/data/plugins
      - DB_TYPE=postgres
      - POSTGRES_HOST=db
      - POSTGRES_PORT=5432
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - MAX_READ_CONNS=${MAX_READ_CONNS:-30}
      - POSTGRES_REPLICA_COUNT=${POSTGRES_REPLICA_COUNT:-1}
      - POSTGRES_CONNECTION_HEADROOM=${POSTGRES_CONNECTION_HEADROOM:-10}
      - ALLOW_LOCAL_CONNECTIONS=${ALLOW_LOCAL_CONNECTIONS:-true}
      - CODING_AGENT_ENABLED=true
      - LOG_LEVEL=info
      - LOG_FORMAT=json
      - WINDSHIFT_MEMORY_LIMIT_MB=${WINDSHIFT_MEMORY_LIMIT_MB:-2048}
    volumes:
      - ./data:/data
    mem_limit: ${WINDSHIFT_CONTAINER_MEMORY_LIMIT:-2g}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "/windshift", "healthcheck"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

  db:
    image: postgres:17
    container_name: windshift-db
    restart: unless-stopped
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

PostgreSQL 默认 max_connections 是 100。Windshift 的连接预算是「副本数 × (MAX_READ_CONNS + 5) + 预留」,启动时会检查这个预算是否安全,超了会在日志里告警。

执行以下命令启动:

bash 复制代码
# 进入 windshift 目录
cd /volume1/docker/windshift

# 将 docker-compose.yml 和 env.txt 放入当前目录

# 一键启动
docker-compose --env-file env.txt up -d  

运行

在浏览器中访问 http://<群晖IP>:8256 即可进入语言设置

接下来是管理员设置

模块默认即可

刚开始使用,还需要创建工作区

输入名称名称和键值(Key)

创建第一块工作区时有个坑:Key 不是名称 。它是工作项编号的前缀(如 DEV-123),规则是从源码里写死的------只允许 A-Z a-z 0-9,长度 2 到 10 位,不能有空格、中文、连字符。名称可以随便用中文,但 Key 请单独填一段英文缩写,比如 DEV、OPS、CS。Key 全局唯一,且建好后改动会让所有历史工作项编号前缀一起变,建议一次定好。

接下来还需要创建工作项

例如:编写 Windshift 安装教程

还可以设置更详细的子项、附件、说明等

更具体的使用请参考官方的说明文档

注意事项

  1. /tmp 的 tmpfs 不能省 :镜像是 scratch 构建,里面没有 /tmp 目录。缺少这个挂载,大附件上传会在超出 32 MiB 内存阈值后写盘失败。必须用短语法 - /tmp:exec,size=64M,Docker 默认的 noexec 会让 git 无法执行 askpass 辅助程序。
  2. 明文 HTTP 只支持 localhost :BASE_URL 填 http://192.168.0.100:8080 时,容器能正常启动,但日志会报 Failed to create CORS middleware ... insecure origin patterns,之后浏览器所有请求返回 CORS_CONFIG_ERROR。两种解法:加反向代理做 HTTPS(推荐),或在内网可信环境下显式开启 ALLOW_INSECURE_HTTP=true。
  3. Passkey 与 PWA 需要 HTTPS :明文 HTTP 下账号密码登录正常,但 Passkey 注册、PWA 安装、Web Push 推送都无法使用,因为浏览器要求安全上下文,且裸 IP 不是合法的 RP ID。
  4. USE_PROXY 只在有反代时为 true :为 true 会信任 X-Forwarded-Proto 和 X-Forwarded-For,直接暴露端口时可被伪造,导致 IP 限流与安全日志失真。用 Traefik/Nginx 时改为 true,并停止对外暴露 8080。
  5. 备份 /data 整个目录 :SQLite 模式下数据库、附件、插件、生成的 VAPID 密钥都在同一个卷里。用 PostgreSQL 也别漏了附件目录,数据库备份不能覆盖它。
  6. 内存给足 2G :看板、树视图、SSE 长连接都比较吃内存。1G 能跑起来,但几十人同时在线会开始换页。WINDSHIFT_MEMORY_LIMIT_MB 与容器的 mem_limit 要成对调整。
  7. pull_policy: always 的意义 :如果跟随 main 这类可变 tag,Dokploy 等平台执行 compose up -d 前不会自动 pull,可能一直跑旧镜像。固定版本号(如 v0.9.1)最稳。
  8. 中文界面已内置:九种语言里包含简体中文,登录后在个人设置里切换,不需要额外装语言包。

参考文档

Windshiftapp/core: Self-hosted, open-source work management platform and Jira alternative (AGPL v3)

地址:https://github.com/Windshiftapp/core
Windshift 官方网站

地址:https://windshift.sh
Docker 部署文档(含反向代理、PostgreSQL、WebAuthn 说明)

地址:https://windshift.sh/self-hosting/02-deployment/01-docker
配置项完整说明(环境变量与命令行参数对照)

地址:https://windshift.sh/self-hosting/03-configuration/01-options

相关推荐
木雷坞1 小时前
Baserow Docker部署:导入设备借用CSV,再用表单新增登记
运维·docker·容器
wefg13 小时前
【Docker】基本指令
运维·docker·容器
牛马工作号4 小时前
Copyparty 轻量文件共享:Docker Compose 部署、权限与多协议使用全指南
运维·docker·容器
oioihoii5 小时前
Windows部署MySpeed自建测速服务:Docker运行后直接测速,再配置固定公网地址远程查看
运维·docker·容器
Jack N7 小时前
【Strapi 系列之八】二次开发 + 生产部署,Docker + PostgreSQL + Nginx 一把梭
nginx·docker·postgresql·strapi
岁月宁静18 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent
正经教主1 天前
【FDE系列】阶段2:Day 42(UP详细版):Dockerfile 实战 — 把你的应用打包成镜像
人工智能·docker·fde
刘马想放假1 天前
Copyparty 轻量文件共享:Docker Compose 部署、权限与多协议使用全指南
docker·容器
岁寒良木1 天前
麒麟 V10 (x86_64) 离线/联网部署 Docker 26.1.4 与 MaxKB v2.10 LTS 实战指南
python·docker·kylin