打包格式

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虚拟机配置。与 imageproton 互斥。
proton通过 Proton 运行 Windows 应用的配置。与 vmcommand 互斥。

运行时分两种类型:container(默认)和 vm。 软件包为容器运行时指定 imageproton, 为虚拟机运行时指定 vm。Proton 是容器运行时的一种特化—— 它底层同样使用 Podman,但会自动生成命令,并从另一个容器镜像中提取 Windows 应用的文件。 这些字段彼此互斥:一个软件包必须恰好包含 image/protonvm 之一。

image 字段

image 字段指明要运行的容器镜像。它接受两种写法:

# 对象写法
image:
  url: nginx:1.26-alpine
  type: oci                # 可选,默认为 "oci"

# 简写字符串写法
image: nginx:1.26-alpine

简短的镜像名会在编译期间自动归一化:

输入归一化为
nginxdocker.io/library/nginx:latest
myuser/myappdocker.io/myuser/myapp:latest
ghcr.io/org/appghcr.io/org/app:latest
nginx:1.26-alpinedocker.io/library/nginx:1.26-alpine

type 唯一有效的取值是 oci(默认值)。镜像 URL 只能包含 字母数字、@._:/-——shell 元字符会被拒绝。

supplies 标签

supplies 字段声明软件包提供了哪些能力,便于分类和筛选。 标签是自由格式的字符串——下面这些是约定俗成的:

标签用于示例
httpWeb 服务器、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 之间的整数。 用不到的话,请整个省略 externalinternaldomains——不要写成空映射或空列表。

端口条目的可以是数字端口字符串 ("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 上发布 DANE TLSA 记录,让支持 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可选的容量上限(例如 512mb2gb1tb)。支持 mbgbtb 后缀。可包含 @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 即允许该提问留空。其余每个提问都必须给出非空的回答。
oauthtype: oauth 时必填,且仅在该类型下有效。安装对话框为获取令牌所运行的设备流程。
show_if指向同一软件包中的某个 boolean 提问。在那个复选框被勾选之前,本提问在安装对话框中处于隐藏状态;未勾选时它编译为空字符串(并免除必填检查)— 这样软件包就能把一组进阶选项收纳在一个开关之后。被引用的提问必须存在、必须是布尔类型,且自身不能是条件提问。

提问类型

类型校验内容
hostname一个小写字母开头,后跟小写字母数字和连字符(模式:^[a-z][a-z0-9-]*$)。留空时自动生成 <package-name>-<4-char-hex>
port1 到 65535 之间的整数。留空或设为 "auto" 时,在 10000-60000 范围内自动挑选一个可用端口。
bytes整数,或带 tbgbmb 后缀的数值(不区分大小写)
volume字母数字、连字符和下划线(模式:^[a-zA-Z0-9-_]+$
archive任意非空字符串
secret留空或设为 "auto" 时自动生成 64 位十六进制字符串(256 位)。也可以用明确的取值覆盖。
duration整数,或带 dhms 后缀的数值(不区分大小写)。会换算为秒。
booleantruefalsetf10(不区分大小写)。归一化为字符串 truefalse。在安装对话框中渲染为复选框;未作答的提问取 default,若未声明默认值则取 false
oauth不靠手动输入,而是从安装对话框运行 OAuth 设备流程取得的令牌。渲染为一个连接按钮;返回的令牌即为回答。其存储和显示方式与 secret 相同。
(省略)任意字符串——不做校验

布尔提问

boolean 提问显示为复选框而不是文本框,其回答会以字面字符串 truefalse 替换进去。由于未勾选同样是一个真实的回答, 带 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 }} 会渲染为 truefalse,并且可以用 {{ 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,以及可选的 headersform 请求体。
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可选的校验类型:urlphoneemail。纯文本请省略。

依赖

软件包可以声明对其他软件包的依赖。依赖会共享父包的 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用字节后缀表示的内存分配(例如 1gb512mb)。默认为 1gb。可包含 @variable@ 模板。
cpus虚拟 CPU 数量。默认为 1。必须为非负数。

vm 字段与 imageproton 互斥。 虚拟机镜像不会像容器镜像那样被归一化。

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 字段与 vmcommand 互斥。

编译

编译会把软件包定义和用户的回答,转化为一份完全解析、可直接运行的配置。 编译流水线包含以下步骤:

  • 依据各提问声明的类型校验全部回答
  • 执行类型专属的校验(端口范围、主机名模式、字节数解析等)
  • 把所有 @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——它们只写入一次,并会保留用户的修改。