Packaging Format
Town OS packages are YAML definitions that describe how to run a containerized service, including its image, networking, storage, and user-facing configuration prompts. Packages can also run virtual machines or Windows applications via Proton.
Repository Structure
A package repository is any git repository containing a packages/ directory.
Anyone can create one — host your own packages for family, friends, or custom deployments.
The structure is simple:
packages/
<package-name>/
<version>.yaml
featured.json # optional
# Example:
packages/
nginx/
1.0.yaml
2.0.yaml
postgres/
1.0.yaml
featured.json
Each package directory under packages/ contains one or more versioned YAML files.
Versions are compared using dot-separated segments — numeric segments are compared numerically,
otherwise lexicographically. The storage system supports upgrading between versions and
temporary uninstallation with later restoration. Town OS ships with a
default repository
of packages, but you can add as many additional repositories as you need.
Package Definition
A complete container package definition with all available fields:
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
The image field also accepts a shorthand string form: image: nginx:1.26-alpine.
Top-Level Fields
| Field | Description |
|---|---|
image | Required (container runtime). Container image reference. Can be a string or an object with url and optional type. Mutually exclusive with vm. |
description | Short human-readable summary of the package. |
supplies | List of semantic capability tags this package provides (e.g. ["database"], ["http"]). |
command | Optional command override for the container. Mutually exclusive with proton. |
environment | Environment variables passed to the container. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]*$. Values may contain @variable@ template markers. |
network | Port mapping and domain configuration. |
volumes | Named volumes with mount configuration. |
questions | Interactive prompts shown during installation. Names must be alphanumeric (^[a-zA-Z0-9]+$). |
archives | Archive extraction specs to pre-populate volumes from container images. |
git_sources | Git repositories to clone into volumes. |
templates | File templates rendered with Go text/template and written to volumes on install. |
notes | Key-value metadata displayed after installation. Supports template substitution. |
vm | Virtual machine configuration. Mutually exclusive with image and proton. |
proton | Windows application configuration via Proton. Mutually exclusive with vm and command. |
Exactly one runtime must be specified: image (container), vm (virtual machine),
or proton (Windows app). These are mutually exclusive.
Image Field
The image field identifies the container image to run. It accepts two forms:
# Object form
image:
url: nginx:1.26-alpine
type: oci # optional, defaults to "oci"
# Shorthand string form
image: nginx:1.26-alpine Short image names are automatically normalized during compilation:
| Input | Normalized To |
|---|---|
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 |
The only valid type value is oci (the default). Image URLs must contain only
alphanumeric characters, @, ., _, :,
/, and - — shell metacharacters are rejected.
Supplies Tags
The supplies field declares what capabilities a package provides,
enabling categorization and filtering. Tags are free-form strings — the following
are conventional:
| Tag | Used For | Examples |
|---|---|---|
http | Web servers, CMS platforms, web applications | nginx, wordpress, gitea |
database | Relational and NoSQL databases | postgres, mysql, mongo |
cache | Caching and key-value stores | redis, memcached, valkey |
search | Search and analytics engines | elasticsearch, opensearch, solr |
messaging | Message brokers and queues | rabbitmq, nats, kafka |
monitoring | Metrics, alerting, and visualization | prometheus, grafana, telegraf |
storage | Object storage and file hosting | minio, registry, nextcloud |
Network Configuration
| Field | Description |
|---|---|
network.external | Port mappings exposed to the host. Keys are host ports, values are container ports. Both are "port" strings and may contain @variable@ templates. |
network.internal | Port mappings available only between containers on the internal network. |
network.domains | List of domain names associated with this package. May contain @variable@ templates. |
Port values must be integers between 1 and 65535 after template substitution.
Omit external, internal, or domains entirely if unused — do not use empty maps or lists.
Volumes
| Field | Description |
|---|---|
mountpoint | Required. Absolute path inside the container where the volume is mounted (must start with /). |
quota | Optional size limit (e.g. 512mb, 2gb, 1tb). Supports mb, gb, tb suffixes. May contain @variable@ templates. |
archive | Optional archive filename to pre-populate the volume with. |
git | Optional git repository URL to clone into the volume. |
uid | Optional numeric user ID for volume ownership. |
gid | Optional numeric group ID for volume ownership. |
Volume names must start with an alphanumeric character and contain only alphanumerics,
dots, hyphens, and underscores (pattern: ^[a-zA-Z0-9][a-zA-Z0-9._-]*$).
Omit volumes entirely if the package has none.
Archives
The archives field extracts files from container images into volumes at install time:
archives:
- image: nginx:latest
directory: /usr/share/nginx/html
volume: html | Field | Description |
|---|---|
image | Required. Container image to extract files from. |
directory | Required. Absolute path in the container image to extract. |
volume | Required. Name of a volume defined in this package to extract into. |
If the target volume is empty during install or reconcile, Podman pulls the image, creates a temporary container, and copies the specified directory into the volume.
Git Sources
The git_sources field clones git repositories into volumes:
git_sources:
- url: "https://github.com/example/config.git"
branch: main
volume: config | Field | Description |
|---|---|
url | Required. Git repository URL (http, https, or ssh). May contain @variable@ templates. |
branch | Branch to clone. May contain @variable@ templates. |
volume | Required. Name of a volume defined in this package to clone into. |
Questions
Questions define interactive prompts shown during package installation. User responses
replace @name@ template markers throughout the definition.
questions:
port:
query: "What external port should nginx listen on?"
type: port
default: "8080" | Field | Description |
|---|---|
query | Required. The prompt text shown to the user. |
type | Optional validation type. Omit for free-form text. |
default | Optional default value suggested to the user. |
Question Types
| Type | Validates |
|---|---|
hostname | Lowercase letter followed by lowercase alphanumerics and hyphens (pattern: ^[a-z][a-z0-9-]*$) |
port | Integer between 1 and 65535 |
bytes | Integer or number with tb, gb, or mb suffix (case-insensitive) |
volume | Alphanumeric characters, hyphens, and underscores (pattern: ^[a-zA-Z0-9-_]+$) |
archive | Any non-empty string |
secret | Any non-empty string |
duration | Integer or number with d, h, m, or s suffix (case-insensitive). Converted to seconds. |
| (omitted) | Any string — no validation |
Templates
The templates field defines files that are rendered using Go text/template
and written to volumes during installation. Templates are only written if the target file does
not already exist, preserving user modifications across upgrades.
templates:
config:
volume: data
path: config.yaml
content: |
host: {{ .Responses.hostname }}
port: {{ .Responses.port }}
name: {{ .Package.Name }}
system: {{ .System.Hostname }} | Field | Description |
|---|---|
volume | Required. Name of a volume defined in this package. |
path | Required. Relative path within the volume. Must not start with / or contain ... |
content | Required. Go text/template content to render. |
Template Data Context
The following data is available inside {{ }} expressions:
| Expression | Description |
|---|---|
.Responses.<name> | User's answer to the named question |
.Package.Name | Package name |
.Package.Version | Package version |
.Package.Repo | Repository name |
.Package.Image | Compiled container image reference |
.Package.Description | Package description |
.System.Hostname | System hostname |
.System.ExternalIP | External IP address (if known) |
.System.InternalIP | Internal/LAN IP address (if known) |
Template names follow the same rules as volume names. Files are written with mode 0600
and parent directories are created with mode 0750.
Notes
Notes provide key-value metadata displayed after installation.
notes:
URL:
value: "http://localhost:@port@"
type: url
Info:
value: "Default admin credentials are admin/admin" | Field | Description |
|---|---|
value | Required. The note text. Supports @variable@ template substitution. |
type | Optional validation type: url, phone, or email. Omit for plain text. |
Template System
Town OS has two template systems that operate at different stages of compilation.
@variable@ Substitution
The @variable@ syntax is substituted during package compilation across all
configurable fields: environment values, network port mappings, network domains, volume
mountpoints, volume quotas, volume git URLs, git source URLs and branches, template
volume and path fields, VM image and memory, Proton configuration, and note values.
Each question name becomes a variable. To include a literal @ character
(e.g. for a git SSH URL like git@@domain@), use @@ — two
consecutive @ characters produce one literal @.
Unresolved variables (referencing a name with no matching question response) are left as-is in the output.
Built-in Variables
| Variable | Description |
|---|---|
@LOCAL_EXTERNAL_HOST@ | The external hostname or IP of the Town OS host |
@LOCAL_INTERNAL_HOST@ | The internal hostname or IP of the Town OS host |
@PACKAGE_DNS@ | The DNS name assigned to this package on the internal network |
Built-in variables are substituted before user question responses, so they take precedence.
Go Templates (in templates field)
The templates field uses Go text/template syntax
({{ .Responses.name }}) for rendering files written to volumes.
See the Templates section for available data context.
VM Runtime
Packages can run virtual machines instead of containers by specifying a vm
field instead of image:
vm:
image: "https://example.com/my-vm.qcow2"
memory: 2gb
cpus: 2
description: A virtual machine package | Field | Description |
|---|---|
image | Required. URL (http/https) or filename of the VM disk image. May contain @variable@ templates. |
memory | Memory allocation using byte suffixes (e.g. 1gb, 512mb). Defaults to 1gb. May contain @variable@ templates. |
cpus | Number of virtual CPUs. Defaults to 1. Must be non-negative. |
The vm field is mutually exclusive with image and proton.
VM images are not normalized like container images.
Proton Runtime
Packages can run Windows applications via Proton by specifying a proton field:
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 | Field | Description |
|---|---|
app_image | Required. Container image containing the Windows application. |
app_directory | Required. Absolute path where the application is located. |
volume | Required. Name of a volume defined in this package. |
exe | Required. Path to the Windows executable. |
args | Optional list of command-line arguments. |
When proton is set, the container command is automatically generated as
["proton", "run", <exe>, ...<args>]. The proton
field is mutually exclusive with vm and command.
Featured Packages
A package repository can include a featured.json file at its root
(alongside the packages/ directory) to highlight selected packages in the UI.
The file contains a JSON array of package name strings — no version or repository prefix:
["wordpress", "nextcloud", "postgres"]
Packages listed in featured.json appear with featured: true in
the API's package list response, allowing the UI to highlight them. The file is optional
— if absent, no packages are featured.
Importing Repositories
Once you've created a package repository, add it to your Town OS instance using either the web UI or direct filesystem configuration.
Via the UI
Log in to the Town OS dashboard, navigate to Packages, select the Repositories tab, and click Add Repository. Provide a name, the git URL, and optional credentials. Click Refresh to pull package metadata immediately.
Via the Filesystem
Edit repositories.json in the btrfs package data directory. The file is a
JSON array of objects:
[
{"name": "default", "url": "https://gitea.com/town-os/default-packages"},
{"name": "my-packages", "url": "https://github.com/myuser/my-packages"}
] Order matters — later entries override earlier ones when package names collide. For private repositories, embed credentials in the URL. Restart the service or hit Refresh in the UI for changes to take effect.
Validation Rules
The following constraints are enforced during package validation and compilation:
| Field | Rule |
|---|---|
| Image URL | Non-empty; characters must match ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$ |
| Image type | Empty (defaults to oci) or oci |
| Environment keys | Must match ^[a-zA-Z_][a-zA-Z0-9_]*$ (POSIX convention) |
| Question names | Must match ^[a-zA-Z0-9]+$ |
| Volume names | Must match ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ |
| Mountpoints | Must start with / |
| Template names | Same rules as volume names |
| Template paths | Non-empty, relative (no leading /), no .. traversal |
| Archive directory | Must be an absolute path |
| Archive/git volume | Must reference a volume defined in the package |
| Git URLs | Must have a valid scheme and host (except file://) |
| VM CPUs | Must be non-negative |
| Proton app_directory | Must be an absolute path |
| Runtime | Exactly one of image, vm, or proton |
Validation runs before template substitution. Fields containing @variable@
markers skip path and URL validation until after compilation resolves the templates.
Style Guidelines
- Omit empty maps (
environment:,internal:,volumes:) — leave them out entirely. - Omit
typeon questions that accept free-form text — do not writetype:with no value. - Include
descriptionwith a short summary of what the package is. - Include
supplieswith relevant capability tags when the package provides a well-known service. - Include
noteswith connection URLs and any important post-install information. - Use
@variable@templates to let users customize ports, hostnames, and credentials at install time. - Use
templatesfor config files that need Go template logic — they are only written once and preserve user edits.