Ir al contenido

Buscar en la documentación

Busca en la documentación de Skiffly

Migrar una app desde Heroku, paso a paso

Una migración resuelta de una app típica de Heroku (dynos web + worker, Heroku Postgres, Heroku Redis, Scheduler, config vars, un dominio propio) a Skiffly sin pérdida de datos y con un cambio de DNS controlado.

La guía de migración desde Heroku mapea los conceptos. Este tutorial lleva una app concreta a través de la migración: una API Node/Express llamada acme-api con un dyno web y otro worker, Heroku Postgres, Heroku Redis, dos tareas de Scheduler, 30 config vars y api.example.com. Ajusta los comandos a tu stack; las páginas por lenguaje tienen los equivalentes para Rails, Django y Laravel.

Necesitas: la CLI de Heroku todavía con sesión iniciada, el código en GitHub, skiffly (npm i -g skiffly && skiffly login), pg_restore y redis-cli en local. Calcula 1–2 horas, incluida una ventana de mantenimiento de unos minutos para copiar la base de datos.

0. Inventario#

heroku ps -a acme-api                       # dynos: web.1, worker.1
cat Procfile                                # web: node dist/server.js  /  worker: node dist/worker.js
heroku addons -a acme-api                   # heroku-postgresql, heroku-redis, scheduler
heroku config -s -a acme-api > heroku.env   # KEY=value, uno por línea
heroku domains -a acme-api                  # api.example.com

Todo lo que hay en Procfile se convierte en un servicio; cada add-on se convierte en una plantilla; release: (si lo tienes) pasa a formar parte del comando de inicio.

1. Proyecto y bases de datos en Skiffly#

cd acme-api
skiffly init --name acme-api
skiffly deploy --template postgres      # "Postgres", volumen de 10 GB; ajusta el tamaño desde el panel si tu base de datos es más grande
skiffly deploy --template redis
skiffly status                          # ambos SUCCESS

Resultado esperado: Postgres y Redis corriendo en la red privada, cada uno con sus propias variables DATABASE_URL y REDIS_URL.

2. El servicio web#

skiffly up -y -d                        # crea el servicio "acme-api", arranca el primer build

El build termina bien, pero la app se cae porque no tiene sus variables: por ahora es lo esperado. Configúralas:

# descarta las URL de add-ons que gestionaba Heroku, conserva el resto
grep -v -E '^(DATABASE_URL|REDIS_URL|REDIS_TLS_URL|HEROKU_|PORT=)' heroku.env > vars.env
skiffly variables set -s acme-api --skip-deploys $(cat vars.env | xargs)
skiffly variables set -s acme-api --skip-deploys 'DATABASE_URL=${{Postgres.DATABASE_URL}}' 'REDIS_URL=${{Redis.REDIS_URL}}'

Sobre las comillas: xargs falla con valores que contienen espacios o #; configúralos uno por uno (skiffly variables set KEY="a value") o usa el editor en bruto del panel, que acepta el texto .env tal cual.

Ahora el comando de inicio. Heroku ejecutaba web: node dist/server.js; Railpack elegiría npm start, que normalmente es lo mismo. Si tenías una fase de release (release: npm run migrate), intégrala:

  • Configuración → Despliegue → Comando de inicio: sh -c "npm run migrate && node dist/server.js"
  • Ruta del health check: /healthz (agrega la ruta si la app no la tiene; Heroku no la necesitaba, pero los rollouts sin tiempo de inactividad de Skiffly sí).
skiffly redeploy -s acme-api
skiffly logs -s acme-api -f
skiffly domain -s acme-api             # https://acme-api-x1y2.skiffly.cloud

Resultado esperado: SUCCESS, y la URL generada responde. Skiffly define PORT exactamente igual que Heroku; los archivos .env al estilo heroku local no se leen: todo son variables.

Diferencias que conviene revisar ahora, antes de mover los datos:

Costumbre de HerokuEn Skiffly
SSL forzado mediante X-Forwarded-Protomisma cabecera; conserva tu configuración de trust proxy / force_ssl
REDIS_TLS_URL / rediss://la plantilla de Redis habla redis:// plano en la red privada; quita las opciones de TLS
?sslmode=require en la URL de Postgresno hace falta (red privada); algunos drivers fallan con él: elimínalo
Disco efímeroigual; las subidas van a S3 o a un volumen
heroku runskiffly ssh -- <cmd>

3. El worker#

Crea un segundo servicio desde el mismo repositorio: en el panel, Nuevo servicio → Repositorio de GitHub (mismo repositorio, nombre worker), o con configuración como código (paso 7). Configuración:

  • Comando de inicio: sh -c 'node -e "require(\"http\").createServer((_,r)=>r.end(\"ok\")).listen(process.env.PORT)" & exec node dist/worker.js'. Skiffly sondea todo servicio de larga duración en su puerto; esta línea mantiene el rollout saludable para un worker que no tiene servidor HTTP propio (consulta Workers).
  • Sin dominio.
  • Variables: la misma lista. Usa referencias para no duplicarlas: DATABASE_URL=${{Postgres.DATABASE_URL}}, REDIS_URL=${{Redis.REDIS_URL}}, y para los secretos de la app, o bien ${{acme-api.SESSION_SECRET}} o muévelos a las variables compartidas del entorno (skiffly variables set --shared).

Resultado esperado: skiffly logs -s worker muestra al worker conectándose a Redis.

4. Tareas de Scheduler#

Cada entrada de Heroku Scheduler se convierte en un servicio con una programación cron (UTC) y el comando de la tarea como comando de inicio; ejecuta un contenedor por tick y termina. Los servicios cron no se sondean y no reciben dominio.

SchedulerServicio en Skiffly
node dist/jobs/digest.js, diario a las 03:00servicio digest, cron 0 3 * * *, comando de inicio node dist/jobs/digest.js
node dist/jobs/cleanup.js, cada 10 minservicio cleanup, cron */10 * * * *

Créalos igual que el worker (mismo repositorio) o en el archivo de configuración de más abajo.

5. Copiar los datos#

Hazlo en una ventana de mantenimiento corta: pon Heroku en modo de mantenimiento, copia, verifica y luego cambia el DNS.

heroku maintenance:on -a acme-api
heroku pg:backups:capture -a acme-api
heroku pg:backups:download -a acme-api          # latest.dump
 
skiffly connect postgres --print                 # abre un proxy TCP e imprime postgresql://postgres:…@edge.skiffly.cloud:2xxxx/app
pg_restore --no-owner --no-acl --clean --if-exists -d "postgresql://postgres:…@edge.skiffly.cloud:2xxxx/app" latest.dump
psql "postgresql://postgres:…@edge.skiffly.cloud:2xxxx/app" -c "select count(*) from users"
skiffly proxy delete <port>                      # cierra el proxy público

Las advertencias de pg_restore sobre el esquema heroku_ext o extensiones propiedad de roles de Heroku son normales; los errores por extensiones faltantes significan que necesitas la plantilla postgis/pgvector en lugar de postgres a secas (despliega esa y repite).

Redis en Heroku suele ser caché y colas: déjalo arrancar vacío. Si tienes que mover claves, la ruta manual es redis-cli --rdb del lado de Heroku y redis-cli -u "$(skiffly connect redis --print)" --pipe después de DEBUG RELOAD.

Reinicia la app para que los pools se reconecten: skiffly restart -s acme-api.

6. Cambio de DNS del dominio#

skiffly domain -s acme-api api.example.com
# agrega los dos registros en tu proveedor de DNS:
#   api.example.com.            CNAME  edge.skiffly.cloud.
#   _skiffly.api.example.com.   TXT    "<token>"
skiffly domain status api.example.com            # espera a "verified"; el certificado se emite en menos de un minuto

Si puedes, baja el TTL de api.example.com un día antes. Hasta que el CNAME se propague, Heroku sigue respondiendo (está en modo de mantenimiento, así que los usuarios ven la página de mantenimiento durante unos minutos como máximo). Cuando curl -sI https://api.example.com devuelva cabeceras de Skiffly, ya estás en producción.

Resultado esperado: SKIFFLY_PUBLIC_DOMAIN en el servicio ahora muestra el dominio propio; actualiza cualquier variable que tuviera el hostname de Heroku (APP_URL, callbacks de OAuth, orígenes CORS).

7. Configuración como código (recomendado antes de borrar nada)#

skiffly config init                     # escribe .skiffly/skiffly.ts con todo lo creado arriba
skiffly config plan                     # sin cambios
.skiffly/skiffly.ts
import { defineSkiffly, github, postgres, preserve, project, redis, service, shared } from "@skiffly/config";
 
export default defineSkiffly(() => {
  const db = postgres("Postgres");
  const cache = redis("Redis");
  const env = { NODE_ENV: "production", DATABASE_URL: db.env.DATABASE_URL, REDIS_URL: cache.env.REDIS_URL, SESSION_SECRET: shared("SESSION_SECRET") };
  const src = github("acme/acme-api");
  const web = service("acme-api", { source: src, start: 'sh -c "npm run migrate && node dist/server.js"', healthcheck: "/healthz", env, domains: ["api.example.com"], domain: true });
  const worker = service("worker", { source: src, start: "sh -c 'node -e \"require(\\\"http\\\").createServer((_,r)=>r.end(\\\"ok\\\")).listen(process.env.PORT)\" & exec node dist/worker.js'", env });
  const digest = service("digest", { source: src, start: "node dist/jobs/digest.js", cron: "0 3 * * *", env });
  return project("acme-api", { resources: [db, cache, web, worker, digest] });
});

Haz commit. skiffly config apply --yes en CI (con un secreto SKIFFLY_TOKEN) reemplaza ahora al pipeline de Heroku.

8. Dar de baja Heroku#

Tras unos días de tráfico en Skiffly sin sorpresas en skiffly logs ni en Proyecto → Observabilidad:

heroku pg:backups:capture -a acme-api        # un último respaldo; descárgalo y consérvalo
heroku apps:destroy -a acme-api

Qué cambia en el día a día#

  • Despliegues: cada push a main compila (o skiffly up desde tu máquina). Las vistas previas de PR reemplazan a las review apps.
  • Logs: las últimas 5 000 líneas por despliegue en skiffly logs; reenvíalos a tu servicio de logs si conservabas un historial al estilo Papertrail.
  • Escalado: réplicas (numReplicas) y límites de CPU/memoria por servicio en lugar de tipos de dyno; un servicio con volumen se queda en una réplica.
  • Facturación: un saldo prepago en lugar de una factura mensual; recarga antes de que el saldo llegue a cero (3 días de gracia y luego los servicios se pausan).
  • Respaldos: snapshots diarios de los volúmenes más un volcado lógico de Postgres, restaurados por soporte; conserva también tu propio cron de pg_dump.