把项目管理搬回自己服务器: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 登录到您的群晖,执行下面的命令,先创建数据目录,并生成一个固定的密钥:

1
2
3
4
5
6
7
8
# 新建文件夹 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:数据库密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# =============================================================================
# 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 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 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) + 预留」,启动时会检查这个预算是否安全,超了会在日志里告警。

执行以下命令启动:

1
2
3
4
5
6
7
# 进入 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