封裝格式

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 欄位宣告套件提供了哪些能力,以便分類與篩選。 標籤是自由格式的字串——以下這些是慣用的:

標籤用於範例
http網頁伺服器、CMS 平台、網頁應用程式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>/ 連上—— 網址中不帶連接埠。沒有主機連接埠需要選;也不要再把 HTTP 連接埠 對應到 external 之下。
  • 入口會為 <PACKAGE_DNS>(例如 gitea.default.home)提供一張本地受信任的終端憑證 (由內建的 Rolodex CA 簽發),而且 Rolodex 會_443 上發布 DANE TLSA 記錄,讓支援 DANE 的用戶端可以釘選該憑證。
  • 由於入口占用了 :443,應用程式產生的任何網址都必須是 不帶連接埠的 HTTPS。請據此設定應用程式的對外網址(例如 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 區塊, 寫明供應商自己的各個網址,因此任何具備裝置式流程的廠商,都不必改動 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必填。操作者用來核准的網址。它會在新分頁開啟,同時也會顯示成連結,以防被彈出視窗封鎖程式擋掉。
user_code選用範本,用於操作者必須在核准頁面上輸入的短碼。GitHub 會給一組;Plex 不會。
poll必填。在核准前重複發出的請求。結構與 start 相同。
token必填。輪詢回應中存放權杖的 JSON 欄位。該欄位不存在或為 null,就是供應商在說“尚未核准”。
interval輪詢頻率。預設為 5s;請遵守供應商公告的速率限制。
timeout放棄前要持續輪詢多久。預設為 5m

網址、標頭與表單值中的 {{...}} 佔位符, 會依 extract 所命名的內容解析,另外還有 {{client_id}}——這是 Town OS 為每次流程產生、 並在每一步都送出的隨機識別碼,Plex 正是把它的 pin 綁在這上面。

權杖是一份憑證,因此也就按憑證對待:在套件資訊面板中會被遮罩,可以複製但絕不列印。 它像其他回答一樣會被快取,因此重新安裝或升級時會直接重用,不必再把操作者送回供應商那邊。

由於呼叫這些網址的是系統控制器而非瀏覽器,它們必須是 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 套件, 可以在它的資料來源網址中參照資料庫主機:

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 網址 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必填。虛擬機器磁碟映像檔的網址(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@ 範本標記替換成解析後的值
  • 把容器映像檔網址正規化為完整限定的參照
  • 產出一份可供安裝、已解析的套件

對虛擬機器套件而言,記憶體字串(例如 2gb)會被剖析成位元組數, 並套用 CPU 的預設值。各欄位的驗證錯誤會一併蒐集後一起回傳, 因此使用者可以一次改完,而不必一個一個碰壁。

回答的保存

回答依版本保存responses/<repo>/<pkg>/<version>.json。 同時也會在 responses/last/<repo>/<pkg>.json 存一份 last 副本,供升級以及從已解除安裝的磁碟區重新安裝時重用。

安裝成功後,last 回答會被清除。這表示如果一個套件被解除安裝、之後又重新安裝, 先前的答案會作為預設值提供給你。一旦新的安裝成功,快取的副本就會被移除。

安裝流程

安裝一個套件會走過一連串既定的步驟。 整個流程涵蓋從檔案準備到服務啟動的一切:

  • 從套件庫中的套件檔案到已安裝目錄建立硬連結
  • 保存回答(版本專屬的以及 last 副本)
  • 建立磁碟區,套用配額與選用的 UID/GID 擁有權
  • 從封存檔與 git 來源填入磁碟區(僅限容器執行環境)
  • 套用範本(把檔案算繪進磁碟區)
  • 產生 systemd unit(容器用 Podman,虛擬機器用 QEMU)
  • 建立網路狀態檔
  • 啟動服務
  • 成功後清除 last 回答

有兩個選用旗標可控制磁碟區的行為:

  • reuse_volumes——重用同一套件先前已解除安裝版本的磁碟區
  • import_from_version——從指定的舊版本匯入磁碟區

解除安裝

解除安裝套件時,預設會保留磁碟區中的資料。 磁碟區只是從 installed/ 前綴搬到 uninstalled/ 前綴, 而不是被刪除。這樣重新安裝時,原有的資料仍然完整。

解除安裝流程也會移除網路狀態檔,並停止、停用並解除安裝相關的 systemd unit。

若想立即刪除磁碟區而不是保留它們,請使用 purge_volumes 旗標。 被清除的磁碟區無法復原。

安裝預覽

在真正開始安裝之前,你可以先預覽會建立哪些東西。 安裝預覽端點(POST /packages/install-preview)會回傳一份預定安裝的摘要, 而且不做任何變更:

  • 將會建立的磁碟區
  • 將會對應的連接埠
  • 升級資訊(若是從舊版本升級)
  • 執行環境型別(容器或虛擬機器)
  • 該套件是否有需要回答的問題
  • 虛擬機器套件的虛擬機器設定細節(映像檔、記憶體、CPU 數)

套件庫可以在根目錄(與 packages/ 目錄並列)放一個 featured.json 檔案,用來在介面上突顯特定套件。

該檔案內含一個由套件名稱字串組成的 JSON 陣列——不帶版本或套件庫前綴:

["wordpress", "nextcloud", "postgres"]

列在 featured.json 中的套件,會在 API 的套件清單回應中帶上 featured: true,讓介面據此加以突顯。這個檔案是選用的—— 若不存在,就沒有任何套件被標為精選。

匯入套件庫

建立好套件庫之後,可以透過網頁介面或直接調整檔案系統設定, 把它加進你的 Town OS 執行個體。

透過介面

登入 Town OS 儀表板,前往 套件,選擇 套件庫 分頁,按下 新增套件庫。填入名稱、 git 網址與選用的憑證。按 重新整理 可立即拉取套件中繼資料。

透過檔案系統

編輯 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非空;字元必須符合 ^[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@ 標記的欄位, 會跳過路徑與網址驗證,直到編譯把範本解析完之後再檢查。

風格指引

  • 省略空的對應(environment: internal: volumes: )——乾脆整個不寫。
  • 接受自由文字的問題請省略 type——不要寫一個沒有值的 type:
  • 寫上 description,簡短說明這個套件是什麼。
  • 當套件提供的是眾所周知的服務時,寫上帶有相應能力標籤的 supplies
  • 寫上 notes,包含連線網址以及任何重要的安裝後資訊。
  • @variable@ 範本,讓使用者在安裝時自訂連接埠、主機名稱與憑證。
  • 需要 Go 範本邏輯的設定檔請使用 templates——它們只會寫入一次,並保留使用者的修改。