打包格式
Town OS 的软件包是一份 YAML 定义,描述如何运行一个容器化服务, 包括它的镜像、网络、存储,以及面向用户的配置提问。 软件包也可以运行虚拟机,或通过 Proton 运行 Windows 应用。
仓库结构
软件包仓库就是任意一个包含 packages/ 目录的 git 仓库。
任何人都可以创建——为家人、朋友或定制化部署托管你自己的软件包。
结构很简单:
packages/
<package-name>/
<version>.yaml
featured.json # 可选
# 示例:
packages/
nginx/
1.0.yaml
2.0.yaml
postgres/
1.0.yaml
featured.json packages/ 下的每个软件包目录都包含一个或多个带版本号的 YAML 文件。
版本按点号分段比较——数字段按数值比较,其余按字典序比较。存储系统支持在版本之间升级,
也支持临时卸载后再恢复。Town OS 自带一个
默认仓库,
但你可以按需添加任意多个额外仓库。
软件包定义
一份包含全部可用字段的完整容器软件包定义:
image:
url: nginx:1.26-alpine
description: Lightweight high-performance web server and reverse proxy
supplies: ["http"]
command: ["optional", "command", "override"]
environment:
NGINX_HOST: "@hostname@"
network:
external:
"@port@": "80"
internal:
"5432": "5432"
domains:
- "@hostname@"
volumes:
html:
mountpoint: /usr/share/nginx/html
quota: 2gb
uid: 1000
gid: 1000
data:
mountpoint: /data
archive: seed-data.tar.gz
questions:
hostname:
query: "What hostname should nginx serve?"
type: hostname
port:
query: "What external port should nginx listen on?"
type: port
default: "8080"
archives:
- image: nginx:latest
directory: /usr/share/nginx/html
volume: html
git_sources:
- url: "https://github.com/example/config.git"
branch: main
volume: data
templates:
config:
volume: data
path: config.yaml
content: |
host: {{ .Responses.hostname }}
port: {{ .Responses.port }}
version: {{ .Package.Version }}
notes:
URL:
value: "http://@hostname@:@port@"
type: url
Support:
value: "+1 (555) 123-4567"
type: phone image 字段也接受简写的字符串形式:image: nginx:1.26-alpine。
顶层字段
| 字段 | 说明 |
|---|---|
image | 必填(容器运行时)。容器镜像引用。可以是字符串,也可以是带 url 和可选 type 的对象。与 vm 互斥。 |
description | 该软件包的简短易读说明。 |
supplies | 该软件包所提供能力的语义标签列表(例如 ["database"]、["http"])。 |
command | 可选的容器命令覆盖。与 proton 互斥。 |
environment | 传给容器的环境变量。键名必须匹配 ^[a-zA-Z_][a-zA-Z0-9_]*$。取值中可以包含 @variable@ 模板标记。 |
network | 端口映射与域名配置。 |
volumes | 带挂载配置的具名卷。 |
questions | 安装期间显示的交互式提问。名称必须是字母数字(^[a-zA-Z0-9]+$)。 |
archives | 归档提取规格,用于从容器镜像预填充卷。 |
git_sources | 要克隆进卷中的 git 仓库。 |
templates | 用 Go text/template 渲染、并在安装时写入卷中的文件模板。 |
notes | 安装后展示的键值信息。支持模板替换。 |
vm | 虚拟机配置。与 image 和 proton 互斥。 |
proton | 通过 Proton 运行 Windows 应用的配置。与 vm 和 command 互斥。 |
运行时分两种类型:container(默认)和 vm。
软件包为容器运行时指定 image 或 proton,
为虚拟机运行时指定 vm。Proton 是容器运行时的一种特化——
它底层同样使用 Podman,但会自动生成命令,并从另一个容器镜像中提取 Windows 应用的文件。
这些字段彼此互斥:一个软件包必须恰好包含
image/proton 或 vm 之一。
image 字段
image 字段指明要运行的容器镜像。它接受两种写法:
# 对象写法
image:
url: nginx:1.26-alpine
type: oci # 可选,默认为 "oci"
# 简写字符串写法
image: nginx:1.26-alpine 简短的镜像名会在编译期间自动归一化:
| 输入 | 归一化为 |
|---|---|
nginx | docker.io/library/nginx:latest |
myuser/myapp | docker.io/myuser/myapp:latest |
ghcr.io/org/app | ghcr.io/org/app:latest |
nginx:1.26-alpine | docker.io/library/nginx:1.26-alpine |
type 唯一有效的取值是 oci(默认值)。镜像 URL 只能包含
字母数字、@、.、_、:、
/ 和 -——shell 元字符会被拒绝。
supplies 标签
supplies 字段声明软件包提供了哪些能力,便于分类和筛选。
标签是自由格式的字符串——下面这些是约定俗成的:
| 标签 | 用于 | 示例 |
|---|---|---|
http | Web 服务器、CMS 平台、Web 应用 | nginx、wordpress、gitea |
database | 关系型和 NoSQL 数据库 | postgres、mysql、mongo |
cache | 缓存与键值存储 | redis、memcached、valkey |
search | 搜索与分析引擎 | elasticsearch、opensearch、solr |
messaging | 消息代理与队列 | rabbitmq、nats、kafka |
monitoring | 指标、告警与可视化 | prometheus、grafana、telegraf |
storage | 对象存储与文件托管 | minio、registry、nextcloud |
网络配置
| 字段 | 说明 |
|---|---|
network.external | 直接暴露在宿主机上的端口映射。键是宿主机端口,值是容器端口。两者都是 "port" 字符串,并可包含 @variable@ 模板。 |
network.internal | 只能从该软件包自身网络(以及共享的 HTTP 入口)访问的端口映射。键是宿主机侧/转发端口,值是容器端口。 |
network.domains | 可选列表,列出面向互联网的额外 FQDN,HTTP 入口会为它们申请公开受信任的(Let’s Encrypt / ACME)证书。可包含 @variable@ 模板。 |
端口取值在模板替换之后必须是 1 到 65535 之间的整数。
用不到的话,请整个省略 external、internal 或 domains——不要写成空映射或空列表。
端口条目的键可以是数字端口字符串
("2222": "22"),也可以是匹配
^[a-zA-Z][a-zA-Z0-9_]*$ 的语义名称(http: "3000")。
给端口命名后,父软件包就能按角色引用它(@dep_KEY_port_http@)而不必写端口号;
而对于特殊名称 http,还意味着让该端口接入下面所述的共享 HTTP 入口。
HTTP 入口(名为 http 的端口)
Town OS 运行着一个共享的 :443 端口入口,
它为每个软件包终结 TLS,并反向代理到该软件包的明文 HTTP 容器端口。
软件包只要把某个 internal 端口命名为
http,就接入了这个入口:
network:
internal:
http: "3000" # 容器的明文 HTTP 端口 这样做之后:
-
服务可通过
https://<PACKAGE_DNS>/访问—— URL 中不带端口。没有宿主机端口需要选;也不要再把 HTTP 端口 映射到external下。 -
入口会为
<PACKAGE_DNS>(例如gitea.default.home)提供一张本地受信任的叶证书 (由内置的 Rolodex CA 签发),并且 Rolodex 会在_443上发布 DANETLSA记录,让支持 DANE 的客户端可以固定该证书。 -
由于入口占用了
:443,应用生成的任何 URL 都必须是 不带端口的 HTTPS。请据此设置应用的对外 URL(例如 gitea 的GITEA__server__ROOT_URL: "https://@PACKAGE_DNS@/"), 并让应用在容器内继续监听明文 HTTP。
非 HTTP 协议(SSH、数据库等)不能使用
http 这个名称——它们不是 HTTP,入口无法为其终结 TLS。
请改用数字映射原样转发:
network:
internal:
http: "3000" # 由 :443 入口托管,终结 TLS
"@sshport@": "22" # 裸 TCP 转发,绝不包装 TLS 面向互联网的名称(domains)
默认情况下,入口只服务本地受信任的 <PACKAGE_DNS> 名称。
若还想以公开受信任的证书把服务暴露到公网,请把真实的 FQDN 列在
network.domains 下,并把该名称的公网 DNS 指向这台主机。
入口会为列出的每个域名申请 ACME(Let’s Encrypt)证书:
network:
internal:
http: "3000"
domains:
- git.example.com 卷
| 字段 | 说明 |
|---|---|
mountpoint | 必填。该卷在容器内的挂载绝对路径(必须以 / 开头)。 |
quota | 可选的容量上限(例如 512mb、2gb、1tb)。支持 mb、gb、tb 后缀。可包含 @variable@ 模板。 |
archive | 可选的归档文件名,用于预填充该卷。 |
git | 可选的 git 仓库 URL,会被克隆进该卷。 |
uid | 可选的数字用户 ID,用于卷的归属。 |
gid | 可选的数字组 ID,用于卷的归属。 |
卷名必须以字母或数字开头,且只能包含字母数字、点、连字符和下划线
(模式:^[a-zA-Z0-9][a-zA-Z0-9._-]*$)。
若软件包没有卷,请整个省略 volumes。
归档
archives 字段会在安装时把容器镜像中的文件提取到卷里:
archives:
- image: nginx:latest
directory: /usr/share/nginx/html
volume: html | 字段 | 说明 |
|---|---|
image | 必填。要从中提取文件的容器镜像。 |
directory | 必填。容器镜像中要提取的绝对路径。 |
volume | 必填。提取目标卷的名称,须为本软件包中已定义的卷。 |
如果目标卷在安装或对账时为空,Podman 会拉取镜像、创建一个临时容器, 并把指定目录复制进该卷。
Git 来源
git_sources 字段会把 git 仓库克隆进卷里:
git_sources:
- url: "https://github.com/example/config.git"
branch: main
volume: config | 字段 | 说明 |
|---|---|
url | 必填。git 仓库 URL(http、https 或 ssh)。可包含 @variable@ 模板。 |
branch | 要克隆的分支。可包含 @variable@ 模板。 |
volume | 必填。克隆目标卷的名称,须为本软件包中已定义的卷。 |
提问
提问定义了软件包安装期间显示的交互式提示。用户的回答会替换定义中各处的
@name@ 模板标记。
questions:
port:
query: "What external port should nginx listen on?"
type: port
default: "8080" | 字段 | 说明 |
|---|---|
query | 必填。展示给用户的提示文本。 |
type | 可选的校验类型。自由文本请省略。 |
default | 可选的默认值,作为建议提供给用户。 |
optional | 设为 true 即允许该提问留空。其余每个提问都必须给出非空的回答。 |
oauth | type: oauth 时必填,且仅在该类型下有效。安装对话框为获取令牌所运行的设备流程。 |
show_if | 指向同一软件包中的某个 boolean 提问。在那个复选框被勾选之前,本提问在安装对话框中处于隐藏状态;未勾选时它编译为空字符串(并免除必填检查)— 这样软件包就能把一组进阶选项收纳在一个开关之后。被引用的提问必须存在、必须是布尔类型,且自身不能是条件提问。 |
提问类型
| 类型 | 校验内容 |
|---|---|
hostname | 一个小写字母开头,后跟小写字母数字和连字符(模式:^[a-z][a-z0-9-]*$)。留空时自动生成 <package-name>-<4-char-hex>。 |
port | 1 到 65535 之间的整数。留空或设为 "auto" 时,在 10000-60000 范围内自动挑选一个可用端口。 |
bytes | 整数,或带 tb、gb、mb 后缀的数值(不区分大小写) |
volume | 字母数字、连字符和下划线(模式:^[a-zA-Z0-9-_]+$) |
archive | 任意非空字符串 |
secret | 留空或设为 "auto" 时自动生成 64 位十六进制字符串(256 位)。也可以用明确的取值覆盖。 |
duration | 整数,或带 d、h、m、s 后缀的数值(不区分大小写)。会换算为秒。 |
boolean | true、false、t、f、1 或 0(不区分大小写)。归一化为字符串 true 或 false。在安装对话框中渲染为复选框;未作答的提问取 default,若未声明默认值则取 false。 |
oauth | 不靠手动输入,而是从安装对话框运行 OAuth 设备流程取得的令牌。渲染为一个连接按钮;返回的令牌即为回答。其存储和显示方式与 secret 相同。 |
| (省略) | 任意字符串——不做校验 |
布尔提问
boolean 提问显示为复选框而不是文本框,其回答会以字面字符串
true 或 false 替换进去。由于未勾选同样是一个真实的回答,
带 default: "true" 的提问也可以被用户关掉——明确的
false 会胜过默认值。
environment:
REGISTRATION_OPEN: "@open@"
METRICS_ENABLED: "@metrics@"
questions:
open:
query: "Allow open registration?"
type: boolean # 未作答 -> "false"
metrics:
query: "Enable metrics?"
type: boolean
default: "true" # 未作答 -> "true"
文件模板看到的也是归一化之后的值,因此
{{ .Responses.metrics }} 会渲染为 true 或
false,并且可以用
{{ if eq .Responses.metrics "true" }} 来判断。
可选提问
除非声明了 optional: true,否则每个提问都必须给出非空的回答。
没有它的话,那些应用确实可以不要的设置——SMTP 中继、API 密钥——就没有诚实的表达方式:
作者只能编造一个占位默认值,然后期待操作者把它改掉。
environment:
SMTP_HOST: "@smtp_host@"
SMTP_PORT: "@smtp_port@"
questions:
smtp_host:
query: "SMTP server hostname"
optional: true
smtp_port:
query: "SMTP server port"
type: port
optional: true
default: "587"
留空时,可选提问会在它的 @marker@ 处替换为空字符串,
因此应用看到的是一个空变量,而不是某个没人选过的取值。它绝不会被自动生成:
留空的可选 secret 会保持为空,而不会变成一串随机字符串,
让应用傻乎乎地拿它去认证。
optional 可以与 type 组合——已作答的可选端口仍会按端口校验,
而留空的则会被编译掉、什么都不留。它对 boolean 没有意义,
因为复选框总会解析为它的两个取值之一。
OAuth 提问
有些应用需要配置一份只有其厂商才能签发的凭据——Plex 账号令牌、GitHub 个人令牌——
而通常的获取方式是在终端里跑个脚本,再把输出粘贴过来。oauth 提问
改为从安装对话框中运行那套流程:操作者点击连接,在浏览器标签页中授权,
返回的令牌就成为回答。
这里没有服务商注册表。提问自带一个 oauth 块,
写明服务商自己的各个 URL,因此任何具备设备式流程的厂商都无需改动 Town OS 即可支持。
environment:
PLEX_TOKEN: "@plextoken@"
questions:
plextoken:
query: "Plex account"
type: oauth
oauth:
start:
method: POST
url: "https://plex.tv/api/v2/pins?strong=true"
headers:
X-Plex-Client-Identifier: "{{client_id}}"
extract:
id: id
code: code
approve: "https://app.plex.tv/auth#?clientID={{client_id}}&code={{code}}"
poll:
url: "https://plex.tv/api/v2/pins/{{id}}"
headers:
X-Plex-Client-Identifier: "{{client_id}}"
token: authToken
interval: 2s
timeout: 10m | 字段 | 说明 |
|---|---|
start | 必填。开启流程的请求:method(默认 GET)、url,以及可选的 headers 和 form 请求体。 |
extract | 要从开始响应中取出、并提供给下面各模板使用的 JSON 字段,写作 name: json_field。 |
approve | 必填。操作者用于授权的 URL。它会在新标签页中打开,同时也会显示为链接,以防被弹窗拦截器挡掉。 |
user_code | 可选模板,用于操作者需要在授权页面上输入的短码。GitHub 会给一个;Plex 不会。 |
poll | 必填。在获得授权前反复发出的请求。结构与 start 相同。 |
token | 必填。轮询响应中存放令牌的 JSON 字段。该字段缺失或为 null,就是服务商在说“尚未授权”。 |
interval | 轮询间隔。默认为 5s;请遵守服务商公布的速率限制。 |
timeout | 放弃前持续轮询多久。默认为 5m。 |
URL、请求头和表单值中的 {{...}} 占位符,
会依据 extract 所命名的内容来解析,另外还有
{{client_id}}——这是 Town OS 为每次流程生成、
并在每一步都发送的随机标识符,Plex 正是把它的 pin 绑定在这上面。
令牌是一份凭据,因此也按凭据对待:在软件包信息面板中被掩码,可复制但绝不打印。 它像其他回答一样被缓存,因此重装或升级时会直接复用,而不必让操作者再跑一趟服务商那边。
由于调用这些 URL 的是系统控制器而不是浏览器,它们必须是 https,
并且不能解析到回环、私有、链路本地或 CGNAT 地址。这项检查在实际建立连接时执行,
并在每一次重定向时再执行一遍,因此软件包无法借由某个流程,让控制器伸手进宿主机自己的网络。
模板
templates 字段定义了一些文件,它们会用 Go text/template 渲染,
并在安装期间写入卷中。只有当目标文件尚不存在时才会写入,从而在升级过程中保留用户的修改。
templates:
config:
volume: data
path: config.yaml
content: |
host: {{ .Responses.hostname }}
port: {{ .Responses.port }}
name: {{ .Package.Name }}
system: {{ .System.Hostname }} | 字段 | 说明 |
|---|---|
volume | 必填。本软件包中已定义的卷名。 |
path | 必填。卷内的相对路径。不得以 / 开头,也不得包含 ..。 |
content | 必填。要渲染的 Go text/template 内容。 |
模板数据上下文
在 {{ }} 表达式内部可以使用以下数据:
| 表达式 | 说明 |
|---|---|
.Responses.<name> | 用户对指定提问的回答 |
.Package.Name | 软件包名称 |
.Package.Version | 软件包版本 |
.Package.Repo | 仓库名称 |
.Package.Image | 编译后的容器镜像引用 |
.Package.Description | 软件包描述 |
.System.Hostname | 系统主机名 |
.System.ExternalIP | 外部 IP 地址(若已知) |
.System.InternalIP | 内部/局域网 IP 地址(若已知) |
模板名遵循与卷名相同的规则。文件以 0600 模式写入,
上级目录以 0750 模式创建。
说明
说明(notes)提供安装后展示的键值信息。
notes:
URL:
value: "http://localhost:@port@"
type: url
Info:
value: "Default admin credentials are admin/admin" | 字段 | 说明 |
|---|---|
value | 必填。说明文本。支持 @variable@ 模板替换。 |
type | 可选的校验类型:url、phone 或 email。纯文本请省略。 |
依赖
软件包可以声明对其他软件包的依赖。依赖会共享父包的 podman 网络, 使同一依赖树中的容器能通过 podman 内置的 DNS 按容器名直接通信。
dependencies:
db:
package: postgres
responses:
password: "@dbpass@"
user: "mattermost"
database: "mattermost"
port: "5432" | 字段 | 说明 |
|---|---|
package | 必填。要安装的依赖软件包名称。 |
repo | 包含该依赖的仓库。默认与父包所在仓库相同。 |
version | 要安装的版本。默认为可用的最新版本。 |
responses | 给该依赖的提问回答。取值支持来自父包提问的 @variable@ 语法。 |
父软件包在运行时会为每个依赖获得对应的环境变量:
TOWNOS_DEP_{KEY}_HOST(容器名)和
TOWNOS_DEP_{KEY}_PORT_{port}(容器侧端口号)。
父包也可以在自己的环境变量值中使用 @dep_KEY_host@ 和
@dep_KEY_port_N@ 模板变量(参见模板系统)。
示例:一个对 PostgreSQL 声明了 db 依赖的 Mattermost 软件包,
可以在它的数据源 URL 中引用数据库主机:
environment:
MM_SQLSETTINGS_DATASOURCE: "postgres://mattermost:@dbpass@@@dep_db_host@:@dep_db_port_5432@/mattermost?sslmode=disable" 模板系统
Town OS 有两套模板系统,分别作用于编译的不同阶段。
@variable@ 替换
@variable@ 语法在软件包编译期间被替换,作用于所有可配置字段:
环境变量值、网络端口映射、网络域名、卷挂载点、卷配额、卷的 git URL、
git 来源的 URL 与分支、模板的 volume 与 path 字段、虚拟机的镜像与内存、
Proton 配置,以及说明项的取值。
每个提问名称都会成为一个变量。若要写出字面的 @ 字符
(例如 git SSH URL git@@domain@),请使用 @@——
两个连续的 @ 会产生一个字面的 @。
未能解析的变量(引用了没有对应提问回答的名称)会原样保留在输出中。
内置变量
| 变量 | 说明 |
|---|---|
@LOCAL_EXTERNAL_HOST@ | Town OS 主机的外部主机名或 IP |
@LOCAL_INTERNAL_HOST@ | Town OS 主机的内部主机名或 IP |
@PACKAGE_DNS@ | 该软件包在内部网络上被分配的 DNS 名称 |
@dep_KEY_host@ | 依赖 KEY 的容器主机名(可通过共享网络上的 podman DNS 解析)。仅在软件包声明了依赖时可用。 |
@dep_KEY_port_N@ | 依赖 KEY 的容器端口 N。仅在软件包声明了依赖时可用。 |
内置变量会先于用户提问的回答被替换,因此优先级更高。
依赖模板变量(@dep_*@)在依赖安装完成后解析,并应用到父包的环境变量值中。
其中 KEY 是小写的依赖键名,N 是容器端口号。
Go 模板(templates 字段中)
templates 字段使用 Go text/template 语法
({{ .Responses.name }})来渲染写入卷中的文件。
可用的数据上下文见模板一节。
虚拟机运行时
软件包可以运行虚拟机而不是容器,只需指定 vm 字段来代替
image:
vm:
image: "https://example.com/my-vm.qcow2"
memory: 2gb
cpus: 2
description: A virtual machine package | 字段 | 说明 |
|---|---|
image | 必填。虚拟机磁盘镜像的 URL(http/https)或文件名。可包含 @variable@ 模板。 |
memory | 用字节后缀表示的内存分配(例如 1gb、512mb)。默认为 1gb。可包含 @variable@ 模板。 |
cpus | 虚拟 CPU 数量。默认为 1。必须为非负数。 |
vm 字段与 image 和 proton 互斥。
虚拟机镜像不会像容器镜像那样被归一化。
Proton 运行时
软件包可以通过指定 proton 字段,用 Proton 运行 Windows 应用:
image:
type: oci
proton:
app_image: "mycompany/windows-app:1.0"
app_directory: /app
volume: app
exe: /app/myapp.exe
args: ["-fullscreen", "-config", "/app/config.ini"]
volumes:
app:
mountpoint: /app | 字段 | 说明 |
|---|---|
app_image | 必填。包含该 Windows 应用的容器镜像。 |
app_directory | 必填。应用所在的绝对路径。 |
volume | 必填。本软件包中已定义的卷名。 |
exe | 必填。Windows 可执行文件的路径。 |
args | 可选的命令行参数列表。 |
设置了 proton 之后,容器命令会自动生成为
["proton", "run", <exe>, ...<args>]。容器镜像默认取
全系统的 proton_image 设置
(quay.io/town/proton:latest),可通过设置 image
在软件包层面覆盖。app_image 字段在编译期间会按与普通容器镜像引用
相同的规则归一化。proton 字段与 vm 和
command 互斥。
编译
编译会把软件包定义和用户的回答,转化为一份完全解析、可直接运行的配置。 编译流水线包含以下步骤:
- 依据各提问声明的类型校验全部回答
- 执行类型专属的校验(端口范围、主机名模式、字节数解析等)
- 把所有
@variable@模板标记替换为解析后的取值 - 把容器镜像 URL 归一化为完全限定的引用
- 产出一份可供安装的、已解析的软件包
对于虚拟机软件包,内存字符串(例如 2gb)会被解析为字节数,
CPU 的默认值也会被套用。各字段的校验错误会一并收集后统一返回,
因此用户可以一次改完,而不必逐个碰壁。
回答的持久化
回答按版本保存在
responses/<repo>/<pkg>/<version>.json。
同时还会在 responses/last/<repo>/<pkg>.json
保存一份 last 副本,供升级以及从已卸载的卷重新安装时复用。
安装成功后,last 回答会被清除。这意味着如果一个软件包被卸载后又重新安装, 先前的回答会作为默认值提供给你。一旦新的安装成功,缓存的副本就会被移除。
安装流程
安装一个软件包会走完一套既定的步骤序列。 整个过程涵盖从文件准备到服务启动的一切:
- 从仓库中的软件包文件到已安装目录建立硬链接
- 持久化回答(版本专属的以及
last副本) - 创建卷,套用配额和可选的 UID/GID 归属
- 从归档和 git 来源填充卷(仅限容器运行时)
- 套用模板(把文件渲染进卷中)
- 生成 systemd 单元(容器基于 Podman,虚拟机基于 QEMU)
- 创建网络状态文件
- 启动服务
- 成功后清除 last 回答
有两个可选标志用于控制卷的行为:
reuse_volumes——复用同一软件包先前已卸载版本的卷import_from_version——从指定的旧版本导入卷
卸载
卸载软件包时默认会保留卷中的数据。
卷只是从 installed/ 前缀移动到 uninstalled/ 前缀,
而不是被删除。这样重新安装时原有数据仍然完好。
卸载过程还会删除网络状态文件,并停止、禁用并卸载相关的 systemd 单元。
如果你想立即删除卷而不是保留它们,请使用 purge_volumes 标志。
被清除的卷无法恢复。
安装预览
在真正开始安装之前,你可以先预览会创建些什么。
安装预览端点(POST /packages/install-preview)会返回一份计划中的安装摘要,
且不做任何改动:
- 将会创建的卷
- 将会映射的端口
- 升级信息(若是从旧版本升级)
- 运行时类型(容器或虚拟机)
- 该软件包是否有需要回答的提问
- 虚拟机软件包的虚拟机配置详情(镜像、内存、CPU 数)
精选软件包
软件包仓库可以在根目录(与 packages/ 目录并列)放一个
featured.json 文件,用于在界面上突出展示选定的软件包。
该文件包含一个由软件包名称字符串组成的 JSON 数组——不带版本号或仓库前缀:
["wordpress", "nextcloud", "postgres"]
列在 featured.json 中的软件包,会在 API 的软件包列表响应中带上
featured: true,界面据此加以突出。该文件是可选的——
如果没有,就没有任何软件包被标为精选。
导入仓库
创建好软件包仓库之后,可以通过网页界面或直接修改文件系统配置, 把它添加到你的 Town OS 实例中。
通过界面
登录 Town OS 仪表盘,进入 软件包,选择 仓库 标签页,点击 添加仓库。填入名称、 git URL 和可选的凭据。点击 刷新 可以立即拉取软件包元数据。
通过文件系统
编辑 btrfs 软件包数据目录中的 repositories.json。该文件是一个
由对象组成的 JSON 数组:
[
{"name": "default", "url": "https://github.com/town-os/default-packages"},
{"name": "my-packages", "url": "https://github.com/myuser/my-packages"}
] 顺序是有意义的——当软件包重名时,靠后的条目会覆盖靠前的。 私有仓库请把凭据嵌入 URL 中。改动需要重启服务,或在界面上点 刷新 才会生效。
校验规则
软件包校验和编译期间会强制执行以下约束:
| 字段 | 规则 |
|---|---|
| 镜像 URL | 非空;字符必须匹配 ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$ |
| 镜像 type | 为空(默认为 oci)或 oci |
| 环境变量键名 | 必须匹配 ^[a-zA-Z_][a-zA-Z0-9_]*$(POSIX 惯例) |
| 提问名称 | 必须匹配 ^[a-zA-Z0-9]+$ |
| 卷名 | 必须匹配 ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ |
| 挂载点 | 必须以 / 开头 |
| 模板名称 | 与卷名规则相同 |
| 模板路径 | 非空、相对路径(不带前导 /)、不含 .. 穿越 |
| 归档目录 | 必须是绝对路径 |
| 归档/git 的 volume | 必须引用该软件包中已定义的卷 |
| Git URL | 必须有合法的协议和主机(file:// 除外) |
| 虚拟机 CPU 数 | 必须为非负数 |
| Proton 的 app_directory | 必须是绝对路径 |
| 运行时 | image/proton(容器)与 vm 之中恰好取其一 |
校验发生在模板替换之前。包含 @variable@ 标记的字段,
会跳过路径和 URL 校验,直到编译解析完模板之后再检查。
风格指引
- 省略空映射(
environment:、internal:、volumes:)——干脆整个不写。 - 接受自由文本的提问请省略
type——不要写一个没有取值的type:。 - 写上
description,简短说明这个软件包是什么。 - 当软件包提供的是众所周知的服务时,写上带有相应能力标签的
supplies。 - 写上
notes,包含连接 URL 以及任何重要的安装后信息。 - 用
@variable@模板,让用户在安装时自定义端口、主机名和凭据。 - 需要 Go 模板逻辑的配置文件请使用
templates——它们只写入一次,并会保留用户的修改。