Configuración como código
Describe un proyecto en `.skiffly/skiffly.ts` con `@skiffly/config`, previsualiza el diff con `skiffly config plan` y concílialo con `skiffly config apply`.
El archivo replica el SDK railway/iac de Railway: los builders conservan sus nombres y defineRailway pasa a ser defineSkiffly. La CLI compara el archivo con un entorno y aplica la diferencia a través de la API pública. No hay archivo de estado; Skiffly es la fuente de verdad, así que ejecutar apply dos veces no hace nada.
Preparación#
npm i -g skiffly && skiffly login
pnpm add -D @skiffly/config # opcional: tipos en tu editor (la CLI incluye su propia copia)import { defineSkiffly, github, postgres, project, service } from "@skiffly/config";
export default defineSkiffly(() => {
const db = postgres("db");
const web = service("web", {
source: github("acme/web"),
build: "pnpm build",
start: "pnpm start",
healthcheck: "/health",
env: {
NODE_ENV: "production",
DATABASE_URL: db.env.DATABASE_URL,
},
domain: true,
});
return project("my-app", { resources: [db, web] });
});skiffly.config.ts en la raíz del proyecto, .js, .mjs y .json también funcionan. TypeScript se transpila en memoria; sin paso de build.
Comandos#
skiffly config init # adopta un proyecto existente: escribe el archivo a partir del entorno vinculado
skiffly config validate # evalúa el archivo, sin llamadas a la API
skiffly config plan [--prune] # + service db, ~ env web.DATABASE_URL, - volume /data
skiffly config apply [--yes] [--prune] [--no-deploy]my-app / production
+ service db # will deploy
+ source.image = "postgres:17"
+ port = 5432
+ env.POSTGRES_PASSWORD = <generated password>
+ volume /var/lib/postgresql/data = "10 GB"
~ service web # will deploy
~ env.DATABASE_URL: "postgres://…" → "${{db.DATABASE_URL}}"
+ domain (generated)planyapplyaceptan-p/--project,-e/--environment,-f/--file,--json,--show-secrets.- Lo que el archivo omite no se gestiona. Un servicio sin
envconserva sus variables intactas;env: {}significa "sin variables" y, con--prune, las elimina. - Los recursos en Skiffly que el archivo no declara se conservan a menos que uses
--prune;applypregunta antes de eliminar (--yesen CI). - Los servicios modificados se despliegan al final de
apply;--no-deploylo omite.
Servicios#
| Opción | Ejemplo | Corresponde a |
|---|---|---|
source | github("acme/web", { branch: "main", rootDir: "apps/web" }), image("nginx:1.27"), empty() | origen |
build | "pnpm build" o { builder: "DOCKERFILE", dockerfilePath: "Dockerfile.prod" } | configuración del build |
start | "node server.js" (null lo borra) | comando de inicio |
healthcheck | "/health" o { path, timeout } | health check |
env | { KEY: "v", URL: db.env.DATABASE_URL, SECRET: preserve(), TOKEN: generate("hex32") } | variables del servicio |
port | 8080 | puerto |
domain | true | debe existir un dominio generado |
domains | ["app.example.com"] | dominios propios |
volumes | ["/data"] o [{ mountPath: "/data", sizeGb: 20 }] | volúmenes |
cron | "0 3 * * *" | programación cron |
resources | { cpu: "500m", memory: "1GB" } | límites |
replicas | 2 | réplicas |
sleep | true | App Sleeping |
restartPolicy | "ON_FAILURE" o { type, maxRetries } | política de reinicio |
fn(name, config) es un servicio con kind: "function"; combínalo con cron para tareas. empty() significa que el origen se gestiona en otro lugar (el panel) mientras el archivo gestiona la configuración, las variables, los dominios y los volúmenes.
Variables y secretos#
const api = service("api");
service("web", {
env: {
DATABASE_URL: db.env.DATABASE_URL, // ${{db.DATABASE_URL}}, con tipos
API_HOST: api.env.SKIFFLY_PRIVATE_DOMAIN, // variable integrada de otro servicio
SELF_URL: "https://${{self.SKIFFLY_PUBLIC_DOMAIN}}", // self = este servicio
REGION: shared("REGION"), // ${{shared.REGION}}
STRIPE_KEY: preserve(), // conserva el valor guardado en Skiffly
JWT_SECRET: generate("hex64"), // aleatorio en el primer apply, luego se conserva
},
});skiffly config init escribe los secretos existentes como process.env.NAME ?? preserve(), así que el archivo generado se puede commitear sin riesgo. plan falla si una variable con preserve() todavía no tiene valor en Skiffly.
Bases de datos#
const db = postgres("db", { sizeGb: 20, env: { POSTGRES_DB: "shop" } });
const cache = redis("cache");
const sql = mysql("sql");
const docs = mongo("docs");Cada una se expande a la plantilla del mismo nombre: imagen, puerto, volumen, contraseña generada y la variable con la cadena de conexión (db.env.DATABASE_URL, cache.env.REDIS_URL, sql.env.MYSQL_URL, docs.env.MONGO_URL). database(name, engine, options) es la forma genérica para otra imagen de un motor conocido.
Entornos en un solo archivo#
export default defineSkiffly((ctx) => {
const prod = ctx.isEnvironment("production");
const web = service("web", { replicas: prod ? 2 : 1, resources: { memory: prod ? "2GB" : "512MB" } });
return project("my-app", { resources: [web] });
});ctx trae command, projectId, environment, isEnvironment(), randomString() y shared (ctx.shared.NAME referencia una variable compartida).
CI#
- run: npx skiffly config apply --yes -p my-app -e production
env:
SKIFFLY_TOKEN: ${{ secrets.SKIFFLY_TOKEN }}Usa un token de espacio de trabajo con el scope write. plan --json produce un diff legible por máquina para comentarios en pull requests.
Diferencias con el SDK de Railway#
defineRailway→defineSkiffly; las variables de plataforma sonSKIFFLY_*.- Los
domainsse aplican, no son solo de importación;domain: truegestiona el dominio generado. - Los
volumesse crean y se eliminan (prune) por ruta de montaje; cambiar el tamaño genera una advertencia. generate()crea secretos en el primer apply;resources,sleepyrestartPolicyson opciones propias de Skiffly.- No disponible:
bucket(),group(),template(), mapas de réplicas multirregión, proxies TCP (usaskiffly proxy).
Referencia completa: el README de @skiffly/config en npm.