Moverte de Fly.io a tu propio servidor
Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta
Fly cobra por uso, lo cual es difícil de proyectar, y “Fly Postgres” sigue siendo un Postgres que operas tú mismo, solo que en las máquinas de Fly en lugar de las tuyas. Si ya tienes un Dockerfile que construye y corre en Fly, ya tienes casi todo lo necesario para correrlo en cualquier otro lado también. Esta guía mueve una app de Fly existente, su entorno, sus secretos y sus datos de Postgres a un solo servidor propio: Docker Compose, Caddy para el HTTPS, una tarea de respaldo real, y luego un cambio de DNS sin ventana de caída. El atajo al final usa el propio importador de Fly de Railyard para las partes que no necesitan un dump y una restauración manuales.
Qué necesitas
- Un servidor. Un Droplet Basic de DigitalOcean con 2 vCPU y 2 GB de RAM corre la mayoría de las apps pequeñas junto con una instancia de Postgres por 18 USD al mes; sube a 4 GB por 24 USD al mes si tu app hace trabajo real en segundo plano. Cualquier máquina Ubuntu equivalente de otro proveedor funciona igual.
- Un nombre de dominio que puedas redirigir, idealmente el que ya está pegado a la app de Fly.
- El CLI
flyctl, ya autenticado contra la cuenta dueña de la app (fly auth whoamidebería mostrar tu cuenta, no un error).
Paso 1: hacer inventario de lo que la app de Fly realmente necesita
Trae la config actual de la app para saber qué estás reproduciendo, en lugar de adivinar:
fly config show -a your-fly-app
Deberías recibir el mismo JSON que Fly despliega, incluyendo el bloque [http_service] o [[services]] con internal_port, el bloque [env] con tus variables no secretas, y cualquier [[mounts]] para volúmenes. Anota el puerto interno en particular, es el único valor en el que todos los demás servicios de esta guía tienen que coincidir.
Lista los secretos por nombre, ya que Fly nunca te muestra sus valores:
fly secrets list -a your-fly-app
Deberías ver una tabla de nombres de secretos, hashes y estado de despliegue, no los valores en sí. Trae los valores reales de donde los configuraste originalmente (un gestor de contraseñas, tus secretos de CI, un compañero de equipo), ya que esto es lo único que Fly de verdad no te puede devolver.
Paso 2: aprovisionar el servidor e instalar Docker
curl -fsSL https://get.docker.com | sh
docker compose version
Deberías ver una cadena de versión de Compose v2.x. Apunta el registro A de tu dominio al nuevo servidor ahora, ya que el paso de HTTPS más adelante depende de eso:
dig +short app.example.com
Paso 3: escribir el archivo de Docker Compose
La mayoría de las apps de Fly ya construyen desde un Dockerfile en el repo, Fly exige uno (o un buildpack que produzca uno) para desplegar siquiera, así que la misma imagen que corre en las máquinas de Fly corre aquí también. Agrega Postgres y Caddy alrededor:
services:
app:
build: .
restart: unless-stopped
env_file: .env
depends_on:
db:
condition: service_healthy
networks:
- app_net
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
start_period: 10s
retries: 20
networks:
- app_net
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- app
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- app_net
networks:
app_net:
volumes:
postgres_data:
caddy_data:
caddy_config:
Arma el .env a partir del bloque [env] que mostró fly config show antes, más los valores de secretos que recuperaste:
POSTGRES_PASSWORD=una-cadena-larga-aleatoria
DATABASE_URL=postgres://app:una-cadena-larga-aleatoria@db:5432/app
PORT=8080
# todo lo del bloque [env] de fly config show va aquí también
# todo lo que fly secrets list haya nombrado va aquí, con su valor real
PORT debería coincidir con el internal_port del paso 1, ya que ese es el puerto en el que el código de tu app realmente escucha, sin importar qué plataforma lo corra.
Paso 4: mover los datos de Postgres
Si la app usa el Postgres no administrado de Fly (una app de Postgres que desplegaste y conectaste tú mismo, el caso común para apps que llevan tiempo corriendo), abre un proxy hacia ella:
fly proxy 15432:5432 -a your-postgres-app
En una segunda terminal, haz el dump de la base a través de ese proxy:
pg_dump -Fc -h localhost -p 15432 -U postgres your_database -f fly-dump.pgdump
Deberías terminar con un archivo fly-dump.pgdump no vacío; ls -lh fly-dump.pgdump debería mostrar un tamaño más o menos acorde a cuántos datos esperas, no un puñado de bytes.
Cópialo al nuevo servidor y restáuralo en el contenedor db una vez que la pila esté arriba:
scp fly-dump.pgdump root@YOUR_SERVER_IP:/root/
docker compose cp /root/fly-dump.pgdump db:/tmp/fly-dump.pgdump
docker compose exec db pg_restore -U app -d app --no-owner --no-acl /tmp/fly-dump.pgdump
--no-owner --no-acl quitan la información de dueño y permisos que solo tenía sentido en el Postgres de Fly, para que la restauración no falle buscando un rol que no existe aquí. Deberías ver que pg_restore termina con, a lo mucho, algunas advertencias sobre objetos que se saltó por la misma razón, no un error duro.
Si en cambio estás en el Managed Postgres más nuevo de Fly, el comando de proxy es fly mpg proxy YOUR_CLUSTER_ID --local-port 15432 y los detalles de conexión (usuario, nombre de base) vienen de fly mpg status en lugar de ser postgres/el nombre de tu app por defecto; los comandos pg_dump/pg_restore en sí son idénticos una vez que estás a través del proxy.
Paso 5: levantar la pila y verificar
docker compose up -d --build
docker compose logs -f app
Deberías ver las mismas líneas normales de arranque de tu app, las mismas que verías en fly logs, terminando con que escucha en el puerto del .env. Confirma que los datos llegaron:
docker compose exec db psql -U app -d app -c "SELECT count(*) FROM your_main_table;"
Ese conteo debería coincidir con lo que veías en Fly antes del dump, salvo algunas filas escritas entre medio.
Paso 6: escribir el Caddyfile y cambiar el DNS
app.example.com {
reverse_proxy app:8080
}
Levanta Caddy y confirma que el certificado se emite (docker compose logs caddy) antes de tocar el DNS. Una vez que https://app.example.com responda bien contra el nuevo servidor, en un subdominio temporal o editando tu /etc/hosts local para probar, actualiza el registro A real para que apunte a la IP del nuevo servidor. Fly sigue sirviendo el subdominio propio de la app todo este tiempo, ya que no tocaste nada ahí, así que no hay un hueco donde ninguno de los dos servidores responda. Una vez confirmado que el tráfico llega al nuevo servidor, pon la app de Fly en cero máquinas y, cuando estés seguro de que ya no la necesitas, bórrala.
Paso 7: respaldos y actualizaciones
Respalda Postgres de la misma forma en que lo volcaste desde Fly, pero ahora con un horario:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db pg_dump -U app -Fc app > "backups/app-db-$STAMP.pgdump"
find backups -name '*.pgdump' -mtime +14 -delete
Copia backups/ fuera del servidor en la misma tarea de cron, a otra máquina o almacenamiento de objetos. Para actualizar la app en sí, reconstruye y recrea solo ese contenedor:
docker compose build app
docker compose up -d app
El atajo: hazlo con Railyard
El importador de Fly de Railyard se encarga de las partes de esto que no involucran tus propios datos: desde Applications → New app → Import from Heroku / Render / Fly, elige Fly, pega un token desde fly tokens create org, y elige la app. Trae la imagen exacta que Fly está corriendo (usando tu token como contraseña de registro, así que nada se reconstruye desde el código fuente), las variables de entorno y el puerto interno desde la config de la máquina, y los nombres de tus secretos, tú vuelves a ingresar sus valores ya que Fly nunca los expone a nada, incluyendo este importador. Otros grupos de procesos más allá del principal todavía no se copian, agrégalos como workers después, y los volúmenes arrancan vacíos, así que el paso 4 de arriba (o su variante de Managed Postgres) sigue siendo cómo se mueven los datos en sí; el importador en sí no toca Postgres para nada.
https://app.railyard.run/deploy?repo=https://github.com/you/your-app
también funciona, para el camino simple de push por Git una vez que el importador resolvió las partes propias de Fly, o si prefieres saltártelo por completo. Mira Importar desde otra plataforma para el panorama completo de lo que trae cada plataforma de origen, Migrar desde Fly.io para el mapeo detalle a detalle entre los conceptos de Fly y los de Railyard, y Costos de hosting si también estás tratando de calcular qué debería costarte realmente el nuevo servidor frente a lo que facturaba Fly.
Solución de problemas
pg_restore falla por un rol o una extensión que no encuentra. Casi siempre es una extensión propia de Fly o un rol referenciado en el dump que no existe en el destino. --no-owner --no-acl resuelve la mayoría de los casos; para una extensión genuinamente faltante, instálala en la imagen de Postgres de destino o quita la línea CREATE EXTENSION del dump antes de restaurar.
La app arranca pero da un 502 inmediato detrás de Caddy. Revisa que PORT en .env coincida tanto con lo que tu app realmente escucha como con lo que apunta la línea reverse_proxy del Caddyfile. Un desajuste aquí es la causa más común, ya que Fly y una configuración genérica de Compose no siempre coinciden en cómo se inyecta el puerto.
El certificado nunca se emite. dig +short app.example.com tiene que resolver primero al nuevo servidor. Si estás probando antes del cambio real, usa un subdominio de staging que ya apunte aquí en lugar de esperar al registro de producción.
Faltan secretos después del import, o la app se cae por un valor de config. La API de Fly nunca devuelve los valores de los secretos, solo los nombres, así que esto se espera: vuelve a ingresar cada uno desde donde lo tenías guardado originalmente, no desde el import.
Sin memoria durante pg_restore en un servidor pequeño. Un droplet de 2 GB puede batallar restaurando un dump grande junto con una app corriendo. Detén el contenedor app durante la restauración (docker compose stop app) para liberar memoria, y arráncalo de nuevo cuando pg_restore termine.
Preguntas frecuentes
¿Tengo que migrar todo de una vez? No. Mueve primero la base de datos y el proceso principal, verifica, y luego agrega cualquier grupo de procesos secundario como servicios worker separados una vez que confirmes que la app principal funciona.
¿Qué pasa con el subdominio que Fly le asigna automáticamente a cada app? Nada, hasta que borres la app de Fly. Sigue respondiendo hasta el momento en que pones la app en cero, que es justo lo que hace posible el cambio sin caída: tanto la dirección vieja como la nueva funcionan al mismo tiempo mientras se propaga el DNS.
¿Esto funciona con apps que usan el despliegue multi-región de Fly? Esta guía te mueve a un solo servidor en una sola región. Si de verdad necesitas varias regiones, repite esta configuración una vez por región detrás de tu propio balanceo de carga, no hay un equivalente de un solo comando aquí al reparto automático entre regiones de Fly.
¿El Managed Postgres de Fly es tan distinto como para necesitar sus propios pasos? Solo el comando de proxy y el usuario por defecto, fly mpg proxy en lugar de fly proxy, y las credenciales desde fly mpg status en lugar del nombre de la app. pg_dump y pg_restore funcionan exactamente igual una vez conectado.
Cualquiera de los dos caminos saca tu app y sus datos del modelo de facturación de Fly y los pone en un servidor donde ves la especificación real y la factura real, los pasos manuales si quieres el control completo del cambio, el importador si prefieres que te traiga el 80 por ciento fácil.
Por qué eligen Railyard para esto
- Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
- Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
- Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
- Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
- Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.