パッケージング形式

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 ファイルが 1 つ以上入ります。 バージョンはドット区切りの各要素で比較され、数値の要素は数値として、それ以外は辞書順で比較されます。 ストレージの仕組みは、バージョン間のアップグレードや、一時的にアンインストールして後から復元する運用にも対応しています。 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 リポジトリ。
templatesGo の text/template で描画され、インストール時にボリュームへ書き込まれるファイルテンプレート。
notesインストール後に表示されるキーと値のメタデータ。テンプレート置換に対応します。
vm仮想マシンの設定。image および proton とは併用できません。
protonProton 経由で動かす Windows アプリケーションの設定。vm および command とは併用できません。

ランタイムには container(既定)と vm の 2 種類があります。 パッケージはコンテナーランタイムなら imageproton を、 VM ランタイムなら vm を指定します。Proton はコンテナーランタイムの特殊形で、 内部では Podman を使いつつ、コマンドを自動生成し、別のコンテナーイメージから Windows アプリケーションのファイルを取り出します。これらのフィールドは排他的で、 パッケージには image/protonvm のいずれか 1 つだけを 含める必要があります。

image フィールド

image フィールドは実行するコンテナーイメージを指定します。書き方は 2 通りです。

# オブジェクト形式
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 に使えるのは 英数字と @._:/- のみで、シェルのメタ文字は拒否されます。

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.domainsHTTP イングレスが公的に信頼される(Let’s Encrypt / ACME)証明書を取得すべき、インターネット向け FQDN の任意の一覧。@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 のコンテナーポートへ リバースプロキシします。パッケージは内部ポートに 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利用者に提示される任意の既定値。
optionaltrue にすると、その質問を空欄のままにできます。それ以外の質問はすべて、空でない値で回答する必要があります。
oauthtype: oauth では必須で、そこでのみ有効です。 トークンを取得するためにインストールダイアログが実行するデバイスフロー。
show_if同じパッケージ内の boolean 型の質問を指定します。この質問は、そのチェックボックスがオンになるまでインストールダイアログで非表示になり、オフの間は空文字列としてコンパイルされます(必須チェックの対象からも外れます)。これにより、上級者向けの項目をひとつのスイッチの裏にまとめられます。参照先の質問は存在し、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 文字の 16 進文字列(256 ビット)を自動生成します。明示的な値で上書きすることもできます。
duration整数、または dhms の接尾辞が付いた数値(大文字小文字は区別しません)。秒に変換されます。
booleantruefalsetf10(大文字小文字は区別しません)。文字列の true または false に正規化されます。インストールダイアログではチェックボックスとして表示され、未回答の場合は default、それが宣言されていなければ false になります。
oauth入力ではなく、インストールダイアログから OAuth のデバイスフローを実行して取得するトークン。接続ボタンとして表示され、返ってきたトークンが回答になります。保存と表示は secret と同じ扱いです。
(省略)任意の文字列。検証は行いません

boolean 型の質問

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 は空欄のままで、アプリケーションが律儀に認証を試みるような ランダム文字列にはなりません。

optionaltype と組み合わせられます。回答された任意のポートは 引き続きポートとして検証され、空欄のものはコンパイル時に何も残しません。 boolean に指定しても意味はありません。チェックボックスは常に 2 つの値の どちらかに決まるからです。

OAuth の質問

アプリケーションによっては、提供元しか発行できない認証情報(Plex のアカウントトークン、 GitHub の個人トークンなど)で設定するものがあり、通常はターミナルでスクリプトを実行して 出力された値を貼り付けます。oauth の質問は、その手順をインストールダイアログから 実行します。運用者が接続をクリックし、ブラウザーのタブで承認すると、 返ってきたトークンがそのまま回答になります。

プロバイダーの登録簿はありません。質問には提供元自身の URL を記した 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必須。 運用者が承認のために開く URL。新しいタブで開かれ、ポップアップブロッカーに阻まれた場合に備えてリンクとしても表示されます。
user_code承認ページで運用者が入力する短いコードのテンプレート(任意)。GitHub は表示しますが、Plex は表示しません。
poll必須。 承認されるまで繰り返すリクエスト。形式は start と同じです。
token必須。 ポーリングのレスポンスでトークンが入っている JSON フィールド。値がない、または null であることが、提供元の「まだ承認されていない」という返答です。
intervalポーリングの間隔。既定は 5s です。提供元が定めるレート制限に従ってください。
timeoutあきらめるまでポーリングを続ける時間。既定は 5m です。

URL、ヘッダー、フォームの値に含まれる {{...}} のプレースホルダーは、 extract で名前を付けたものと、{{client_id}} に解決されます。 後者は Town OS がフローごとに生成してすべての手順で送るランダムな識別子で、 Plex はこれに pin を紐づけます。

トークンは認証情報なので、そのように扱われます。パッケージ情報のパネルでは伏せ字になり、 コピーはできても表示されることはありません。他の回答と同様にキャッシュされるため、 再インストールやアップグレードの際は運用者を提供元へ送り返さずに再利用されます。

これらの URL を呼び出すのはブラウザーではなくシステムコントローラーなので、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内部・LAN の IP アドレス(判明している場合)

テンプレート名はボリューム名と同じ規則に従います。ファイルはモード 0600 で書き込まれ、 親ディレクトリはモード 0750 で作成されます。

ノート

ノートは、インストール後に表示されるキーと値のメタデータです。

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 には、コンパイルの異なる段階で働く 2 つのテンプレートの仕組みがあります。

@variable@ の置換

@variable@ 構文は、パッケージのコンパイル時に、設定可能なすべてのフィールドで 置換されます。環境変数の値、ネットワークのポートマッピング、ネットワークのドメイン、 ボリュームのマウントポイント、ボリュームのクォータ、ボリュームの git URL、git ソースの URL と ブランチ、テンプレートの volume と path、VM のイメージとメモリ、Proton の設定、 ノートの値が対象です。

質問の名前がそれぞれ変数になります。@ という文字そのものを含めたい場合 (git@@domain@ のような git の SSH URL など)は @@ と書きます。 @ を 2 つ続けると、@ 1 つになります。

解決できなかった変数(対応する質問の回答がない名前)は、出力にそのまま残ります。

組み込み変数

変数説明
@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 フィールドを指定すると、パッケージは コンテナーではなく仮想マシンを実行できます。

vm:
  image: "https://example.com/my-vm.qcow2"
  memory: 2gb
  cpus: 2
description: A virtual machine package
フィールド説明
image必須。 VM のディスクイメージの URL(http/https)またはファイル名。@variable@ テンプレートを含められます。
memoryバイトの接尾辞を使ったメモリ割り当て(例: 1gb512mb)。既定は 1gb です。@variable@ テンプレートを含められます。
cpus仮想 CPU の数。既定は 1 です。負の値は指定できません。

vm フィールドは image および proton とは併用できません。 VM のイメージは、コンテナーイメージのようには正規化されません。

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 を完全修飾の参照に正規化する
  • インストール可能な状態まで解決したパッケージを生成する

VM パッケージの場合、メモリの文字列(2gb など)はバイト数に解析され、 CPU の既定値が適用されます。検証エラーはすべてのフィールドについてまとめて収集され、 一括で返されるため、利用者は 1 度の修正ですべてを直せます。エラーに 1 つずつ ぶつかることはありません。

回答の保存

回答はバージョンごとに保存されます。保存先は responses/<repo>/<pkg>/<version>.json です。 アップグレードや、アンインストール済みのボリュームからの再インストールで再利用するために、 responses/last/<repo>/<pkg>.json にも last のコピーが 保存されます。

last の回答はインストールが成功すると消去されます。つまり、パッケージをアンインストールして 後から再インストールすると、以前の回答が既定値として提示されます。新しいインストールが 成功した時点で、キャッシュされたコピーは削除されます。

インストールの流れ

パッケージのインストールは、決められた一連の手順で進みます。 ファイルの準備からサービスの起動まで、すべてがこの流れで処理されます。

  • リポジトリのパッケージファイルから、インストール先ディレクトリへのハードリンクの作成
  • 回答の保存(バージョン別と last のコピー)
  • クォータと任意の UID/GID 所有者を設定したボリュームの作成
  • アーカイブと git ソースからのボリュームの初期化(コンテナーランタイムのみ)
  • テンプレートの適用(ファイルをボリュームへ描画)
  • systemd ユニットの生成(コンテナーは Podman ベース、VM は QEMU ベース)
  • ネットワーク状態ファイルの作成
  • サービスの開始
  • 成功時の last の回答の消去

ボリュームの扱いは、2 つの任意フラグで制御できます。

  • reuse_volumes — 同じパッケージの以前のアンインストール済みバージョンのボリュームを再利用する
  • import_from_version — 特定の以前のバージョンからボリュームを取り込む

アンインストール

パッケージをアンインストールしても、既定ではボリュームのデータは保持されます。 ボリュームは削除されるのではなく、installed/ のプレフィックスから uninstalled/ のプレフィックスへ移動されます。これにより、元のデータを保ったまま 再インストールできます。

アンインストールの処理では、ネットワーク状態ファイルの削除も行い、関連する systemd ユニットの 停止、無効化、アンインストールも実施します。

保持せずにボリュームをすぐ削除したい場合は、purge_volumes フラグを使います。 削除されたボリュームは復元できません。

インストールのプレビュー

インストールを確定する前に、何が作られるのかを事前に確認できます。 インストールプレビューのエンドポイント(POST /packages/install-preview)は、 変更を一切加えずに、予定されるインストール内容の要約を返します。

  • 作成されるボリューム
  • 割り当てられるポート
  • アップグレードの情報(以前のバージョンからアップグレードする場合)
  • ランタイムの種類(コンテナーまたは VM)
  • 回答すべき質問があるかどうか
  • VM パッケージの場合は VM の構成の詳細(イメージ、メモリ、CPU 数)

パッケージリポジトリのルート(packages/ ディレクトリと同じ階層)に featured.json ファイルを置くと、選んだパッケージを UI 上で目立たせられます。

このファイルには、パッケージ名の文字列からなる JSON 配列を記述します。バージョンや リポジトリのプレフィックスは付けません。

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

featured.json に列挙されたパッケージは、API のパッケージ一覧のレスポンスで featured: true として返されるため、UI 側で強調表示できます。このファイルは任意で、 存在しない場合はどのパッケージも注目扱いになりません。

リポジトリの取り込み

パッケージリポジトリを作ったら、ウェブ UI かファイルシステム上の設定のどちらかで、 自分の Town OS インスタンスに追加します。

UI から追加する

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 に認証情報を埋め込んでください。変更を反映するには、 サービスを再起動するか、UI で更新を実行します。

検証ルール

パッケージの検証とコンパイルでは、次の制約が適用されます。

フィールドルール
イメージ URL空でないこと。文字は ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$ に一致すること
イメージのタイプ空(既定で oci)または oci
環境変数のキー^[a-zA-Z_][a-zA-Z0-9_]*$ に一致すること(POSIX の慣例)
質問の名前^[a-zA-Z0-9]+$ に一致すること
ボリューム名^[a-zA-Z0-9][a-zA-Z0-9._-]*$ に一致すること
マウントポイント/ で始まること
テンプレート名ボリューム名と同じ規則
テンプレートのパス空でなく、相対パスであり(先頭に / がない)、.. による移動を含まないこと
アーカイブのディレクトリ絶対パスであること
アーカイブ・git のボリュームパッケージ内で定義されたボリュームを参照すること
git の URL正しいスキームとホストを持つこと(file:// は例外)
VM の CPU 数負の値でないこと
Proton の app_directory絶対パスであること
ランタイムimage/proton(コンテナー)か vm のちょうど 1 つ

検証はテンプレート置換の前に実行されます。@variable@ の記号を含むフィールドは、 コンパイルがテンプレートを解決するまで、パスと URL の検証を飛ばします。

スタイルの指針

  • 空のマップ(environment: internal: volumes: )は書かず、まるごと省略してください。
  • 自由入力のテキストを受け付ける質問では type を省略してください。値のない type: を書かないでください。
  • そのパッケージが何であるかを短くまとめた description を入れてください。
  • よく知られたサービスを提供するパッケージでは、該当する機能タグを supplies に入れてください。
  • 接続 URL やインストール後に重要となる情報を notes に入れてください。
  • ポート、ホスト名、認証情報をインストール時に利用者が指定できるよう、@variable@ テンプレートを使ってください。
  • Go テンプレートのロジックが必要な設定ファイルには templates を使ってください。一度しか書き込まれないため、利用者の編集が保持されます。