Back to sh0
sh0

El Deploy Hub: 183 opciones, una pagina

Como construimos un hub de despliegue estilo Softaculous con 183 opciones en 5 categorias, 7 componentes de formulario y una UX de panel dividido que hace que desplegar cualquier cosa se sienta como un clic.

Thales & Claude | March 30, 2026 11 min sh0
EN/ FR/ ES
deployuxdashboardsveltetemplatespaassoftaculous

La parte mas dificil del software auto-hospedado es el primer despliegue. No porque la tecnologia sea dificil -- Docker ha hecho eso casi trivial -- sino porque la UI te hace sentir que necesitas saber Docker antes de poder usarla.

Coolify te pide que elijas un "tipo de recurso" de un dropdown. Easypanel muestra un formulario con campos como "imagen Docker" y "mapeo de puertos". CapRover quiere un archivo tar. Para desarrolladores que viven en Docker, esto esta bien. Para el desarrollador que solo quiere ejecutar WordPress, o la agencia que necesita levantar un sitio Next.js para un cliente, es un muro.

Queriamos algo diferente. Queriamos la experiencia de Softaculous -- el panel de instalacion en un clic que viene con cPanel desde hace mas de una decada. Abrir una pagina, ver todo lo que puedes desplegar, hacer clic en lo que quieres, llenar un nombre y listo.

El 15 de marzo de 2026, construimos esa pagina. Ciento ochenta y tres opciones de despliegue. Cinco categorias. Siete componentes de formulario especializados. Un layout de panel dividido que muestra el catalogo a la izquierda y el formulario de despliegue a la derecha. Y funciona.

La base es un archivo TypeScript llamado deploy-catalog.ts que define cada opcion desplegable como un objeto estructurado:

typescriptexport interface DeployOption {
  id: string;
  name: string;
  icon: string;        // Nombre de icono Lucide
  category: DeployCategory;
  group: string;       // Sub-categoria
  formType: DeployFormType;
  description: string;
  popular?: boolean;
  defaultPort?: number;
  defaultBuildCmd?: string;
  defaultStartCmd?: string;
}

export type DeployCategory = 'source' | 'framework' | 'database' | 'app';
export type DeployFormType = 'git' | 'upload' | 'docker-image' | 'dockerfile'
                           | 'compose' | 'service' | 'framework';

Las 183 opciones se desglosan asi:

CategoriaCantidadEjemplos
Tipos de fuente6Repo Git, Subir ZIP, Imagen Docker, Dockerfile, Compose, Docker Hub
Frameworks6310 estaticos, 11 PHP, 15 JS/TS, 8 Python, 5 Go, 4 Rust, 4 Java, 3 .NET, 2 Ruby, 1 Elixir
Bases de datos27PostgreSQL, MySQL, MongoDB, Redis, MariaDB, Cassandra, ClickHouse, etc.
Apps87WordPress, Ghost, Discourse, Gitea, Minio, Grafana, etc.

Cada opcion especifica su formType, que determina que componente de formulario de despliegue se renderiza cuando el usuario la selecciona. Una base de datos PostgreSQL usa FormService.svelte (un clic, nombre auto-generado). Una app Next.js usa FormFramework.svelte (URL Git, comandos de build e inicio pre-llenados). Una imagen Docker cruda usa FormDockerImage.svelte (nombre de imagen, puerto, variables de entorno).

El catalogo es datos puros -- sin componentes, sin efectos secundarios. Se importa en la pagina de despliegue y se filtra del lado del cliente. Esto significa que la busqueda, el filtrado por categoria y la navegacion por sub-grupo son todos instantaneos. Sin llamadas API, sin spinners de carga.

El layout de la pagina

La pagina de despliegue usa un diseno de panel dividido inspirado en clientes de correo y paneles de configuracion de IDE:

Panel izquierdo (60% en escritorio, ancho completo en movil): La cuadricula del catalogo. Arriba, una barra de busqueda de ancho completo con debounce de 150ms, autofoco y conteo de resultados en vivo. Debajo, pestanas de categoria (Todas, Tipos de fuente, Frameworks, Bases de datos, Apps) con insignias de conteo. Debajo de las pestanas, las pastillas de sub-grupo aparecen cuando se selecciona una categoria -- por ejemplo, seleccionar "Frameworks" muestra pastillas para JavaScript, Python, PHP, Go, Rust, Java, .NET, Ruby, Elixir. Debajo de las pastillas, la cuadricula de tarjetas de despliegue, agrupadas por sub-categoria con encabezados de seccion.

Panel derecho (40% en escritorio, apilado debajo en movil): El formulario de despliegue. Aparece cuando se selecciona una tarjeta. Muestra el icono y nombre de la opcion seleccionada arriba, un selector de stack obligatorio, y el componente de formulario apropiado debajo.

Cuando no hay opcion seleccionada, el panel derecho muestra una seccion destacada con seis opciones populares: Repo Git, Subir ZIP, WordPress, Next.js, PostgreSQL e Imagen Docker. Estos son los atajos de "solo quiero empezar".

svelte<div class="flex flex-col lg:flex-row gap-6">
  <!-- Izquierda: Catalogo -->
  <div class="lg:w-3/5" class:opacity-40={selectedOption}>
    <SearchBar bind:query onchange={handleSearch} resultCount={filtered.length} />
    <CategoryNav {categories} bind:active={activeCategory} />
    {#if activeGroup}
      <div class="flex flex-wrap gap-2 my-3">
        {#each groups as group}
          <button class="px-3 py-1 rounded-full text-xs"
                  class:bg-[var(--accent)]={activeGroup === group.id}
                  onclick={() => activeGroup = group.id}>
            {group.label}
          </button>
        {/each}
      </div>
    {/if}
    <DeployGrid options={filtered} {selectedOption} onselect={handleSelect} />
  </div>

  <!-- Derecha: Formulario -->
  <div class="lg:w-2/5">
    {#if selectedOption}
      <DeployForm option={selectedOption} {preSelectedStackId} />
    {:else}
      <FeaturedSection onselect={handleSelect} />
    {/if}
  </div>
</div>

El panel izquierdo se atenua al 40% de opacidad cuando se selecciona una opcion, manteniendo el catalogo visible pero dirigiendo la atencion al formulario. Esta es una senal visual sutil pero efectiva: "has elegido algo, ahora enfocate aqui".

Los siete componentes de formulario

Cada metodo de despliegue tiene su propio componente de formulario, adaptado a las entradas especificas de ese metodo:

FormGit.svelte -- El formulario mas comun. Campos: nombre de app, URL del repositorio Git, rama (por defecto: main), puerto, y una seccion expandible de variables de entorno. La URL Git se valida del lado del cliente para detectar errores obvios antes de llegar a la API.

FormUpload.svelte -- Subida de archivos con arrastrar y soltar para archivos .zip y .tar.gz. Usa la API de archivos HTML5 con una zona de soltar que se resalta al arrastrar encima. Muestra el nombre y tamano del archivo seleccionado antes del envio.

FormDockerImage.svelte -- Para descargar imagenes pre-construidas. Campos: nombre de app, imagen Docker (p. ej., nginx:latest), puerto y variables de entorno. El nombre de imagen soporta tanto la abreviatura de Docker Hub (nginx) como rutas completas de registro (ghcr.io/org/image:tag).

FormDockerfile.svelte -- Un area de texto para pegar un Dockerfile crudo. El backend construira la imagen y ejecutara el contenedor resultante. Util para configuraciones personalizadas que no encajan en ningun template.

FormCompose.svelte -- Un area de texto para pegar un archivo docker-compose.yml, con un boton "Validar" que verifica la sintaxis YAML antes del despliegue. Este formulario crea multiples servicios a la vez, cada uno rastreado como una app separada dentro del stack.

FormService.svelte -- El formulario de un clic. Usado para bases de datos y apps pre-configuradas como WordPress o Gitea. El nombre se auto-genera (p. ej., postgres-7f3a), la imagen y configuracion estan pre-establecidas por la entrada del catalogo. El usuario literalmente hace clic en "Desplegar" y espera.

FormFramework.svelte -- Un formulario de despliegue Git especializado para frameworks. Pre-llena el comando de build (p. ej., npm run build para Next.js, cargo build --release para Rust) y el comando de inicio (p. ej., npm start, ./target/release/app). El usuario proporciona la URL Git y opcionalmente sobreescribe los valores por defecto.

Los siete formularios comparten un patron comun arriba: el selector de stack obligatorio.

El problema del selector de stack

Este fue un bug que detectamos y corregimos durante la misma sesion. El Deploy Hub original no tenia concepto de stacks -- creaba apps sin un project_id, lo que rompia la arquitectura de alcance por stack que acababamos de construir.

La correccion fue un componente StackSelector arriba de cada formulario:

svelte<!-- Dentro de DeployForm.svelte -->
<script lang="ts">
  let stacks = $state<Stack[]>([]);
  let selectedStackId = $state<string | null>(preSelectedStackId);
  let newStackName = $state('');
  let creatingStack = $state(false);

  $effect(() => {
    projectsApi.list().then(data => {
      stacks = data;
      // Auto-seleccionar si solo existe un stack
      if (stacks.length === 1 && !selectedStackId) {
        selectedStackId = stacks[0].id;
      }
    });
  });
</script>

<div class="mb-4">
  <label class="text-sm font-medium">{t('deploy.selectStack')}</label>
  <select bind:value={selectedStackId} class="w-full mt-1 ...">
    <option value={null}>{t('deploy.chooseStack')}</option>
    {#each stacks as stack}
      <option value={stack.id}>{stack.name}</option>
    {/each}
  </select>

  <!-- Crear nuevo stack en linea -->
  {#if creatingStack}
    <div class="flex gap-2 mt-2">
      <input bind:value={newStackName} placeholder="Nombre del stack"
             onkeydown={(e) => e.key === 'Enter' && createStack()} />
      <Button onclick={createStack}>Crear</Button>
    </div>
  {:else}
    <button onclick={() => creatingStack = true}
            class="text-sm text-[var(--accent)] mt-1">
      + {t('deploy.createNewStack')}
    </button>
  {/if}
</div>

Tres comportamientos hacen esto fluido:

  1. Auto-seleccion. Si solo existe un stack, se pre-selecciona. Sin clic extra necesario para el caso comun.
  2. Parametro URL. Navegar desde el boton "Anadir Servicio" de un stack establece ?stack=id, que pre-selecciona ese stack. El usuario nunca tiene que elegir.
  3. Creacion en linea. Si llegas al Deploy Hub sin un stack, puedes crear uno sin salir de la pagina. Escribe un nombre, presiona Enter, y aparece en el dropdown, ya seleccionado.

Cada componente de formulario valida que un stack este seleccionado antes del envio. Sin stack, sin despliegue. Esta restriccion se aplica a nivel de UI y de nuevo a nivel de API (el backend rechaza apps sin project_id).

El problema del mapeo de iconos

El catalogo define 183 opciones, cada una con un nombre de icono como "globe", "database", "docker" o "code". Estos necesitan renderizarse como componentes Lucide Svelte reales. Pero no puedes importar dinamicamente un componente Svelte por nombre de cadena en tiempo de ejecucion.

La solucion fue icon-map.ts: un mapeo estatico de nombres de cadena a componentes Lucide importados.

typescriptimport {
  Globe, Database, Docker, Code, Upload, FileCode,
  Server, Layers, Terminal, Boxes, Package, Rocket,
  // ... 40+ imports mas
} from 'lucide-svelte';

export const iconMap: Record<string, typeof Globe> = {
  globe: Globe,
  database: Database,
  docker: Docker,
  code: Code,
  upload: Upload,
  'file-code': FileCode,
  server: Server,
  layers: Layers,
  terminal: Terminal,
  // ... 40+ entradas mas
};

export function getIcon(name: string) {
  return iconMap[name] || Package; // fallback al icono Package
}

El componente DeployCard.svelte llama a getIcon(option.icon) y lo renderiza con <svelte:component this={icon} />. El tree-shaking elimina cualquier icono Lucide no referenciado en el mapa, por lo que el bundle se mantiene razonable a pesar de importar de una libreria con mas de 1.500 iconos.

La experiencia de busqueda

La busqueda es lo primero que ven los usuarios. La barra de busqueda tiene autofoco al cargar la pagina, y mientras escribes, la cuadricula se filtra en tiempo real. La implementacion es directa -- un filtro con debounce sobre el nombre y descripcion de la opcion:

typescriptlet filtered = $derived(
  catalog.filter(opt =>
    opt.name.toLowerCase().includes(query.toLowerCase()) ||
    opt.description.toLowerCase().includes(query.toLowerCase())
  )
);

El conteo de resultados se actualiza en vivo junto a la barra de busqueda: "183 opciones" se convierte en "12 resultados" mientras escribes "post" (coincidiendo con PostgreSQL, PostgREST, Postal, etc.). Las pestanas de categoria y las pastillas de sub-grupo tambien actualizan sus conteos para reflejar el conjunto filtrado.

Este bucle de retroalimentacion inmediata es lo que hace que el Deploy Hub se sienta rapido. No hay boton "Buscar", no hay estado de carga, no hay ida y vuelta al servidor. Todo el catalogo esta en memoria porque son datos estaticos -- 183 objetos que pesan quizas 30 KB. El filtrado del lado del cliente es la arquitectura correcta aqui.

Reemplazando el modal Anadir Servicio

La existencia del Deploy Hub hizo que el AddServiceModal del rediseno de stacks fuera redundante. El modal tenia tres pestanas (Servicios, Templates, Personalizado) que eran un subconjunto estricto de lo que ofrecia el Deploy Hub. Mantener ambos significaria mantener dos experiencias de despliegue.

Eliminamos el modal completamente. El boton "Anadir Servicio" en la sidebar contextual se convirtio en un enlace: <a href="/deploy?stack={stackId}">. La tarjeta de accion rapida "Nuevo Despliegue" de la pagina de inicio se corrigio de href="/" a href="/deploy".

Una superficie de despliegue. Un conjunto de componentes de formulario. Un lugar para mantener.

La adicion de i18n

El Deploy Hub anadio 42 claves de traduccion a los cinco archivos de locale. Las claves cubren la barra de busqueda, nombres de categorias, etiquetas de formulario, mensajes de validacion, estados de exito/error y la seccion destacada.

Lo que aprendimos

Un catalogo es una decision de producto, no solo una decision de UI. Elegir listar 183 opciones -- en lugar de mostrar un formulario vacio y decir "introduce una imagen Docker" -- es una declaracion sobre quienes son nuestros usuarios. Son desarrolladores que quieren desplegar cosas, no operadores Docker que quieren gestionar contenedores. El catalogo cierra la brecha.

Los layouts de panel dividido funcionan para flujos de seleccion-luego-configuracion. El panel izquierdo es navegar, el panel derecho es actuar. La atenuacion del panel izquierdo cuando se hace una seleccion es un detalle pequeno que reduce la carga cognitiva: "has terminado de navegar, ahora enfocate aqui".

El selector de stack resolvio un problema arquitectonico real. Sin el, las apps se escaparian del modelo de stacks, rompiendo la navegacion y la agrupacion. Al hacer la seleccion de stack obligatoria y fluida, mantuvimos la integridad estructural de la UX.

Los datos estaticos superan a las llamadas API para catalogos. Las 183 opciones nunca cambian en tiempo de ejecucion. Cambian cuando lanzamos una nueva version. Codificarlas como datos TypeScript significa que la busqueda y el filtrado son instantaneos, y la pagina funciona offline.

El Deploy Hub es posiblemente la pagina mas importante del dashboard de sh0. Es la respuesta a "acabo de instalar sh0, ahora que?" Abre el Deploy Hub, elige algo, llena un nombre, despliega. Esa es la experiencia que estabamos construyendo desde el primer crate.


Siguiente en la serie: Terminal web y explorador de archivos en un PaaS auto-hospedado -- como construimos un terminal basado en navegador y un explorador de archivos estilo Docker Desktop, funcionalidades que la mayoria de PaaS auto-hospedados no tienen.

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles