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

FieldDescription
imageRequired (container runtime). Container image reference. Can be a string or an object with url and optional type. Mutually exclusive with vm.
descriptionShort human-readable summary of the package.
suppliesList of semantic capability tags this package provides (e.g. ["database"], ["http"]).
commandOptional command override for the container. Mutually exclusive with proton.
environmentEnvironment variables passed to the container. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]*$. Values may contain @variable@ template markers.
networkPort mapping and domain configuration.
volumesNamed volumes with mount configuration.
questionsInteractive prompts shown during installation. Names must be alphanumeric (^[a-zA-Z0-9]+$).
archivesArchive extraction specs to pre-populate volumes from container images.
git_sourcesGit repositories to clone into volumes.
templatesFile templates rendered with Go text/template and written to volumes on install.
notesKey-value metadata displayed after installation. Supports template substitution.
vmVirtual machine configuration. Mutually exclusive with image and proton.
protonWindows 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:

InputNormalized To
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

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:

TagUsed ForExamples
httpWeb servers, CMS platforms, web applicationsnginx, wordpress, gitea
databaseRelational and NoSQL databasespostgres, mysql, mongo
cacheCaching and key-value storesredis, memcached, valkey
searchSearch and analytics engineselasticsearch, opensearch, solr
messagingMessage brokers and queuesrabbitmq, nats, kafka
monitoringMetrics, alerting, and visualizationprometheus, grafana, telegraf
storageObject storage and file hostingminio, registry, nextcloud

Network Configuration

FieldDescription
network.externalPort mappings exposed to the host. Keys are host ports, values are container ports. Both are "port" strings and may contain @variable@ templates.
network.internalPort mappings available only between containers on the internal network.
network.domainsList 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

FieldDescription
mountpointRequired. Absolute path inside the container where the volume is mounted (must start with /).
quotaOptional size limit (e.g. 512mb, 2gb, 1tb). Supports mb, gb, tb suffixes. May contain @variable@ templates.
archiveOptional archive filename to pre-populate the volume with.
gitOptional git repository URL to clone into the volume.
uidOptional numeric user ID for volume ownership.
gidOptional 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
FieldDescription
imageRequired. Container image to extract files from.
directoryRequired. Absolute path in the container image to extract.
volumeRequired. 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
FieldDescription
urlRequired. Git repository URL (http, https, or ssh). May contain @variable@ templates.
branchBranch to clone. May contain @variable@ templates.
volumeRequired. 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"
FieldDescription
queryRequired. The prompt text shown to the user.
typeOptional validation type. Omit for free-form text.
defaultOptional default value suggested to the user.

Question Types

TypeValidates
hostnameLowercase letter followed by lowercase alphanumerics and hyphens (pattern: ^[a-z][a-z0-9-]*$)
portInteger between 1 and 65535
bytesInteger or number with tb, gb, or mb suffix (case-insensitive)
volumeAlphanumeric characters, hyphens, and underscores (pattern: ^[a-zA-Z0-9-_]+$)
archiveAny non-empty string
secretAny non-empty string
durationInteger 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 }}
FieldDescription
volumeRequired. Name of a volume defined in this package.
pathRequired. Relative path within the volume. Must not start with / or contain ...
contentRequired. Go text/template content to render.

Template Data Context

The following data is available inside {{ }} expressions:

ExpressionDescription
.Responses.<name>User's answer to the named question
.Package.NamePackage name
.Package.VersionPackage version
.Package.RepoRepository name
.Package.ImageCompiled container image reference
.Package.DescriptionPackage description
.System.HostnameSystem hostname
.System.ExternalIPExternal IP address (if known)
.System.InternalIPInternal/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"
FieldDescription
valueRequired. The note text. Supports @variable@ template substitution.
typeOptional 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

VariableDescription
@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
FieldDescription
imageRequired. URL (http/https) or filename of the VM disk image. May contain @variable@ templates.
memoryMemory allocation using byte suffixes (e.g. 1gb, 512mb). Defaults to 1gb. May contain @variable@ templates.
cpusNumber 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
FieldDescription
app_imageRequired. Container image containing the Windows application.
app_directoryRequired. Absolute path where the application is located.
volumeRequired. Name of a volume defined in this package.
exeRequired. Path to the Windows executable.
argsOptional 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.

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:

FieldRule
Image URLNon-empty; characters must match ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$
Image typeEmpty (defaults to oci) or oci
Environment keysMust match ^[a-zA-Z_][a-zA-Z0-9_]*$ (POSIX convention)
Question namesMust match ^[a-zA-Z0-9]+$
Volume namesMust match ^[a-zA-Z0-9][a-zA-Z0-9._-]*$
MountpointsMust start with /
Template namesSame rules as volume names
Template pathsNon-empty, relative (no leading /), no .. traversal
Archive directoryMust be an absolute path
Archive/git volumeMust reference a volume defined in the package
Git URLsMust have a valid scheme and host (except file://)
VM CPUsMust be non-negative
Proton app_directoryMust be an absolute path
RuntimeExactly 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 type on questions that accept free-form text — do not write type: with no value.
  • Include description with a short summary of what the package is.
  • Include supplies with relevant capability tags when the package provides a well-known service.
  • Include notes with connection URLs and any important post-install information.
  • Use @variable@ templates to let users customize ports, hostnames, and credentials at install time.
  • Use templates for config files that need Go template logic — they are only written once and preserve user edits.