Migre do Fly.io pro seu próprio servidor
Siga isso no seu próprio servidor. Comece grátis, sem cartão
O Fly cobra por uso, o que é difícil de prever, e o “Fly Postgres” ainda é um Postgres que você opera sozinho, só que nas máquinas do Fly em vez das suas. Se você já tem um Dockerfile que faz build e roda no Fly, você já tem quase tudo que precisa pra rodar ele em qualquer outro lugar também. Este guia move uma aplicação Fly existente, o ambiente dela, os secrets dela e os dados de Postgres dela pra um único servidor que é seu: Docker Compose, Caddy pro HTTPS, um job de backup de verdade, e depois um cutover de DNS sem janela de downtime. O caminho mais rápido no final usa o próprio importador de Fly da Railyard pras partes que não precisam de um dump e restore manual.
O que você vai precisar
- Um servidor. Um Basic Droplet da DigitalOcean com 2 vCPUs e 2 GB de RAM roda a maioria das aplicações pequenas e uma instância Postgres juntas por US$18/mês; vá pra 4 GB a US$24/mês se a sua aplicação faz algum trabalho real em segundo plano. Qualquer máquina Ubuntu equivalente de outro provedor funciona do mesmo jeito.
- Um nome de domínio que você pode reapontar, idealmente o que já está conectado à aplicação no Fly.
- O CLI
flyctl, já autenticado contra a conta que é dona da aplicação (fly auth whoamideve imprimir a sua conta, não um erro).
Passo 1: faça um inventário do que a aplicação Fly realmente precisa
Baixe a configuração atual da aplicação pra você saber o que está reproduzindo, em vez de adivinhar:
fly config show -a your-fly-app
Você deve receber de volta o mesmo JSON que o Fly implanta a partir dele, incluindo o bloco [http_service] ou [[services]] com internal_port, o bloco [env] com as suas variáveis não secretas, e qualquer [[mounts]] pra volumes. Anote a porta interna especificamente, é o único valor que todo outro serviço neste guia precisa concordar.
Liste os secrets por nome, já que o Fly nunca te mostra os valores deles:
fly secrets list -a your-fly-app
Você deve ver uma tabela de nomes de secrets, digests e status de deploy, não os valores em si. Puxe os valores reais de onde quer que você tenha definido eles originalmente (um gerenciador de senhas, os secrets do seu CI, um colega de equipe), já que essa é a única coisa que o Fly genuinamente não consegue devolver pra você.
Passo 2: provisione o servidor e instale o Docker
curl -fsSL https://get.docker.com | sh
docker compose version
Você deve ver uma string de versão do Compose v2.x. Aponte o registro A do seu domínio pro servidor novo agora, já que a etapa de HTTPS mais pra frente depende disso:
dig +short app.example.com
Passo 3: escreva o arquivo Docker Compose
A maioria das aplicações Fly já faz build a partir de um Dockerfile no repositório, o Fly exige um (ou um buildpack que produza um) pra fazer deploy de verdade, então a mesma imagem que roda nas máquinas do Fly roda aqui também. Adicione Postgres e Caddy em volta dele:
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:
Monte o .env a partir do bloco [env] que o fly config show imprimiu antes, mais os valores de secret que você recuperou:
POSTGRES_PASSWORD=a-long-random-string
DATABASE_URL=postgres://app:a-long-random-string@db:5432/app
PORT=8080
# tudo do bloco [env] do fly config show vai aqui também
# tudo que o fly secrets list nomeou vai aqui, com o valor real
PORT deve corresponder ao internal_port do Passo 1, já que essa é a porta em que o próprio código da sua aplicação está escutando independente de qual plataforma a roda.
Passo 4: mova os dados do Postgres
Se a aplicação usa o Postgres não gerenciado do Fly (uma aplicação Postgres que você implantou e conectou você mesmo, o caso comum pra aplicações rodando há um tempo), abra um proxy pra ele:
fly proxy 15432:5432 -a your-postgres-app
Num segundo terminal, faça o dump do banco de dados através desse proxy:
pg_dump -Fc -h localhost -p 15432 -U postgres your_database -f fly-dump.pgdump
Você deve acabar com um arquivo fly-dump.pgdump não vazio; ls -lh fly-dump.pgdump deve mostrar um tamanho mais ou menos de acordo com o quanto de dados você espera, não uns poucos bytes.
Copie ele pro servidor novo e restaure dentro do container db depois que a stack subir:
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 descartam informação de role e permissão que só fazia sentido no Postgres do Fly, então a restauração não falha procurando por um role que não existe aqui. Você deve ver o pg_restore terminar com, no máximo, alguns avisos sobre objetos que ele pulou pelo mesmo motivo, não um erro fatal.
Se em vez disso você estiver no Managed Postgres mais novo do Fly, o comando de proxy é fly mpg proxy YOUR_CLUSTER_ID --local-port 15432 e os detalhes de conexão (usuário, nome do banco de dados) vêm de fly mpg status em vez de serem postgres/o nome da sua aplicação por padrão; os próprios comandos pg_dump/pg_restore são idênticos uma vez que você está através do proxy.
Passo 5: suba a stack e verifique
docker compose up -d --build
docker compose logs -f app
Você deve ver as linhas de log de inicialização normais da sua própria aplicação, as mesmas que você veria em fly logs, terminando com ela escutando na porta do .env. Confirme que os dados vieram:
docker compose exec db psql -U app -d app -c "SELECT count(*) FROM your_main_table;"
Essa contagem deve corresponder ao que você viu no Fly antes do dump, dando ou tirando linhas escritas no meio do caminho.
Passo 6: escreva o Caddyfile e faça o cutover de DNS
app.example.com {
reverse_proxy app:8080
}
Suba o Caddy e confirme que o certificado é emitido (docker compose logs caddy) antes de tocar no DNS. Depois que https://app.example.com responder corretamente contra o servidor novo num subdomínio temporário ou editando o seu /etc/hosts local pra testar, atualize o registro A de verdade pra apontar pro IP do servidor novo. O Fly continua servindo o próprio subdomínio da aplicação o tempo todo, já que você não tocou em nada ali, então não existe uma lacuna onde nenhum dos dois servidores responde. Depois de confirmar que o tráfego está fluindo pro servidor novo, reduza a aplicação no Fly pra zero máquinas e, quando você tiver confiança de que não vai precisar dela, apague.
Passo 7: backups e upgrades
Faça backup do Postgres do mesmo jeito que você fez o dump dele a partir do Fly, num horário agendado desta vez:
#!/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
Copie backups/ pra fora do servidor no mesmo cron job, pra outra máquina ou armazenamento de objetos. Pra atualizar a própria aplicação, refaça o build e recrie só esse container:
docker compose build app
docker compose up -d app
O caminho mais rápido: faça isso com a Railyard
O importador de Fly da Railyard trata as partes disso que não envolvem os seus próprios dados: em Applications → New app → Import from Heroku / Render / Fly, escolha Fly, cole um token de fly tokens create org, e escolha a aplicação. Ele puxa a imagem exata que o Fly está rodando (usando o seu token como a senha do registry, então nada refaz build a partir da fonte), as variáveis de ambiente e a porta interna a partir da configuração da máquina, e os nomes dos seus secrets, você redigita os valores deles já que o Fly nunca os expõe pra nada, incluindo esse importador. Outros grupos de processo além do principal ainda não são copiados, adicione eles como workers depois, e qualquer volume começa vazio, então o Passo 4 acima (ou a variante de Postgres gerenciado dele) ainda é como os dados reais se movem; o próprio importador não toca no Postgres de forma alguma.
https://app.railyard.run/deploy?repo=https://github.com/you/your-app
também funciona, pro caminho simples de git-push depois que o importador tiver tratado as partes específicas do Fly ou se você preferir pular ele totalmente. Veja Import from another platform pro quadro completo do que cada plataforma de origem traz, Migrating from Fly.io pro mapeamento campo a campo entre os conceitos do Fly e os da Railyard, e Hosting costs se você também estiver tentando descobrir o que o servidor novo deveria realmente custar comparado ao que o Fly estava cobrando.
Resolução de problemas
pg_restore falha num role ou extensão que não consegue achar. Isso é quase sempre uma extensão específica do Fly ou um role que o dump referencia que não existe no destino. --no-owner --no-acl trata a maior parte disso; pra uma extensão genuinamente faltante, instale ela na imagem Postgres de destino ou remova a linha CREATE EXTENSION do dump antes de restaurar.
A aplicação sobe mas imediatamente retorna 502 atrás do Caddy. Cheque se PORT no .env corresponde tanto ao que a sua aplicação de fato vincula quanto ao que a linha reverse_proxy do Caddyfile aponta. Uma incompatibilidade aqui é a causa mais comum de todas, já que o Fly e uma configuração genérica de Compose não sempre concordam em como a porta é injetada.
O certificado nunca é emitido. dig +short app.example.com tem que resolver pro servidor novo primeiro. Se você estiver testando antes do cutover real, use um subdomínio de staging que já aponte pra aqui em vez de esperar pelo registro de produção.
Secrets faltando depois da importação, ou a aplicação quebra num valor de configuração. A própria API do Fly nunca retorna valores de secret, só nomes, então isso é esperado: redigite cada um de onde quer que você tenha armazenado originalmente, não da importação.
Falta de memória durante o pg_restore num servidor pequeno. Um droplet de 2 GB pode ter dificuldade restaurando um dump grande junto com uma aplicação rodando. Pare o container app durante a restauração (docker compose stop app) pra liberar a memória, e inicie de novo quando o pg_restore terminar.
Perguntas frequentes
Eu preciso migrar tudo de uma vez? Não. Traga o banco de dados e o processo principal primeiro, verifique, e depois adicione qualquer grupo de processo secundário como serviços worker separados depois de confirmar que a aplicação principal funciona.
O que acontece com o subdomínio que o Fly atribui automaticamente a cada aplicação? Nada, até você apagar a aplicação no Fly. Ele continua respondendo até você reduzir a aplicação pra zero, que é o que torna o cutover sem downtime possível: tanto o endereço antigo quanto o novo funcionam simultaneamente enquanto o DNS se propaga.
Isso funciona pra aplicações usando o deploy multi-região do Fly? Este guia te move pra um servidor numa região. Se você genuinamente precisa de várias regiões, rode essa configuração uma vez por região atrás do seu próprio balanceamento de carga, não existe um equivalente de comando único pro fanout automático de região do Fly aqui.
O Managed Postgres do Fly é diferente o suficiente pra precisar das próprias etapas? Só o comando de proxy e o nome de usuário padrão, fly mpg proxy em vez de fly proxy, e credenciais de fly mpg status em vez do nome da aplicação. pg_dump e pg_restore funcionam exatamente igual depois que você está conectado.
Qualquer um dos dois caminhos tira a sua aplicação e os dados dela do modelo de cobrança do Fly e coloca num servidor onde você consegue ver a especificação real e a conta real, as etapas manuais se você quiser controle total sobre o cutover, o importador se você preferir deixar ele puxar os 80 por cento fáceis pra você.
Por que escolhem o Railyard para isso
- Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
- Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
- Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
- Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
- Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.