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
| Campo | Descripción |
|---|---|
image | Obligatorio (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. |
description | Resumen breve y legible del paquete. |
supplies | Lista de etiquetas semánticas de las capacidades que ofrece el paquete (por ejemplo ["database"], ["http"]). |
command | Comando opcional que sustituye al del contenedor. Mutuamente excluyente con proton. |
environment | Variables 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@. |
network | Configuración de asignación de puertos y de dominios. |
volumes | Volúmenes con nombre y su configuración de montaje. |
questions | Preguntas interactivas que se muestran durante la instalación. Los nombres deben ser alfanuméricos (^[a-zA-Z0-9]+$). |
archives | Especificaciones de extracción para rellenar volúmenes desde imágenes de contenedor. |
git_sources | Repositorios git que se clonan dentro de los volúmenes. |
templates | Plantillas de fichero que se generan con text/template de Go y se escriben en los volúmenes al instalar. |
notes | Datos de clave y valor que se muestran después de instalar. Admiten sustitución de plantillas. |
vm | Configuración de máquina virtual. Mutuamente excluyente con image y proton. |
proton | Configuració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:
| Entrada | Se normaliza a |
|---|---|
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 |
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:
| Etiqueta | Se usa para | Ejemplos |
|---|---|---|
http | Servidores web, plataformas CMS, aplicaciones web | nginx, wordpress, gitea |
database | Bases de datos relacionales y NoSQL | postgres, mysql, mongo |
cache | Cachés y almacenes de clave y valor | redis, memcached, valkey |
search | Motores de búsqueda y analítica | elasticsearch, opensearch, solr |
messaging | Intermediarios de mensajes y colas | rabbitmq, nats, kafka |
monitoring | Métricas, alertas y visualización | prometheus, grafana, telegraf |
storage | Almacenamiento de objetos y alojamiento de ficheros | minio, registry, nextcloud |
Configuración de red
| Campo | Descripción |
|---|---|
network.external | Asignaciones 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.internal | Asignaciones 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.domains | Lista 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 bajoexternal. -
La entrada presenta un certificado hoja de confianza local (emitido por
la CA integrada de Rolodex) para
<PACKAGE_DNS>(por ejemplogitea.default.home), y Rolodex publica registros DANETLSAen_443para 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, elGITEA__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
| Campo | Descripción |
|---|---|
mountpoint | Obligatorio. Ruta absoluta dentro del contenedor donde se monta el volumen (debe empezar con /). |
quota | Límite de tamaño opcional (por ejemplo 512mb, 2gb, 1tb). Admite los sufijos mb, gb y tb. Puede llevar plantillas @variable@. |
archive | Nombre opcional de fichero comprimido con el que rellenar el volumen de antemano. |
git | URL opcional de un repositorio git que se clona dentro del volumen. |
uid | ID numérico de usuario opcional para el propietario del volumen. |
gid | ID 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 | Campo | Descripción |
|---|---|
image | Obligatorio. Imagen de contenedor de la que se extraen los ficheros. |
directory | Obligatorio. Ruta absoluta dentro de la imagen que se va a extraer. |
volume | Obligatorio. 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 | Campo | Descripción |
|---|---|
url | Obligatorio. URL del repositorio git (http, https o ssh). Puede llevar plantillas @variable@. |
branch | Rama que se va a clonar. Puede llevar plantillas @variable@. |
volume | Obligatorio. 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" | Campo | Descripción |
|---|---|
query | Obligatorio. El texto del mensaje que ve el usuario. |
type | Tipo de validación opcional. Omítelo para texto libre. |
default | Valor predeterminado opcional que se le sugiere al usuario. |
optional | Ponlo en true para permitir dejar la pregunta en blanco. Cualquier otra pregunta debe responderse con un valor no vacío. |
oauth | Obligatorio 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_if | Nombra 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
| Tipo | Qué valida |
|---|---|
hostname | Una 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. |
port | Entero entre 1 y 65535. Genera solo un puerto libre al azar en el rango 10000-60000 cuando está vacío o puesto en "auto". |
bytes | Entero o número con sufijo tb, gb o mb (sin distinguir mayúsculas) |
volume | Caracteres alfanuméricos, guiones y guiones bajos (patrón: ^[a-zA-Z0-9-_]+$) |
archive | Cualquier cadena no vacía |
secret | Genera 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. |
duration | Entero o número con sufijo d, h, m o s (sin distinguir mayúsculas). Se convierte a segundos. |
boolean | true, 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. |
oauth | Un 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 | Campo | Descripción |
|---|---|
start | Obligatorio. La petición que abre el flujo: method (por omisión GET), url, y opcionalmente headers y un cuerpo form. |
extract | Campos JSON que se sacan de la respuesta inicial y quedan disponibles para las plantillas de abajo, escritos como nombre: campo_json. |
approve | Obligatorio. 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_code | Plantilla opcional para un código corto que el operador debe escribir en la página de aprobación. GitHub muestra uno; Plex no. |
poll | Obligatorio. La petición que se repite hasta la aprobación. Con la misma forma que start. |
token | Obligatorio. 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”. |
interval | Cada cuánto sondear. Por omisión 5s; respeta el límite de tasa documentado del proveedor. |
timeout | Cuá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 }} | Campo | Descripción |
|---|---|
volume | Obligatorio. Nombre de un volumen definido en este paquete. |
path | Obligatorio. Ruta relativa dentro del volumen. No debe empezar con / ni contener ... |
content | Obligatorio. 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ón | Descripción |
|---|---|
.Responses.<nombre> | La respuesta del usuario a la pregunta indicada |
.Package.Name | Nombre del paquete |
.Package.Version | Versión del paquete |
.Package.Repo | Nombre del repositorio |
.Package.Image | Referencia compilada de la imagen de contenedor |
.Package.Description | Descripción del paquete |
.System.Hostname | Nombre de host del sistema |
.System.ExternalIP | Dirección IP externa (si se conoce) |
.System.InternalIP | Direcció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" | Campo | Descripción |
|---|---|
value | Obligatorio. El texto de la nota. Admite sustitución de plantillas @variable@. |
type | Tipo 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" | Campo | Descripción |
|---|---|
package | Obligatorio. Nombre del paquete de la dependencia que se va a instalar. |
repo | Repositorio que contiene la dependencia. Por omisión, el del paquete padre. |
version | Versión que se va a instalar. Por omisión, la más reciente disponible. |
responses | Respuestas 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
| Variable | Descripció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 | Campo | Descripción |
|---|---|
image | Obligatorio. URL (http/https) o nombre de fichero de la imagen de disco de la máquina virtual. Puede llevar plantillas @variable@. |
memory | Memoria asignada con sufijos de bytes (por ejemplo 1gb, 512mb). Por omisión 1gb. Puede llevar plantillas @variable@. |
cpus | Nú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 | Campo | Descripción |
|---|---|
app_image | Obligatorio. Imagen de contenedor que contiene la aplicación de Windows. |
app_directory | Obligatorio. Ruta absoluta donde se encuentra la aplicación. |
volume | Obligatorio. Nombre de un volumen definido en este paquete. |
exe | Obligatorio. Ruta al ejecutable de Windows. |
args | Lista 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
lastsi 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 paqueteimport_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
Paquetes destacados
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:
| Campo | Regla |
|---|---|
| URL de la imagen | No vacía; los caracteres deben cumplir ^[a-zA-Z0-9@][a-zA-Z0-9._:/@-]*$ |
| Tipo de imagen | Vacío (por omisión oci) u oci |
| Claves de entorno | Deben cumplir ^[a-zA-Z_][a-zA-Z0-9_]*$ (convención POSIX) |
| Nombres de pregunta | Deben cumplir ^[a-zA-Z0-9]+$ |
| Nombres de volumen | Deben cumplir ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ |
| Puntos de montaje | Deben empezar con / |
| Nombres de plantilla | Las mismas reglas que los nombres de volumen |
| Rutas de plantilla | No vacías, relativas (sin / inicial), sin recorridos .. |
| Directorio del fichero comprimido | Debe ser una ruta absoluta |
| Volumen de fichero/git | Debe referirse a un volumen definido en el paquete |
| URL de git | Deben tener un esquema y un host válidos (salvo file://) |
| CPU de la máquina virtual | Deben ser no negativas |
| app_directory de Proton | Debe ser una ruta absoluta |
| Entorno de ejecución | Exactamente 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
typeen las preguntas que aceptan texto libre — no escribastype:sin valor. - Incluye
descriptioncon un resumen breve de qué es el paquete. - Incluye
suppliescon las etiquetas de capacidad que correspondan cuando el paquete ofrezca un servicio conocido. - Incluye
notescon 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
templatespara los ficheros de configuración que necesiten lógica de plantillas de Go — solo se escriben una vez y conservan las ediciones del usuario.