Formato de empaquetado

Los paquetes de Town OS son definiciones YAML que describen cómo ejecutar un servicio en contenedor, incluyendo su imagen, su red, su almacenamiento y las preguntas de configuración que ve el usuario. Los paquetes también pueden ejecutar máquinas virtuales o aplicaciones de Windows mediante Proton.

Estructura del repositorio

Un repositorio de paquetes es cualquier repositorio git que contenga un directorio packages/. Cualquiera puede crear uno — aloja tus propios paquetes para la familia, los amigos o despliegues a medida. La estructura es simple:

packages/
  <nombre-del-paquete>/
    <versión>.yaml
featured.json          # opcional

# Ejemplo:
packages/
  nginx/
    1.0.yaml
    2.0.yaml
  postgres/
    1.0.yaml
featured.json

Cada directorio de paquete bajo packages/ contiene uno o más ficheros YAML versionados. Las versiones se comparan por segmentos separados por punto — los segmentos numéricos se comparan como números, y el resto en orden lexicográfico. El sistema de almacenamiento admite actualizar entre versiones y desinstalar temporalmente para restaurar después. Town OS trae un repositorio predeterminado de paquetes, pero puedes añadir tantos repositorios extra como necesites.

Definición de un paquete

Una definición completa de paquete en contenedor, con todos los campos disponibles:

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

El campo image también acepta una forma abreviada de cadena: image: nginx:1.26-alpine.

Campos de nivel superior

CampoDescripción
imageObligatorio (entorno de contenedor). Referencia a la imagen del contenedor. Puede ser una cadena o un objeto con url y un type opcional. Mutuamente excluyente con vm.
descriptionResumen breve y legible del paquete.
suppliesLista de etiquetas semánticas de las capacidades que ofrece el paquete (por ejemplo ["database"], ["http"]).
commandComando opcional que sustituye al del contenedor. Mutuamente excluyente con proton.
environmentVariables de entorno que se le pasan al contenedor. Las claves deben cumplir ^[a-zA-Z_][a-zA-Z0-9_]*$. Los valores pueden llevar marcadores de plantilla @variable@.
networkConfiguración de asignación de puertos y de dominios.
volumesVolúmenes con nombre y su configuración de montaje.
questionsPreguntas interactivas que se muestran durante la instalación. Los nombres deben ser alfanuméricos (^[a-zA-Z0-9]+$).
archivesEspecificaciones de extracción para rellenar volúmenes desde imágenes de contenedor.
git_sourcesRepositorios git que se clonan dentro de los volúmenes.
templatesPlantillas de fichero que se generan con text/template de Go y se escriben en los volúmenes al instalar.
notesDatos de clave y valor que se muestran después de instalar. Admiten sustitución de plantillas.
vmConfiguración de máquina virtual. Mutuamente excluyente con image y proton.
protonConfiguración de aplicación de Windows mediante Proton. Mutuamente excluyente con vm y command.

Hay dos tipos de entorno de ejecución: container (el predeterminado) y vm. Un paquete especifica image o proton para el entorno de contenedor, o vm para el de máquina virtual. Proton es una especialización del entorno de contenedor — usa Podman por debajo, pero genera el comando solo y extrae los ficheros de la aplicación de Windows desde otra imagen de contenedor. Estos campos son mutuamente excluyentes: un paquete debe incluir exactamente uno de image/proton o vm.

El campo image

El campo image identifica la imagen de contenedor que se va a ejecutar. Acepta dos formas:

# Forma de objeto
image:
  url: nginx:1.26-alpine
  type: oci                # opcional, por omisión "oci"

# Forma abreviada de cadena
image: nginx:1.26-alpine

Los nombres cortos de imagen se normalizan solos durante la compilación:

EntradaSe normaliza a
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

El único valor válido de type es oci (el predeterminado). Las URL de imagen solo pueden contener caracteres alfanuméricos, @, ., _, :, / y - — los metacaracteres del shell se rechazan.

Etiquetas supplies

El campo supplies declara qué capacidades ofrece un paquete, lo que permite clasificarlos y filtrarlos. Las etiquetas son cadenas libres — estas son las convencionales:

EtiquetaSe usa paraEjemplos
httpServidores web, plataformas CMS, aplicaciones webnginx, wordpress, gitea
databaseBases de datos relacionales y NoSQLpostgres, mysql, mongo
cacheCachés y almacenes de clave y valorredis, memcached, valkey
searchMotores de búsqueda y analíticaelasticsearch, opensearch, solr
messagingIntermediarios de mensajes y colasrabbitmq, nats, kafka
monitoringMétricas, alertas y visualizaciónprometheus, grafana, telegraf
storageAlmacenamiento de objetos y alojamiento de ficherosminio, registry, nextcloud

Configuración de red

CampoDescripción
network.externalAsignaciones de puertos expuestas directamente en el anfitrión. Las claves son puertos del anfitrión y los valores puertos del contenedor. Ambos son cadenas de "puerto" y pueden llevar plantillas @variable@.
network.internalAsignaciones de puertos accesibles solo desde la propia red del paquete (y desde la entrada HTTP compartida). Las claves son puertos del lado del anfitrión o redirigidos, y los valores puertos del contenedor.
network.domainsLista opcional de FQDN adicionales de cara a internet para los que la entrada HTTP debe obtener un certificado de confianza pública (Let’s Encrypt / ACME). Puede llevar plantillas @variable@.

Los valores de puerto deben ser enteros entre 1 y 65535 después de sustituir las plantillas. Omite external, internal o domains por completo si no los usas — no pongas mapas ni listas vacías.

La clave de una entrada de puerto es o bien una cadena numérica de puerto ("2222": "22") o bien un nombre semántico que cumpla ^[a-zA-Z][a-zA-Z0-9_]*$ (http: "3000"). Ponerle nombre a un puerto permite que los paquetes padre lo referencien por su papel (@dep_KEY_port_http@) en lugar de por número, y — para el nombre especial http — inscribe ese puerto en la entrada HTTP compartida que se describe abajo.

Entrada HTTP (el puerto llamado http)

Town OS ejecuta una sola entrada compartida en el puerto :443 que termina el TLS de todos los paquetes y hace de proxy inverso hacia el puerto HTTP en claro del contenedor. Un paquete se suma a esa entrada llamando http a un puerto internal:

network:
  internal:
    http: "3000"      # puerto HTTP en claro del contenedor

Cuando haces esto:

  • El servicio queda accesible en https://<PACKAGE_DNS>/sin puerto en la URL. No hay puerto del anfitrión que elegir; tampoco asignes el puerto HTTP bajo external.
  • La entrada presenta un certificado hoja de confianza local (emitido por la CA integrada de Rolodex) para <PACKAGE_DNS> (por ejemplo gitea.default.home), y Rolodex publica registros DANE TLSA en _443 para que los clientes que entienden DANE puedan fijar el certificado.
  • Como la entrada es dueña del :443, cualquier URL que genere la aplicación tiene que ser HTTPS sin puerto. Configura la URL pública de la aplicación en consecuencia (por ejemplo, el GITEA__server__ROOT_URL: "https://@PACKAGE_DNS@/" de gitea) y deja que la aplicación siga escuchando en HTTP en claro dentro del contenedor.

Los protocolos que no son HTTP (SSH, bases de datos, etc.) no deben usar el nombre http — no son HTTP y la entrada no puede terminarles el TLS. Redirígelos tal cual con una asignación numérica:

network:
  internal:
    http: "3000"          # detrás de la entrada :443, con TLS terminado
    "@sshport@": "22"     # redirección TCP pura, nunca envuelta en TLS

Nombres de cara a internet (domains)

De forma predeterminada, la entrada solo sirve el nombre <PACKAGE_DNS> de confianza local. Para exponer además un servicio a internet con un certificado de confianza pública, lista los FQDN reales bajo network.domains y apunta el DNS público de ese nombre a este anfitrión. La entrada obtiene un certificado ACME (Let’s Encrypt) para cada dominio listado:

network:
  internal:
    http: "3000"
  domains:
    - git.example.com

Volúmenes

CampoDescripción
mountpointObligatorio. Ruta absoluta dentro del contenedor donde se monta el volumen (debe empezar con /).
quotaLímite de tamaño opcional (por ejemplo 512mb, 2gb, 1tb). Admite los sufijos mb, gb y tb. Puede llevar plantillas @variable@.
archiveNombre opcional de fichero comprimido con el que rellenar el volumen de antemano.
gitURL opcional de un repositorio git que se clona dentro del volumen.
uidID numérico de usuario opcional para el propietario del volumen.
gidID numérico de grupo opcional para el propietario del volumen.

Los nombres de volumen deben empezar con un carácter alfanumérico y contener solo alfanuméricos, puntos, guiones y guiones bajos (patrón: ^[a-zA-Z0-9][a-zA-Z0-9._-]*$). Omite volumes por completo si el paquete no tiene ninguno.

Ficheros comprimidos

El campo archives extrae ficheros desde imágenes de contenedor hacia los volúmenes durante la instalación:

archives:
  - image: nginx:latest
    directory: /usr/share/nginx/html
    volume: html
CampoDescripción
imageObligatorio. Imagen de contenedor de la que se extraen los ficheros.
directoryObligatorio. Ruta absoluta dentro de la imagen que se va a extraer.
volumeObligatorio. Nombre de un volumen definido en este paquete al que se extraerá.

Si el volumen de destino está vacío durante la instalación o la reconciliación, Podman descarga la imagen, crea un contenedor temporal y copia el directorio indicado dentro del volumen.

Orígenes git

El campo git_sources clona repositorios git dentro de los volúmenes:

git_sources:
  - url: "https://github.com/example/config.git"
    branch: main
    volume: config
CampoDescripción
urlObligatorio. URL del repositorio git (http, https o ssh). Puede llevar plantillas @variable@.
branchRama que se va a clonar. Puede llevar plantillas @variable@.
volumeObligatorio. Nombre de un volumen definido en este paquete dentro del cual se clonará.

Preguntas

Las preguntas definen los mensajes interactivos que aparecen al instalar un paquete. Las respuestas del usuario sustituyen los marcadores de plantilla @name@ por toda la definición.

questions:
  port:
    query: "What external port should nginx listen on?"
    type: port
    default: "8080"
CampoDescripción
queryObligatorio. El texto del mensaje que ve el usuario.
typeTipo de validación opcional. Omítelo para texto libre.
defaultValor predeterminado opcional que se le sugiere al usuario.
optionalPonlo en true para permitir dejar la pregunta en blanco. Cualquier otra pregunta debe responderse con un valor no vacío.
oauthObligatorio con type: oauth, y solo válido ahí. El flujo de dispositivo que ejecuta el diálogo de instalación para obtener un token.
show_ifNombra una pregunta boolean del mismo paquete. Esta pregunta queda oculta en el diálogo de instalación hasta que se marque esa casilla, y mientras esté apagada se compila a la cadena vacía (y queda exenta del requisito de responderla) — así un paquete puede esconder un grupo avanzado detrás de un solo interruptor. La pregunta referenciada debe existir, ser booleana y no ser ella misma condicional.

Tipos de pregunta

TipoQué valida
hostnameUna letra minúscula seguida de alfanuméricos en minúscula y guiones (patrón: ^[a-z][a-z0-9-]*$). Genera solo <nombre-del-paquete>-<4-hex> cuando está vacío.
portEntero entre 1 y 65535. Genera solo un puerto libre al azar en el rango 10000-60000 cuando está vacío o puesto en "auto".
bytesEntero o número con sufijo tb, gb o mb (sin distinguir mayúsculas)
volumeCaracteres alfanuméricos, guiones y guiones bajos (patrón: ^[a-zA-Z0-9-_]+$)
archiveCualquier cadena no vacía
secretGenera sola una cadena hexadecimal de 64 caracteres (256 bits) cuando está vacía o puesta en "auto". Se puede sobrescribir con un valor explícito.
durationEntero o número con sufijo d, h, m o s (sin distinguir mayúsculas). Se convierte a segundos.
booleantrue, false, t, f, 1 o 0 (sin distinguir mayúsculas). Se normaliza a la cadena true o false. Se dibuja como casilla en el diálogo de instalación; una pregunta sin responder toma el default, o false si no se declaró ninguno.
oauthUn token que se obtiene ejecutando un flujo de dispositivo OAuth desde el diálogo de instalación, en vez de escribiéndolo. Se dibuja como un botón de Conectar; el token que vuelve es la respuesta. Se guarda y se muestra igual que un secret.
(omitido)Cualquier cadena — sin validación

Preguntas booleanas

Una pregunta boolean se muestra como casilla en vez de campo de texto, y su respuesta se sustituye como la cadena literal true o false. Como una casilla sin marcar sigue siendo una respuesta de verdad, el usuario puede apagar una pregunta con default: "true" — el false explícito prevalece sobre el predeterminado.

environment:
  REGISTRATION_OPEN: "@open@"
  METRICS_ENABLED: "@metrics@"
questions:
  open:
    query: "Allow open registration?"
    type: boolean          # sin responder -> "false"
  metrics:
    query: "Enable metrics?"
    type: boolean
    default: "true"        # sin responder -> "true"

El valor normalizado es también lo que ven las plantillas de fichero, así que {{ .Responses.metrics }} se genera como true o false y se puede evaluar con {{ if eq .Responses.metrics "true" }}.

Preguntas opcionales

Toda pregunta debe responderse con un valor no vacío, salvo que declare optional: true. Sin eso, un ajuste del que la aplicación realmente puede prescindir — un relay SMTP, una clave de API — no tiene forma honesta de expresarse: el autor tiene que inventarse un valor de relleno y confiar en que el operador lo cambie.

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"

Si se deja en blanco, una pregunta opcional sustituye la cadena vacía en sus marcadores @marker@, de modo que la aplicación ve la variable vacía en vez de puesta en algo que nadie eligió. Nunca se genera sola: un secret opcional en blanco se queda en blanco, en lugar de convertirse en una cadena aleatoria con la que la aplicación intentaría autenticarse obedientemente.

optional se combina con type — un puerto opcional que sí se respondió se sigue validando como puerto, mientras que uno en blanco se compila hasta desaparecer. No tiene sentido en un boolean, que es una casilla y siempre acaba en uno de sus dos valores.

Preguntas OAuth

Algunas aplicaciones se configuran con una credencial que solo su proveedor puede emitir — un token de cuenta de Plex, un token personal de GitHub — y lo normal para conseguirla es ejecutar un script en una terminal y pegar lo que imprime. Una pregunta oauth ejecuta ese flujo desde el diálogo de instalación: el operador pulsa Conectar, aprueba en una pestaña del navegador, y el token que vuelve se convierte en la respuesta.

No hay un registro de proveedores. La pregunta trae un bloque oauth que nombra las URL del propio proveedor, así que cualquier proveedor con un flujo de tipo dispositivo funciona sin cambiarle nada a 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
CampoDescripción
startObligatorio. La petición que abre el flujo: method (por omisión GET), url, y opcionalmente headers y un cuerpo form.
extractCampos JSON que se sacan de la respuesta inicial y quedan disponibles para las plantillas de abajo, escritos como nombre: campo_json.
approveObligatorio. La URL que abre el operador para aprobar. Se abre en una pestaña nueva y además se muestra como enlace, por si un bloqueador de ventanas emergentes se la come.
user_codePlantilla opcional para un código corto que el operador debe escribir en la página de aprobación. GitHub muestra uno; Plex no.
pollObligatorio. La petición que se repite hasta la aprobación. Con la misma forma que start.
tokenObligatorio. El campo JSON de la respuesta del sondeo que contiene el token. Que falte o sea null es la forma en que un proveedor dice “todavía no está aprobado”.
intervalCada cuánto sondear. Por omisión 5s; respeta el límite de tasa documentado del proveedor.
timeoutCuánto tiempo seguir sondeando antes de rendirse. Por omisión 5m.

Los marcadores {{...}} en las URL, las cabeceras y los valores de formulario se resuelven con lo que haya nombrado extract, más {{client_id}} — un identificador aleatorio que Town OS genera por cada flujo y envía en cada paso, que es a lo que Plex ata su pin.

El token es una credencial, así que se trata como tal: enmascarado en el panel de información del paquete, copiable pero nunca impreso. Se cachea igual que cualquier otra respuesta, así que reinstalar o actualizar lo reutiliza en vez de mandar al operador de vuelta con el proveedor.

Como quien llama a esas URL es el controlador del sistema — no el navegador — tienen que ser https y no pueden resolverse a una dirección de bucle local, privada, de enlace local ni de CGNAT. La comprobación se hace cuando la conexión se establece de verdad, y otra vez en cada redirección, así que un paquete no puede usar un flujo para que el controlador meta la mano en la propia red del anfitrión.

Plantillas

El campo templates define ficheros que se generan con text/template de Go y se escriben en los volúmenes durante la instalación. Las plantillas solo se escriben si el fichero de destino no existe todavía, así se conservan las modificaciones del usuario entre actualizaciones.

templates:
  config:
    volume: data
    path: config.yaml
    content: |
      host: {{ .Responses.hostname }}
      port: {{ .Responses.port }}
      name: {{ .Package.Name }}
      system: {{ .System.Hostname }}
CampoDescripción
volumeObligatorio. Nombre de un volumen definido en este paquete.
pathObligatorio. Ruta relativa dentro del volumen. No debe empezar con / ni contener ...
contentObligatorio. El contenido text/template de Go que se va a generar.

Contexto de datos de las plantillas

Dentro de las expresiones {{ }} están disponibles estos datos:

ExpresiónDescripción
.Responses.<nombre>La respuesta del usuario a la pregunta indicada
.Package.NameNombre del paquete
.Package.VersionVersión del paquete
.Package.RepoNombre del repositorio
.Package.ImageReferencia compilada de la imagen de contenedor
.Package.DescriptionDescripción del paquete
.System.HostnameNombre de host del sistema
.System.ExternalIPDirección IP externa (si se conoce)
.System.InternalIPDirección IP interna o de red local (si se conoce)

Los nombres de plantilla siguen las mismas reglas que los de volumen. Los ficheros se escriben con permisos 0600 y los directorios padre se crean con 0750.

Notas

Las notas ofrecen datos de clave y valor que se muestran después de instalar.

notes:
  URL:
    value: "http://localhost:@port@"
    type: url
  Info:
    value: "Default admin credentials are admin/admin"
CampoDescripción
valueObligatorio. El texto de la nota. Admite sustitución de plantillas @variable@.
typeTipo de validación opcional: url, phone o email. Omítelo para texto simple.

Dependencias

Los paquetes pueden declarar dependencias de otros paquetes. Las dependencias comparten la red de podman del paquete padre, lo que permite que los contenedores del mismo árbol de dependencias se comuniquen directamente por nombre de contenedor gracias al DNS integrado de podman.

dependencies:
  db:
    package: postgres
    responses:
      password: "@dbpass@"
      user: "mattermost"
      database: "mattermost"
      port: "5432"
CampoDescripción
packageObligatorio. Nombre del paquete de la dependencia que se va a instalar.
repoRepositorio que contiene la dependencia. Por omisión, el del paquete padre.
versionVersión que se va a instalar. Por omisión, la más reciente disponible.
responsesRespuestas a las preguntas de la dependencia. Los valores admiten la sintaxis @variable@ con las preguntas del padre.

Los paquetes padre reciben variables de entorno para cada dependencia en tiempo de ejecución: TOWNOS_DEP_{KEY}_HOST (el nombre del contenedor) y TOWNOS_DEP_{KEY}_PORT_{port} (el número de puerto del lado del contenedor). Los padres también pueden usar las variables de plantilla @dep_KEY_host@ y @dep_KEY_port_N@ en los valores de su entorno (mira el sistema de plantillas).

Ejemplo: un paquete de Mattermost con una dependencia db de PostgreSQL puede referirse al host de la base de datos en su URL de origen de datos:

environment:
  MM_SQLSETTINGS_DATASOURCE: "postgres://mattermost:@dbpass@@@dep_db_host@:@dep_db_port_5432@/mattermost?sslmode=disable"

Sistema de plantillas

Town OS tiene dos sistemas de plantillas que actúan en distintas etapas de la compilación.

Sustitución @variable@

La sintaxis @variable@ se sustituye durante la compilación del paquete en todos los campos configurables: valores de entorno, asignaciones de puertos, dominios de red, puntos de montaje de volúmenes, cuotas de volúmenes, URL git de volúmenes, URL y ramas de los orígenes git, los campos volume y path de las plantillas, la imagen y la memoria de las máquinas virtuales, la configuración de Proton y los valores de las notas.

El nombre de cada pregunta se convierte en una variable. Para incluir un carácter @ literal (por ejemplo, en una URL SSH de git como git@@domain@), usa @@ — dos @ seguidos producen un @ literal.

Las variables sin resolver (las que apuntan a un nombre sin respuesta de pregunta asociada) se dejan tal cual en la salida.

Variables integradas

VariableDescripción
@LOCAL_EXTERNAL_HOST@El nombre de host o la IP externa del anfitrión con Town OS
@LOCAL_INTERNAL_HOST@El nombre de host o la IP interna del anfitrión con Town OS
@PACKAGE_DNS@El nombre DNS asignado a este paquete en la red interna
@dep_KEY_host@Nombre de host del contenedor de la dependencia KEY (resoluble por el DNS de podman en la red compartida). Solo está disponible si el paquete declara dependencias.
@dep_KEY_port_N@Puerto N del contenedor de la dependencia KEY. Solo está disponible si el paquete declara dependencias.

Las variables integradas se sustituyen antes que las respuestas del usuario, así que tienen prioridad. Las variables de plantilla de dependencias (@dep_*@) se resuelven después de instalar la dependencia y se aplican a los valores de entorno del padre. KEY es el nombre de la clave de dependencia en minúsculas, y N es el número de puerto del contenedor.

Plantillas de Go (en el campo templates)

El campo templates usa la sintaxis text/template de Go ({{ .Responses.name }}) para generar los ficheros que se escriben en los volúmenes. El contexto de datos disponible está en la sección de plantillas.

Entorno de máquina virtual

Los paquetes pueden ejecutar máquinas virtuales en lugar de contenedores, especificando un campo vm en vez de image:

vm:
  image: "https://example.com/my-vm.qcow2"
  memory: 2gb
  cpus: 2
description: A virtual machine package
CampoDescripción
imageObligatorio. URL (http/https) o nombre de fichero de la imagen de disco de la máquina virtual. Puede llevar plantillas @variable@.
memoryMemoria asignada con sufijos de bytes (por ejemplo 1gb, 512mb). Por omisión 1gb. Puede llevar plantillas @variable@.
cpusNúmero de CPU virtuales. Por omisión 1. Debe ser no negativo.

El campo vm es mutuamente excluyente con image y proton. Las imágenes de máquina virtual no se normalizan como las de contenedor.

Entorno Proton

Los paquetes pueden ejecutar aplicaciones de Windows mediante Proton especificando un campo proton:

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
CampoDescripción
app_imageObligatorio. Imagen de contenedor que contiene la aplicación de Windows.
app_directoryObligatorio. Ruta absoluta donde se encuentra la aplicación.
volumeObligatorio. Nombre de un volumen definido en este paquete.
exeObligatorio. Ruta al ejecutable de Windows.
argsLista opcional de argumentos de línea de comandos.

Cuando se define proton, el comando del contenedor se genera solo como ["proton", "run", <exe>, ...<args>]. La imagen de contenedor toma por omisión el ajuste global proton_image (quay.io/town/proton:latest), que se puede cambiar por paquete definiendo image. El campo app_image se normaliza durante la compilación con las mismas reglas que cualquier referencia de imagen de contenedor. El campo proton es mutuamente excluyente con vm y command.

Compilación

La compilación transforma la definición de un paquete y las respuestas del usuario en una configuración completamente resuelta y lista para ejecutarse. La secuencia de compilación hace estos pasos:

  • Valida todas las respuestas contra los tipos declarados
  • Aplica la validación propia de cada tipo (rangos de puertos, patrones de nombre de host, análisis de bytes, etc.)
  • Sustituye todos los marcadores de plantilla @variable@ por los valores resueltos
  • Normaliza las URL de las imágenes de contenedor a referencias completamente cualificadas
  • Produce un paquete resuelto y listo para instalarse

En los paquetes de máquina virtual, las cadenas de memoria (por ejemplo 2gb) se convierten a cantidad de bytes y se aplican los valores predeterminados de CPU. Los errores de validación se juntan de todos los campos y se devuelven de una vez, para que el usuario pueda arreglarlo todo en una sola pasada en lugar de irse topando con los errores uno por uno.

Persistencia de las respuestas

Las respuestas se guardan por versión en responses/<repo>/<pkg>/<versión>.json. También se guarda una copia last en responses/last/<repo>/<pkg>.json para reutilizarla al actualizar y al reinstalar desde volúmenes desinstalados.

Las respuestas de last se borran tras una instalación correcta. Eso significa que si se desinstala un paquete y luego se vuelve a instalar, las respuestas anteriores se ofrecen como predeterminadas. En cuanto la nueva instalación sale bien, la copia en caché se elimina.

Flujo de instalación

Instalar un paquete recorre una secuencia de pasos bien definida. El proceso se encarga de todo, desde preparar los ficheros hasta arrancar el servicio:

  • Crear un enlace duro desde el fichero del paquete en el repositorio hacia el directorio de instalados
  • Persistir las respuestas (la de esa versión y la copia last)
  • Crear los volúmenes con sus cuotas y, si procede, con propietario UID/GID
  • Rellenar los volúmenes desde ficheros comprimidos y orígenes git (solo en el entorno de contenedor)
  • Aplicar las plantillas (generar los ficheros dentro de los volúmenes)
  • Generar la unidad de systemd (basada en Podman para contenedores, en QEMU para máquinas virtuales)
  • Crear el fichero de estado de red
  • Arrancar el servicio
  • Borrar las respuestas de last si todo ha ido bien

Hay dos indicadores opcionales que controlan el comportamiento de los volúmenes:

  • reuse_volumes — reutiliza los volúmenes de una versión anterior desinstalada del mismo paquete
  • import_from_version — importa los volúmenes de una versión anterior concreta

Desinstalación

Desinstalar un paquete conserva los datos de los volúmenes de forma predeterminada. Los volúmenes se mueven del prefijo installed/ al prefijo uninstalled/ en lugar de borrarse. Así se puede reinstalar con los datos originales intactos.

El proceso de desinstalación también borra el fichero de estado de red y detiene, deshabilita y desinstala las unidades de systemd asociadas.

Para borrar los volúmenes de inmediato en lugar de conservarlos, usa el indicador purge_volumes. Los volúmenes purgados no se pueden recuperar.

Vista previa de la instalación

Antes de comprometerte con una instalación, puedes ver qué se crearía. El endpoint de vista previa (POST /packages/install-preview) devuelve un resumen de la instalación planificada sin hacer ningún cambio:

  • Los volúmenes que se crearían
  • Los puertos que se asignarían
  • Información de la actualización (si vienes de una versión anterior)
  • El tipo de entorno (contenedor o máquina virtual)
  • Si el paquete tiene preguntas que responder
  • Los detalles de la máquina virtual (imagen, memoria, CPU) en los paquetes de ese tipo

Un repositorio de paquetes puede incluir un fichero featured.json en su raíz (junto al directorio packages/) para destacar ciertos paquetes en la interfaz.

El fichero contiene un array JSON de cadenas con nombres de paquete — sin versión ni prefijo de repositorio:

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

Los paquetes listados en featured.json aparecen con featured: true en la respuesta del listado de paquetes de la API, lo que permite a la interfaz destacarlos. El fichero es opcional — si no está, no se destaca ningún paquete.

Importar repositorios

Una vez que hayas creado un repositorio de paquetes, añádelo a tu instancia de Town OS desde la interfaz web o configurando el sistema de ficheros directamente.

Desde la interfaz

Entra en el panel de Town OS, ve a Paquetes, elige la pestaña Repositorios y pulsa Añadir repositorio. Pon un nombre, la URL de git y, si hace falta, las credenciales. Pulsa Actualizar para traer los metadatos de los paquetes de inmediato.

Desde el sistema de ficheros

Edita repositories.json en el directorio btrfs de datos de paquetes. El fichero es un array JSON de objetos:

[
  {"name": "default", "url": "https://github.com/town-os/default-packages"},
  {"name": "my-packages", "url": "https://github.com/myuser/my-packages"}
]

El orden importa — las entradas posteriores prevalecen sobre las anteriores cuando los nombres de paquete chocan. Para repositorios privados, mete las credenciales en la URL. Reinicia el servicio o pulsa Actualizar en la interfaz para que los cambios surtan efecto.

Reglas de validación

Durante la validación y la compilación del paquete se aplican estas restricciones:

CampoRegla
URL de la imagenNo vacía; los caracteres deben cumplir ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$
Tipo de imagenVacío (por omisión oci) u oci
Claves de entornoDeben cumplir ^[a-zA-Z_][a-zA-Z0-9_]*$ (convención POSIX)
Nombres de preguntaDeben cumplir ^[a-zA-Z0-9]+$
Nombres de volumenDeben cumplir ^[a-zA-Z0-9][a-zA-Z0-9._-]*$
Puntos de montajeDeben empezar con /
Nombres de plantillaLas mismas reglas que los nombres de volumen
Rutas de plantillaNo vacías, relativas (sin / inicial), sin recorridos ..
Directorio del fichero comprimidoDebe ser una ruta absoluta
Volumen de fichero/gitDebe referirse a un volumen definido en el paquete
URL de gitDeben tener un esquema y un host válidos (salvo file://)
CPU de la máquina virtualDeben ser no negativas
app_directory de ProtonDebe ser una ruta absoluta
Entorno de ejecuciónExactamente uno entre image/proton (contenedor) o vm

La validación ocurre antes de sustituir las plantillas. Los campos que contienen marcadores @variable@ se saltan la validación de rutas y URL hasta que la compilación resuelve las plantillas.

Guía de estilo

  • Omite los mapas vacíos (environment: , internal: , volumes: ) — déjalos fuera por completo.
  • Omite type en las preguntas que aceptan texto libre — no escribas type: sin valor.
  • Incluye description con un resumen breve de qué es el paquete.
  • Incluye supplies con las etiquetas de capacidad que correspondan cuando el paquete ofrezca un servicio conocido.
  • Incluye notes con las URL de conexión y cualquier información importante posterior a la instalación.
  • Usa plantillas @variable@ para que el usuario personalice puertos, nombres de host y credenciales durante la instalación.
  • Usa templates para los ficheros de configuración que necesiten lógica de plantillas de Go — solo se escriben una vez y conservan las ediciones del usuario.