- Railway lanzó Railpack, un nuevo sistema de compilación que reemplaza a Nixpacks
- Railpack ofrece mejoras frente a Nixpacks en granularidad del versionado, imágenes más pequeñas y mejor caché
- El modelo de versionado basado en commits de Nixpacks mostró limitaciones para distintas necesidades de usuarios y de escalabilidad
- Railpack mejora la estabilidad y flexibilidad del entorno de compilación con integración con BuildKit, protección de variables de entorno secretas y compatibilidad con varios lenguajes y frameworks
- Actualmente es compatible con Node, Python, Go, PHP y HTML estático, y sigue ampliando el soporte para frameworks y lenguajes
Resumen y contexto
- Railway presentó Railpack, su sistema de compilación de próxima generación
- Railpack es una herramienta nueva desarrollada a partir de la experiencia obtenida al compilar más de 14 millones de apps con Nixpacks en la plataforma Railway
- Nixpacks funcionaba bien para el 80% de los usuarios, pero más de 200 mil se toparon con limitaciones que les generaron fricción
- Consideraron necesario un gran upgrade para escalar la base de usuarios y mantener un entorno de compilación sostenible
Principales mejoras de Railpack
- Granularidad del versionado: permite especificar versiones detalladas en formato
major.minor.patch para cada paquete, superando las limitaciones del esquema poco claro de versiones en Nix
- Imágenes más pequeñas: reduce el tamaño base de las imágenes de compilación hasta en 38% para Node y 77% para Python, ofreciendo despliegues más rápidos
- Mejor caché: se integra directamente con BuildKit para controlar capas y sistema de archivos, mejorar el acierto de caché y compartir caché entre entornos
- Las compilaciones con Railpack ya se están usando en railway.com y en servicios centrales
Problemas al usar Nixpacks
- El sistema de versionado de paquetes de Nix tiene una estructura basada en commits, ofrece solo la versión major más reciente y cada versión corresponde a un commit específico del repositorio nixpkgs
- Existe la ineficiencia de tener que gestionar manualmente incluso versiones pequeñas de parche, y para quienes contribuyen el versionado tampoco resulta intuitivo, lo que reduce la accesibilidad
- Incluso en lenguajes como Node o Python, al final solo se admite la versión major más reciente
- Al actualizar una versión, cambiar el hash del commit afecta al mismo tiempo otras versiones de paquetes, lo que reduce la confiabilidad para el usuario y puede provocar fallas inesperadas de compilación
- En Nixpacks, todas las dependencias están incluidas en una sola capa de
/nix/store, por lo que es difícil dividir la imagen de forma efectiva o reducir su tamaño
- La caché tampoco se aprovecha bien, porque cada vez que se inyectan variables de entorno la capa siempre se invalida
No es un problema de Nix en sí, sino una limitación en la forma de usarlo
- El problema no está en el diseño de Nix en sí, sino en la forma en que Railway lo usaba y abstraía
- Intentaron diseñarlo para que el usuario no tuviera que entender el concepto de derivation de Nix ni su estructura interna de versiones, pero concluyeron que en la práctica eso no era posible
- Para resolver estos problemas avanzaron con el desarrollo de Railpack
Arquitectura técnica de Railpack
- Cambio de base de código de Rust a Go: se pasó a Go para aprovechar BuildKit y fortalecer la capacidad de adaptación al ecosistema
- LLB y frontend de BuildKit: generan directamente un LLB y frontend personalizados de BuildKit para controlar con precisión la estructura de las imágenes de compilación → las imágenes base de Node y Python quedaron mucho más ligeras que con Nixpacks
- Versionado con Mise: usan Mise para la instalación de paquetes y la resolución de versiones, lo que también facilita admitir otras fuentes de ejecutables en el futuro
- Si una compilación se completa con éxito, se aplica un lock-in de dependencias en ese momento → aunque la versión base de Node cambie de 22 a 24, la compilación existente no se rompe
- Aprovechan la función de secretos de BuildKit para mejorar la seguridad y administración de variables de entorno
Etapas de compilación de Railpack
- Analyze: analiza el código para determinar los paquetes necesarios, comandos de ejecución y comando de inicio
- Plan: genera un plan de compilación en un formato serializable a JSON (incluye varias etapas, y cada etapa depende del resultado de la anterior o de la imagen completa)
- Generates: genera el grafo de compilación de BuildKit (con base en entradas y salidas)
Compilación estratégica con BuildKit
- Mientras que Dockerfile funciona en serie, BuildKit procesa varios comandos en paralelo y permite un control fino de entradas y salidas por etapa
- Railpack define todas las etapas de compilación a partir del análisis del código y especifica con detalle de bajo nivel las dependencias entre etapas
- Luego convierte ese plan en un grafo LLB de BuildKit y lo resuelve
- Cuando cambian variables de entorno u otros valores, monta un archivo con el hash de ese valor; si no hay cambios en el código ni en las variables, se garantiza el acierto de caché
- Como resultado, Railpack puede controlar por completo la forma en que se generan las imágenes
Nuevas funciones posibles con Railpack
- Compatibilidad sin configuración para compilar y desplegar sitios estáticos con Vite, Astro, CRA y Angular
- Integración estrecha del proceso de compilación con la UI de Railway
- Soporte para versiones más recientes de los lenguajes sin necesidad de una nueva release de Railpack
- Optimización de caché entre entornos por proyecto
- Actualmente es compatible con Node, Python, Go, PHP y HTML estático, y sigue ampliando el soporte para frameworks y lenguajes
Código abierto y planes futuros
- Railpack se publica en estado Beta y puede usarse de inmediato con solo activarlo
- En railpack.com están disponibles la documentación oficial, el código real y canales públicos de soporte
- A futuro, planean priorizar soporte profundo para lenguajes ampliamente usados y luego ampliar el alcance una vez establecidos la API central y el nivel de abstracción
1 comentarios
Comentarios en Hacker News
Soy fan de Nix, pero espero que crean que no estoy emocionalmente aferrado a la decisión de no usar Nix. Aun así, hay varias quejas de este texto que no termino de entender y siento que necesitan más explicación. Por ejemplo, eso de que “el mayor problema de Nix es el versionado de paquetes basado en commits”. Nixpkgs es un recurso excelente, pero Nix y Nixpkgs no son lo mismo. Si quieres traer una versión arbitraria de un toolchain, Nixpkgs sí es bastante poco adecuado, pero con Nix hay otras formas. Por ejemplo, existen herramientas de Nix realmente bien hechas para obtener versiones arbitrarias de Rust. También escuché eso de que “no se pueden dividir las dependencias de Nix en capas separadas”, y sinceramente me parece que no tiene sentido. Se puede dividir de la forma que quieras. Las herramientas de Docker de Nixpkgs también lo soportan. La parte donde movieron el codebase de Rust a Go no está directamente relacionada con Nix, pero me pareció interesante. Normalmente uno no decide cambiar de lenguaje a la ligera; suele pasar cuando ya planeabas rehacerlo desde cero. Sospecho que Railpacks y Nixpacks no fueron trabajo de las mismas personas. También he visto qué pasa cuando gente que no conoce bien Nix termina lidiando en una organización con una solución de Nix que ni siquiera estaba terminada. No se ve nada bien, y la mayoría de la gente no intenta aprender Nix. Por eso en mi trabajo original casi no usamos Nix, para evitar justo esa situación
Me gusta aprovechar Nix, pero cada vez que se discuten problemas básicos de uso de Nix, siempre me responden con “hay una forma de rodearlo” (pero con documentación deficiente, un lenguaje raro, malos mensajes de error y decenas o cientos de líneas de código extra basadas en conocimiento escaso que solo tiene la gente que ya lo usó), y eso ya me tiene harto. La mayoría de los problemas relacionados con Nix no vienen de que sea Turing completo, sino de que carece de cosas básicas incluidas, como APIs intuitivas. Si en todos los proyectos usar Nix termina convirtiéndose poco a poco en una obsesión por resolver los propios problemas de Nix, no hay razón para usarlo cuando ya existen herramientas mainstream bien documentadas. De hecho, por eso la mayoría de la gente termina eligiendo Docker. Me decepciona mucho que Nix insista en una pureza ideal en lugar de resolver problemas reales de experiencia de desarrollador en tiempos razonables. Claro, todo el mundo contribuye de forma voluntaria, pero da mucha pena ver que todo ese esfuerzo técnico termina siendo prácticamente inutilizable por un UX mal diseñado
Yo no uso Nix, pero esa afirmación de “Nix ≠ Nixpkgs” me suena desconectada de la realidad. Para la gran mayoría de los usuarios, si la alternativa exige investigación y esfuerzo extra, entonces Nixpkgs termina siendo Nix en la práctica. Y sobre “se puede dividir en capas separadas”, me pregunto si eso realmente es intuitivo, simple y el comportamiento por defecto
Lo importante es que los usuarios de Railway son desarrolladores que quieren especificar la versión exacta de los paquetes que quieren. Por la estructura de Nix y Nixpkgs, fijar la versión de un paquete significa fijar el commit de todo el árbol de nixpkgs. Como muchos builds de paquetes de node/python/ruby dependen de cosas fuera del árbol, terminas necesitando un mapeo entre versión y commit. Esa abstracción no es perfecta, así que incluso si un usuario solo quiere hacer
yarn add paquete, podría terminar teniendo que alinear el estado del árbol. Usar solo Nix sin Nixpkgs está bien para usos limitados, pero para una plataforma como Railway es una decisión difícilNo entiendo bien la controversia sobre el versionado. Apenas estoy empezando con Nix, pero claramente sí tengo paquetes traídos desde commits específicos
Creo que lo explicaste muy bien. Nixpkgs y Nix son distintos, pero en la práctica Nixpkgs es la ventaja real. Usando NixOS fue la primera vez que pude usar una versión nueva del kernel de Linux el mismo día de su lanzamiento. Debian Stable está bien, pero siempre se siente como volver varios años al pasado. Dicho eso, el lenguaje de Nix tiene muchas cosas criticables. Es un lenguaje viejo, y aunque es el mejor resultado que pudieron sacar, no creo que valga la pena cambiarlo. El sistema de build de Nix me parece clásico hasta el punto de provocar demasiados rebuilds innecesarios. Por ejemplo, si en el ISO de instalación de NixOS cambias una sola línea del command line que se pasa al kernel (por ejemplo, la velocidad del puerto de consola), ocurre el fenómeno extraño de un build que tarda como 3 minutos. Es chistoso, pero no por eso voy a abandonar Nix. Aun así, es algo que jamás permitiría en mi propio sistema de builds. Personalmente, me parece terrible usar Nix para construir imágenes Docker. Una vez quise meter solo el binario
pg_dumpde Postgres en un binario hecho en Go, y como el equipo de infraestructura me recomendó Nix, lo usé; el binario comprimido de Go pesaba 50 MB, pero la imagen terminó siendo un monstruo de 1.5 GB.pg_dumpapenas pesa 464 KB. Al final lo resolví mucho mejor con una combinación de Bazel, rules_debian y distroless. La mayoría de los sistemas con Nix se sienten como si 1.4 GB fuera el valor por defecto. Nix tampoco es especialmente bueno construyendo grandes proyectos de C++. De hecho, los sistemas diseñados para compilar tu propio software suelen ajustarse mejor a las necesidades concretas. A mí me gusta Bazel, y para proyectos Go simplemente quiero usargo build. En el 99% de los casos uso esas herramientas en lugar de Nix, aunque sí podría escribir un flake para actualización o despliegue y usarlo con home-managerLa selección de versiones se siente rara. La versión de nixpkgs claramente tiene sentido cuando operas o construyes un sistema. Pero si eres una plataforma que provee runtimes/compiladores, necesitas ofrecer las versiones directamente, como hace devenv. Por ejemplo, nixpkgs-python ofrece “todas las versiones de Python, actualizadas cada hora con Nix”. El hecho de que Railway inyecte una variable de entorno con el ID de despliegue en todos los builds también podría haberse hecho en una capa posterior a la instalación. Los paquetes también se pueden dividir en varias capas, y hasta se puede automatizar el ajuste de cuántas capas usar
Como alguien con experiencia en DevOps/SRE, he visto que cuando alguien intenta construir un sistema de gestión de dependencias, normalmente termina yéndose por una de dos direcciones (por ejemplo, en Python). Opción 1: “monorepo + entorno compartido”; ventajas: administración fácil, parches de seguridad simples, centralización. Desventajas: siempre habrá alguien que quiere una versión especial, es difícil hacer rollouts graduales y hay problemas para construir imágenes slim. Opción 2: “cada quien con su conda/venv”; ventajas: personalización individual, exclusión de paquetes innecesarios, upgrades graduales. Desventajas: demasiados entornos, compatibilidad mutua sin validar, y la seguridad se vuelve una pesadilla. Al final, con los años de experiencia se vuelve cada vez más real esa frase de que “no hay soluciones, solo trade-offs”
Creo que eso de “Nix en sí no tenía problema. El problema fue cómo se usó” es un buen ejemplo de “usa la herramienta adecuada para el trabajo adecuado”. Nix es excelente en algunos casos, pero en otros es de lo peor. El problema es que aprenderlo toma mucho tiempo, así que para cuando ya sabes lo suficiente como para tomar una decisión, ya te pesa la inversión de tiempo y te cuesta cambiar de rumbo, y al final terminas forzando Nix para seguir usándolo en el objetivo original
shell.nixoconfiguration.nixde acuerdo con una especificación también se beneficia de esa estructura. Yo también a veces creo entornos por repositorio completamente encapsulados, y con flakes probablemente se puedan hacer entornos aún más reproducibles. (flake.nixes parecido ashell.nix, pero también soporta fijar versiones...)Parece que están intentando introducir versiones a la fuerza donde no las hay. ¿Que una “versión por defecto” rompe dependencias? Eso se parece a usar la etiqueta
:latestde Docker y que el servidor se rompa cada vez que cambia. No entiendo bien el contenido de ese blog. Tampoco coincido con eso de que “no se pueden separar las dependencias de Nix en capas distintas”. Puedes dividir/nix/storetanto como quieras, y da la impresión de que tampoco entienden bien cómo usar contenedores con Nix. Si el nivel técnico es tan bajo, me parece que la alternativa que proponen solo va a repetir los mismos problemas. Es un ejemplo clásico del síndrome NIH (hacer tu propia herramienta)Claro, no usar Nix donde no encaja es totalmente válido, pero me parece fundamentalmente raro reconstruir todo un sistema de punta a punta cuando son problemas que otras personas ya resolvieron y que podrías descubrir investigando un poco.
nix2containero flakes probablemente habrían resuelto todos esos problemas. Incluso con el versionado: flakes que escribí hace 3 años todavía construyen igual hoy y producen el mismo resultado. También me deja cierta impresión de que esto huele a un cambio de plataforma para salir al mercado o atraer inversión. Por cierto, revisé el GitHub de nixpacks y solo usanrustPlatform; si el problema era Rust, entonces rust-overlay es prácticamente la respuesta correctaSi lo piensas desde el ángulo de qué método ayuda más a conseguir capital de riesgo, el título de “plataforma de despliegue” vende mejor que un wrapper de Nix
Contrario a eso de que “no se pueden separar las dependencias de Nix en capas distintas”, nix2container justamente permite esa separación. Por ejemplo, si necesitas una imagen con bash, puedes construir por separado una capa que incluya bash, y esa capa solo se rebuild/pushea cuando cambia bash. Eso de que “por las dependencias se genera una imagen gigante en una sola capa de
/nix/store” sí aplica a la funciónnixpkgs.dockerTools.buildImage, pero no anix2containerni anixpkgs.dockerTools.streamLayeredImage. En la práctica, esta herramienta genera un script y a través de él hace el push de la imagen.nix2containerconvierte las rutas de todas las capas a JSON y usa Skopeo para hacer push de la imagen a Docker, registries, podman, etc. (Por cierto, yo soy el autor de nix2container)De verdad quiero agradecerte por nix2container. Lo usamos para despliegues en AWS (ECR), y el tiempo de cambio entre builds bajó a segundos de un solo dígito
Nosotros también pensábamos probar nix2container por el tema del tamaño de las imágenes Docker. Gracias por crear una herramienta tan buena
Creo que el problema central aquí es la insistencia en mantener esa “sopa de versiones personalizadas” fomentada por los package managers de lenguaje (y ese enfoque no es sostenible). La alternativa, Mise, no entiende las restricciones de versión entre paquetes ni prueba absolutamente nada entre ellos. No puedes esperar ni remotamente el mismo nivel de confiabilidad
Es cierto que esa sopa de versiones personalizadas no es sostenible, pero la razón por la que la gente la sigue usando es que funciona bastante bien. Las librerías a nivel OS se gestionan de forma muy conservadora y no se rompen fácilmente, y sobre eso puedes montar combinaciones de versiones personalizadas con herramientas como mise o asdf y, por lo general, todo sigue funcionando. Si algo se rompe, normalmente se arregla de inmediato tocando la versión o la configuración. Que se rompa es molesto, sí, pero no suele ser importante. Cualquier sistema que implique aprendizaje o esfuerzo adicional se percibe como pérdida de tiempo. En cambio, la gente que valora más “que no se rompa” sí tiende a preferir Nix, incluso si tiene una curva de aprendizaje pesada y resulta incómodo. Para un servicio como Railway, que apunta a muchos usuarios, al final es lógico que prioricen más al primer grupo (facilidad e inercia)
Me pregunto qué significa exactamente “sopa de versiones personalizadas” y cuál sería la alternativa
Ambas cosas son totalmente posibles. Por ejemplo, los paquetes de Rust se pueden construir fácilmente con Nix usando la información de
Cargo.lock. Nixpkgs sí choca con combinaciones personalizadas de versiones, pero Nix por sí mismo lo hace bastante bienNix no garantiza versiones arbitrarias, sino garantías a nivel de commit. Puede hacerte sufrir en casos límite como cambios de glibc o conflictos de librerías compartidas. Quizá ya sea demasiado tarde ahora, pero incluso podría dar consultoría sobre formas más elegantes de usar Nix. El producto en sí me parece muy bueno
Nix evita con muchísima fuerza los conflictos de librerías compartidas. Pero también hace que hasta cambios triviales (comentarios, documentación, etc.) provoquen rebuild completo de todas las dependencias aguas abajo relacionadas. El resultado es que puedes terminar necesitando rebuilds enormes, y eso puede volver doloroso el desarrollo. Se ve claramente en el proceso de staging de nixpkgs
Entiendo perfectamente el valor de Nix. Solo creo que decir que “todo se rompe” es un poco exagerado. Sí, pierdes algunas garantías grandes frente a Nix, pero aun así probablemente funcione mucho mejor que la mayoría del software
No entiendo por qué dependieron del hash de nixpkgs en lugar de crear sus propias derivations
Me pareció interesante que muchos comentarios tenían ese tono de “en realidad Nix sí resuelve todo, pero solo si eres un experto como yo”
Si una empresa hiciera toda su tecnología y su negocio en JavaScript, y luego por no entender conceptos centrales ya existentes (funciones, arreglos, etc.) terminara cayendo en NIH (crear un lenguaje nuevo con especificación propia), eso se parecería más a una carencia interna
Ese es el ambiente de siempre cada vez que sale el tema de Nix
Justamente esa es la vibra de Nix. La narrativa típica de “yo voy a salvar el mundo” y, cuando alguien responde “la función que necesito no existe”, siempre le contestan “es que no lo estás usando bien”