Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save insinfo/ae6d11f3177331e6538763ec3357ddf0 to your computer and use it in GitHub Desktop.

Select an option

Save insinfo/ae6d11f3177331e6538763ec3357ddf0 to your computer and use it in GitHub Desktop.

Tutorial: migrar uma aplicação Python/Flask do Supabase para uma VPS Hostinger com Coolify


1. Resultado esperado

A arquitetura recomendada ao final da migração será:

Internet
   ↓
DNS do domínio
   ↓
Coolify / proxy reverso / HTTPS
   ↓
Aplicação Flask executada pelo Gunicorn
   ├── PostgreSQL privado
   ├── volume persistente para uploads, se necessário
   └── armazenamento S3/R2/MinIO, opcional

Exemplo de endereços:

https://seudominio.com.br          → aplicação Flask
https://coolify.seudominio.com.br  → painel do Coolify

O PostgreSQL não deve ser publicado diretamente na internet. A aplicação deve acessá-lo pela rede privada do Docker/Coolify.


2. Antes de começar: descubra o quanto a aplicação depende do Supabase

Há uma diferença importante entre:

  1. usar o Supabase apenas como PostgreSQL;
  2. usar o cliente supabase-py para consultar tabelas;
  3. usar Supabase Auth;
  4. usar Supabase Storage;
  5. usar Realtime;
  6. usar Edge Functions;
  7. usar RLS, auth.uid(), auth.jwt() ou roles como anon e authenticated.

A migração é simples quando a aplicação usa apenas o banco PostgreSQL. Ela exige adaptação de código quando usa os demais serviços.

2.1 Confirme se a aplicação é Flask

Na raiz do projeto, execute no PowerShell:

Get-ChildItem -Recurse -File |
  Select-String -Pattern "from flask|import flask|Flask\(__name__\)|create_app\("

Com ripgrep, se estiver instalado:

rg -n "from flask|import flask|Flask\(__name__\)|create_app\(" .

Indícios de Flask:

from flask import Flask

app = Flask(__name__)

ou:

def create_app():
    app = Flask(__name__)
    return app

Outros frameworks comuns:

rg -n "FastAPI|from fastapi|django|manage\.py|uvicorn|streamlit" .

Interpretação:

Resultado encontrado Framework provável
from flask import Flask Flask
from fastapi import FastAPI FastAPI
manage.py, django Django
streamlit run Streamlit

O restante deste tutorial usa Flask. A parte de banco, Supabase, VPS e Coolify continua válida para outros frameworks, mas o comando de inicialização muda.

2.2 Localize o ponto de entrada da aplicação

Procure:

Get-ChildItem -Recurse -File -Include app.py,main.py,wsgi.py,run.py

Os casos mais comuns do Gunicorn são:

app.py contém "app = Flask(...)"        → app:app
main.py contém "app = Flask(...)"       → main:app
wsgi.py contém "app = create_app()"     → wsgi:app
pacote com factory create_app()          → nome_do_pacote:create_app()

Guarde essa informação. Ela será usada no Dockerfile.

2.3 Procure dependências do Supabase

Execute:

rg -n "supabase|SUPABASE_|create_client|\.table\(|\.auth\.|\.storage\.|\.rpc\(|\.channel\(" .

Procure também as variáveis de ambiente:

rg -n "DATABASE_URL|DB_HOST|DB_PORT|DB_NAME|DB_USER|DB_PASSWORD|POSTGRES" .

Verifique:

requirements.txt
pyproject.toml
Pipfile
poetry.lock
.env
.env.example
docker-compose.yml
compose.yaml

2.4 Matriz de migração

Recurso usado atualmente O que fazer no novo ambiente
Conexão PostgreSQL direta Trocar apenas a conexão para o PostgreSQL do Coolify
supabase.table(...).select(...) Reescrever para SQLAlchemy ou Psycopg
Supabase Auth Escolher estratégia própria de autenticação ou hospedar o Auth
Supabase Storage Migrar arquivos para volume local, MinIO ou serviço S3
Supabase Realtime Reimplementar com WebSocket, SSE, fila ou PostgreSQL LISTEN/NOTIFY
Supabase Edge Functions Mover a lógica para rotas, serviços ou workers Python
RPC em funções PostgreSQL Pode ser mantida se a função for exportada com o schema
RLS com auth.uid() Reimplementar autorização no Flask ou manter uma pilha Supabase compatível
Acesso do navegador diretamente ao Supabase Trocar por chamadas à API Flask

Ponto crítico: o cliente supabase-py não se conecta diretamente a um PostgreSQL comum. Ele consome APIs do Supabase, como PostgREST, Auth e Storage. Se o código usa supabase.table(...), não basta trocar a URL: será necessário usar SQLAlchemy, Psycopg ou hospedar a pilha Supabase completa.


3. Escolha entre PostgreSQL simples e Supabase completo auto-hospedado

Caminho recomendado neste tutorial: Flask + PostgreSQL simples

Use quando:

  • a aplicação é majoritariamente backend;
  • as regras de autorização podem ficar no Flask;
  • não existe dependência forte de Realtime;
  • os uploads podem ir para volume persistente ou S3;
  • você quer reduzir consumo de RAM e complexidade.

Arquitetura:

Coolify
├── aplicação Flask + Gunicorn
├── PostgreSQL
└── volume de uploads ou armazenamento S3

Caminho alternativo: Supabase completo auto-hospedado

Considere hospedar a pilha Supabase completa quando a aplicação depende intensamente de:

  • Supabase Auth com muitos usuários;
  • login social, MFA, magic link ou OTP;
  • PostgREST;
  • Realtime;
  • Storage com RLS;
  • funções e políticas baseadas em auth.uid() e auth.jwt().

Essa opção exige mais memória e inclui vários serviços. Não é o caminho principal deste tutorial.


4. Faça um inventário antes de tocar na produção

Crie uma pasta de migração:

New-Item -ItemType Directory -Force .\migracao

Registre:

[ ] URL atual da aplicação
[ ] Project Ref do Supabase
[ ] versão do PostgreSQL de origem
[ ] schemas usados pela aplicação
[ ] extensões PostgreSQL usadas
[ ] quantidade de tabelas
[ ] contagem de registros das tabelas principais
[ ] buckets do Storage
[ ] quantidade e tamanho dos arquivos
[ ] número de usuários no Supabase Auth
[ ] funções SQL, triggers e views
[ ] Edge Functions
[ ] políticas RLS
[ ] integrações externas
[ ] domínio e provedor DNS

4.1 Obtenha a conexão do banco no Supabase

No painel do Supabase:

Project
→ Connect
→ escolha Direct connection ou Session pooler

A conexão terá formato semelhante a:

postgresql://usuario:SENHA@host:5432/postgres?sslmode=require

No PowerShell:

$env:SUPABASE_DB_URL = "postgresql://USUARIO:SENHA@HOST:5432/postgres?sslmode=require"

Não coloque essa URL em arquivo versionado.

Caso a senha contenha @, :, /, #, % ou outros caracteres reservados, ela precisa estar percent-encoded na URL.

4.2 Descubra a versão do PostgreSQL

Com o cliente PostgreSQL instalado:

psql "$env:SUPABASE_DB_URL" -c "select version();"

Consulte também:

SHOW server_version;

Não escolha automaticamente PostgreSQL 16 ou 17 no destino antes de verificar a origem. Para a primeira migração, o caminho de menor risco é usar a mesma versão principal ou uma versão posterior compatível.

O pg_dump usado para exportar deve ter versão principal igual ou superior à versão do servidor de origem. Um pg_dump mais antigo recusa exportar um servidor PostgreSQL mais novo.

4.3 Liste schemas

psql "$env:SUPABASE_DB_URL" -c "
select schema_name
from information_schema.schemata
order by schema_name;
"

Normalmente, as tabelas da aplicação estão em:

public

Mas podem existir schemas próprios, como:

app
cms
portal
financeiro

4.4 Liste as extensões

psql "$env:SUPABASE_DB_URL" -c "
select extname, extversion
from pg_extension
order by extname;
"

Extensões comuns:

pgcrypto
uuid-ossp
citext
pg_trgm
unaccent
vector

Algumas extensões específicas do ambiente Supabase podem não existir na imagem oficial do PostgreSQL. Identifique isso antes da restauração.

4.5 Liste tabelas e tamanho

psql "$env:SUPABASE_DB_URL" -c "
select
  schemaname,
  relname as tabela,
  pg_size_pretty(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(relname))) as tamanho
from pg_stat_user_tables
order by pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(relname)) desc;
"

4.6 Gere contagens exatas das tabelas principais

Exemplo:

psql "$env:SUPABASE_DB_URL" -c "
select count(*) as total_usuarios from public.usuarios;
select count(*) as total_noticias from public.noticias;
select count(*) as total_arquivos from public.arquivos;
"

Troque os nomes pelas tabelas reais. Salve os resultados para comparar depois.


5. Prepare o repositório

Crie uma branch:

git switch -c migracao-hostinger-coolify

Confirme que segredos não serão enviados ao Git:

.env
.env.*
!.env.example
*.dump
*.backup
migracao/
uploads/
__pycache__/
*.py[cod]
.venv/
venv/

Crie um .env.example sem valores reais:

APP_ENV=production
SECRET_KEY=

DB_HOST=
DB_PORT=5432
DB_NAME=
DB_USER=
DB_PASSWORD=

UPLOAD_DIR=/app/uploads

GUNICORN_WORKERS=2
GUNICORN_THREADS=4
GUNICORN_TIMEOUT=120

Nunca coloque no Git:

SUPABASE_SERVICE_ROLE_KEY
senha do PostgreSQL
SECRET_KEY real
chaves S3
credenciais SMTP
tokens OAuth

6. Prepare a VPS Hostinger

6.1 Tamanho sugerido

Para uma aplicação Flask pequena ou média:

Mínimo razoável:
2 vCPU
4 GB de RAM
60 a 80 GB de disco

Mais confortável:
4 vCPU
8 GB de RAM
100 GB ou mais

O tamanho real depende de:

  • quantidade de acessos;
  • tamanho do banco;
  • processamento de imagens;
  • workers;
  • tarefas em segundo plano;
  • volume de uploads;
  • builds Docker.

6.2 Escolha o template

Na Hostinger, prefira:

Ubuntu 24.04 with Coolify

No template atual, o Coolify já vem instalado.

6.3 Primeiro acesso ao Coolify

Acesse:

http://IP_DA_VPS:3000

Crie o primeiro usuário administrador.

No onboarding, se a aplicação será executada na mesma VPS, selecione:

localhost

A porta inicial usada pelo template atual da Hostinger é 3000. Não use a porta 8000 para acessar o painel, a menos que a sua instalação específica tenha sido configurada de outra forma.

6.4 Atualize o servidor

Entre por SSH:

ssh root@IP_DA_VPS

Atualize:

apt update
apt upgrade -y

Defina o fuso horário:

timedatectl set-timezone America/Sao_Paulo

Confira:

timedatectl

6.5 Firewall

No firewall da Hostinger ou no firewall adotado para a VPS, permita:

22/TCP   → SSH, preferencialmente limitado ao seu IP
80/TCP   → HTTP
443/TCP  → HTTPS
3000/TCP → temporariamente, para o primeiro acesso ao Coolify

Não exponha:

5432/TCP → PostgreSQL
8000/TCP → Gunicorn
Docker socket

Depois de configurar um domínio HTTPS para o painel do Coolify, feche o acesso público à porta 3000 ou restrinja-o ao seu IP.

Docker pode interagir com regras de firewall de maneira diferente de processos comuns. Valide externamente quais portas realmente estão abertas.

6.6 SSH por chave

Na sua máquina:

ssh-keygen -t ed25519 -a 100

Copie a chave pública para a VPS e valide o login antes de desabilitar login por senha.


7. Configure o DNS

Sugestão:

Tipo  Nome      Valor
A     @         IP_DA_VPS
A     www       IP_DA_VPS
A     coolify   IP_DA_VPS

Resultado:

seudominio.com.br
www.seudominio.com.br
coolify.seudominio.com.br

Caso a aplicação seja apenas uma API:

A     api       IP_DA_VPS

Antes da virada definitiva, reduza o TTL do registro para facilitar rollback.

Se usar Cloudflare:

  1. deixe inicialmente em DNS only;
  2. configure o domínio e o certificado no Coolify;
  3. teste HTTPS;
  4. depois ative o proxy da Cloudflare, se desejar.

8. Prepare a aplicação Flask para produção

O servidor de desenvolvimento do Flask não deve ser usado em produção. Use Gunicorn atrás do proxy do Coolify.

8.1 Dependências

Mantenha as dependências reais já existentes e adicione, conforme a arquitetura:

Flask
gunicorn
psycopg[binary]
SQLAlchemy
Flask-SQLAlchemy
python-dotenv

Exemplo mínimo:

Flask>=3.1,<4
gunicorn>=23,<24
psycopg[binary]>=3.2,<4
SQLAlchemy>=2.0,<3
Flask-SQLAlchemy>=3.1,<4
python-dotenv>=1.0,<2

Não substitua cegamente o requirements.txt existente. Apenas adicione o que estiver faltando.

Instale localmente:

python -m pip install -r requirements.txt

8.2 Configuração do banco sem problemas com caracteres na senha

Crie ou adapte config.py:

import os

from sqlalchemy import URL


def required(name: str) -> str:
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f"Variável obrigatória ausente: {name}")
    return value


def build_database_url() -> str:
    # Permite manter compatibilidade com projetos que já usam DATABASE_URL.
    configured_url = os.getenv("DATABASE_URL")
    if configured_url:
        return configured_url

    url = URL.create(
        drivername="postgresql+psycopg",
        username=required("DB_USER"),
        password=required("DB_PASSWORD"),
        host=required("DB_HOST"),
        port=int(os.getenv("DB_PORT", "5432")),
        database=required("DB_NAME"),
    )

    return url.render_as_string(hide_password=False)


class Config:
    SECRET_KEY = required("SECRET_KEY")

    SQLALCHEMY_DATABASE_URI = build_database_url()
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ENGINE_OPTIONS = {
        "pool_pre_ping": True,
        "pool_recycle": 1800,
        "pool_size": int(os.getenv("DB_POOL_SIZE", "5")),
        "max_overflow": int(os.getenv("DB_MAX_OVERFLOW", "5")),
    }

    MAX_CONTENT_LENGTH = int(
        os.getenv("MAX_CONTENT_LENGTH", str(25 * 1024 * 1024))
    )

    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SECURE = True
    SESSION_COOKIE_SAMESITE = "Lax"

    UPLOAD_DIR = os.getenv("UPLOAD_DIR", "/app/uploads")

Se o projeto usa psycopg2 em vez de Psycopg 3, use:

postgresql+psycopg2

8.3 Factory Flask, proxy e endpoints de saúde

Exemplo:

from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import text
from werkzeug.middleware.proxy_fix import ProxyFix

from config import Config

db = SQLAlchemy()


def create_app() -> Flask:
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)

    # Use somente porque a aplicação estará atrás do proxy do Coolify.
    # O valor 1 representa um proxy confiável à frente da aplicação.
    app.wsgi_app = ProxyFix(
        app.wsgi_app,
        x_for=1,
        x_proto=1,
        x_host=1,
    )

    @app.get("/health")
    def health():
        # Verifica apenas se o processo Flask está respondendo.
        return jsonify(status="ok"), 200

    @app.get("/ready")
    def ready():
        # Verifica também o banco.
        db.session.execute(text("select 1"))
        return jsonify(status="ready"), 200

    return app

Se a Cloudflare ficar na frente do Coolify, haverá mais de um proxy na cadeia. Valide os cabeçalhos antes de alterar os valores de ProxyFix. Configurá-lo incorretamente pode permitir falsificação de endereço, host ou protocolo.

8.4 Arquivo wsgi.py

from app import create_app

app = create_app()

Se a aplicação já possui app = Flask(__name__), o wsgi.py pode ser:

from app import app

8.5 Configuração do Gunicorn

Crie gunicorn.conf.py:

import os

bind = "0.0.0.0:8000"

workers = int(os.getenv("GUNICORN_WORKERS", "2"))
threads = int(os.getenv("GUNICORN_THREADS", "4"))

timeout = int(os.getenv("GUNICORN_TIMEOUT", "120"))
graceful_timeout = 30
keepalive = 5

accesslog = "-"
errorlog = "-"
capture_output = True

worker_tmp_dir = "/dev/shm"

Comece com poucos workers. Workers demais aumentam bastante o consumo de RAM.

8.6 Dockerfile

Este exemplo usa Python 3.12 por compatibilidade. Troque somente depois de confirmar que todas as dependências suportam outra versão.

FROM python:3.12-slim AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       build-essential \
       libpq-dev \
    && rm -rf /var/lib/apt/lists/*

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --upgrade pip \
    && pip install -r requirements.txt


FROM python:3.12-slim AS runtime

ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       libpq5 \
    && rm -rf /var/lib/apt/lists/* \
    && groupadd --system app \
    && useradd --system --gid app --home-dir /app app

COPY --from=builder /opt/venv /opt/venv
COPY . .

RUN mkdir -p /app/uploads \
    && chown -R app:app /app

USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)" || exit 1

CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi:app"]

Troque wsgi:app pelo ponto de entrada real:

app:app
main:app
wsgi:app
nome_do_pacote:create_app()

8.7 .dockerignore

.git
.gitignore
.env
.env.*
!.env.example

.venv
venv
__pycache__
*.pyc
*.pyo
*.pyd

.pytest_cache
.mypy_cache
.coverage
htmlcov

migracao
backups
uploads

Dockerfile*
compose*.yaml
docker-compose*.yml
README*

Se o projeto precisa de algum arquivo excluído, ajuste a lista.

8.8 Teste local do container

docker build -t minha-app-flask .

Crie um .env.local apenas para teste e execute:

docker run --rm `
  --env-file .env.local `
  -p 8000:8000 `
  minha-app-flask

Teste:

curl.exe http://localhost:8000/health

Resposta esperada:

{"status":"ok"}

9. Crie o PostgreSQL no Coolify

Há dois modos possíveis.


9.1 Modo recomendado: PostgreSQL como recurso nativo do Coolify

No painel:

Projects
→ New Project
→ nome-do-projeto
→ Production
→ Add New Resource
→ Database
→ PostgreSQL

Defina:

Database: appdb
User: app_user
Password: senha forte
Versão: mesma versão principal da origem ou uma versão posterior validada

Não publique a porta 5432.

Depois que o banco estiver ativo:

  1. copie a conexão interna mostrada pelo Coolify;
  2. ou anote host interno, porta, banco, usuário e senha;
  3. cadastre esses valores nas variáveis da aplicação.

Exemplo:

DB_HOST=HOST_INTERNO_FORNECIDO_PELO_COOLIFY
DB_PORT=5432
DB_NAME=appdb
DB_USER=app_user
DB_PASSWORD=SENHA_FORTE

Vantagens desse modo:

  • gerenciamento separado;
  • terminal próprio;
  • configuração de backups no painel;
  • atualização da aplicação sem recriar o banco;
  • menor risco de exclusão acidental do volume.

9.2 Alternativa: aplicação e PostgreSQL em um único Docker Compose

Use quando quiser versionar toda a pilha no mesmo repositório.

Crie compose.yaml:

services:
  db:
    image: postgres:${POSTGRES_MAJOR:-17}
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:?}
      POSTGRES_USER: ${POSTGRES_USER:?}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    expose:
      - "5432"
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 20s

  web:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    environment:
      APP_ENV: production
      SECRET_KEY: ${SECRET_KEY:?}

      DB_HOST: db
      DB_PORT: "5432"
      DB_NAME: ${POSTGRES_DB:?}
      DB_USER: ${POSTGRES_USER:?}
      DB_PASSWORD: ${POSTGRES_PASSWORD:?}

      UPLOAD_DIR: /app/uploads

      GUNICORN_WORKERS: ${GUNICORN_WORKERS:-2}
      GUNICORN_THREADS: ${GUNICORN_THREADS:-4}
      GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-120}
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - uploads_data:/app/uploads
    expose:
      - "8000"
    healthcheck:
      test:
        - CMD
        - python
        - -c
        - "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3)"
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    stop_grace_period: 30s

volumes:
  postgres_data:
  uploads_data:

Pontos importantes:

  • não use container_name;
  • não publique 5432:5432;
  • não publique 8000:8000;
  • o Coolify acessará o serviço web pela rede interna;
  • atribua o domínio ao serviço web e à porta interna 8000;
  • o arquivo Compose é a fonte de verdade desse deploy.

Variáveis no Coolify:

POSTGRES_MAJOR=17
POSTGRES_DB=appdb
POSTGRES_USER=app_user
POSTGRES_PASSWORD=SENHA_FORTE
SECRET_KEY=SEGREDO_FORTE

GUNICORN_WORKERS=2
GUNICORN_THREADS=4
GUNICORN_TIMEOUT=120

Troque POSTGRES_MAJOR pela versão escolhida após verificar a origem e as extensões.


10. Exporte o banco do Supabase

Para sair do Supabase e ir para PostgreSQL simples, exporte apenas os schemas da aplicação.

Não exporte automaticamente todos os schemas internos do Supabase.

10.1 Opção principal: pg_dump com schemas explícitos

Instale o cliente PostgreSQL com versão principal igual ou superior à versão do servidor Supabase.

Exporte o schema public em formato custom:

pg_dump `
  --dbname="$env:SUPABASE_DB_URL" `
  --format=custom `
  --file=".\migracao\app.dump" `
  --no-owner `
  --no-acl `
  --no-publications `
  --no-subscriptions `
  --schema=public

Se houver schemas adicionais:

pg_dump `
  --dbname="$env:SUPABASE_DB_URL" `
  --format=custom `
  --file=".\migracao\app.dump" `
  --no-owner `
  --no-acl `
  --no-publications `
  --no-subscriptions `
  --schema=public `
  --schema=app `
  --schema=cms

O formato custom permite:

  • restaurar seletivamente;
  • inspecionar o conteúdo;
  • usar pg_restore;
  • ignorar owners e permissões da origem.

Liste o conteúdo:

pg_restore --list .\migracao\app.dump |
  Out-File -Encoding utf8 .\migracao\app_dump_conteudo.txt

Gere checksum:

Get-FileHash .\migracao\app.dump -Algorithm SHA256

10.2 Alternativa: Supabase CLI

A CLI do Supabase aplica filtros específicos e exclui schemas gerenciados. Ela exige Docker para o comando db dump.

Schema:

supabase db dump `
  --db-url "$env:SUPABASE_DB_URL" `
  --schema public `
  --file ".\migracao\schema.sql"

Dados:

supabase db dump `
  --db-url "$env:SUPABASE_DB_URL" `
  --schema public `
  --data-only `
  --use-copy `
  --file ".\migracao\data.sql"

Com schemas adicionais:

--schema public,app,cms

Para PostgreSQL simples, normalmente não é necessário exportar as roles do Supabase.


11. Verifique dependências específicas do Supabase antes de restaurar

11.1 Procure RLS e roles do Supabase no dump

Para dump SQL:

rg -n "anon|authenticated|service_role|auth\.uid|auth\.jwt|storage\.|realtime" `
  .\migracao\schema.sql

Para dump custom, gere um SQL temporário para inspeção:

pg_restore `
  --file=".\migracao\app_para_inspecao.sql" `
  .\migracao\app.dump

Depois:

rg -n "anon|authenticated|service_role|auth\.uid|auth\.jwt|storage\.|realtime" `
  .\migracao\app_para_inspecao.sql

Se houver políticas como:

TO authenticated
USING (auth.uid() = user_id)

elas não funcionarão em um PostgreSQL comum sem a infraestrutura do Supabase.

Não crie roles falsas apenas para esconder o erro.

Escolha uma estratégia:

  1. mover as regras de autorização para o Flask;
  2. criar uma nova estratégia de RLS deliberadamente;
  3. hospedar Supabase Auth/PostgREST compatível;
  4. manter o Supabase temporariamente enquanto refatora.

11.2 Procure extensões não disponíveis

Compare as extensões da origem com as disponíveis no destino:

select name, default_version, installed_version
from pg_available_extensions
order by name;

Extensões comuns que podem ser ativadas:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;

Ative somente as realmente usadas.

Se usa vector, confirme que a imagem do PostgreSQL contém pgvector. A imagem oficial simples do PostgreSQL não inclui todas as extensões externas.

11.3 Procure chamadas Supabase no Python

rg -n "supabase\.table|supabase\.rpc|supabase\.auth|supabase\.storage" .

Exemplo que precisa ser reescrito:

response = supabase.table("noticias").select("*").execute()

Exemplo com SQLAlchemy:

from sqlalchemy import select

noticias = (
    db.session.execute(
        select(Noticia).order_by(Noticia.publicada_em.desc())
    )
    .scalars()
    .all()
)

Exemplo com Psycopg:

from psycopg.rows import dict_row

with connection.cursor(row_factory=dict_row) as cursor:
    cursor.execute(
        """
        select id, titulo, publicada_em
        from public.noticias
        order by publicada_em desc
        limit %s
        """,
        (50,),
    )
    noticias = cursor.fetchall()

Sempre use parâmetros. Não monte SQL concatenando texto recebido do usuário.


12. Envie o dump para a VPS

Crie a pasta:

mkdir -p /root/migracao
chmod 700 /root/migracao

Na máquina Windows:

scp .\migracao\app.dump root@IP_DA_VPS:/root/migracao/app.dump

Confira na VPS:

ls -lh /root/migracao/app.dump
sha256sum /root/migracao/app.dump

Compare o SHA-256 com o calculado no Windows.


13. Restaure no PostgreSQL do Coolify

13.1 Localize o container correto

Na VPS:

docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'

Haverá containers do próprio Coolify. Escolha cuidadosamente o PostgreSQL criado para a aplicação.

Não selecione o banco interno do Coolify.

13.2 Copie o dump para o container

docker cp \
  /root/migracao/app.dump \
  CONTAINER_POSTGRES_DA_APLICACAO:/tmp/app.dump

13.3 Ative extensões necessárias

Abra um terminal no PostgreSQL pelo Coolify ou execute:

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  psql -U app_user -d appdb

No psql:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;

Adicione somente as extensões usadas.

Saia:

\q

13.4 Restaure

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  pg_restore \
  --username=app_user \
  --dbname=appdb \
  --no-owner \
  --no-acl \
  --exit-on-error \
  /tmp/app.dump

Para repetir a restauração em um banco de teste já preenchido:

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  pg_restore \
  --username=app_user \
  --dbname=appdb \
  --clean \
  --if-exists \
  --no-owner \
  --no-acl \
  --exit-on-error \
  /tmp/app.dump

--clean é destrutivo. Use apenas no banco correto e quando souber que o conteúdo atual pode ser apagado.

13.5 Atualize estatísticas

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  psql -U app_user -d appdb -c "ANALYZE;"

13.6 Confira tabelas

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  psql -U app_user -d appdb -c "\dt public.*"

13.7 Compare contagens

docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
  psql -U app_user -d appdb -c "
select count(*) as total_usuarios from public.usuarios;
select count(*) as total_noticias from public.noticias;
select count(*) as total_arquivos from public.arquivos;
"

As contagens devem coincidir com a origem.


14. Adapte a autenticação

Caso A: não existe login

Nada precisa ser migrado.

Caso B: existe apenas um painel administrativo pequeno

O caminho mais simples costuma ser:

  1. criar uma tabela local de usuários administrativos;
  2. usar sessão Flask, Flask-Login ou JWT;
  3. criar novas senhas;
  4. forçar troca de senha no primeiro acesso;
  5. remover as chaves Supabase do projeto.

Caso C: existem muitos usuários com email e senha

O Supabase armazena hashes de senha com bcrypt na coluna:

auth.users.encrypted_password

Tecnicamente os hashes podem ser migrados, mas a operação precisa preservar:

  • UUID do usuário;
  • email;
  • status de confirmação;
  • metadados;
  • identities;
  • recuperação de senha;
  • bloqueios;
  • MFA;
  • provedores OAuth;
  • regras de sessão;
  • revogação de tokens.

Para uma aplicação Flask própria, faça uma migração planejada e auditada. Não copie apenas email e hash sem tratar o restante.

Alternativas seguras:

  1. manter Supabase Auth temporariamente;
  2. auto-hospedar o Auth/GoTrue;
  3. importar usuários e exigir novo login;
  4. exigir redefinição de senha;
  5. usar um provedor OIDC externo.

Caso D: OAuth, magic link, OTP ou MFA

Hospedar apenas PostgreSQL não substitui esses serviços. Será necessário configurar outro provedor ou hospedar o Supabase Auth.

Tokens existentes

Ao trocar o mecanismo de autenticação ou o segredo JWT, os tokens já emitidos deixam de ser válidos. Planeje que os usuários precisem entrar novamente.


15. Migre o Supabase Storage

Os arquivos do Supabase Storage não estão dentro do pg_dump da aplicação.

Você precisa migrar:

  1. arquivos físicos;
  2. caminhos usados pela aplicação;
  3. URLs armazenadas no banco;
  4. metadados necessários;
  5. regras de acesso.

15.1 Escolha o destino

Volume local na VPS

Bom para:

  • aplicação pequena;
  • poucos arquivos;
  • custo mínimo;
  • simplicidade.

Exemplo:

/app/uploads

Exige backup externo obrigatório.

Cloudflare R2 ou outro S3

Bom para:

  • muitos arquivos;
  • CDN;
  • crescimento;
  • separação entre aplicação e mídia;
  • backup mais simples.

MinIO na VPS

Bom para:

  • API compatível com S3;
  • controle próprio;
  • mais de uma aplicação.

Exige mais memória, operação e backup.

15.2 Migração com rclone, sem Node.js

No Supabase:

Storage
→ Configuration
→ S3
→ habilitar S3
→ gerar Access Key ID e Secret Access Key

Anote:

Endpoint
Region
Access Key ID
Secret Access Key

O endpoint normalmente termina com:

/storage/v1/s3

Configure:

rclone config

Crie um remote chamado:

supabase

Parâmetros principais:

Storage: s3
Provider: Other
Access Key ID: chave gerada
Secret Access Key: segredo gerado
Endpoint: endpoint S3 do projeto
Region: região do projeto

Teste:

rclone lsd supabase:

Liste o conteúdo de um bucket:

rclone lsf supabase:NOME_DO_BUCKET --recursive

Baixe preservando os caminhos:

rclone copy `
  supabase:NOME_DO_BUCKET `
  .\migracao\storage\NOME_DO_BUCKET `
  --progress `
  --transfers 4 `
  --checkers 8

Compare tamanho e quantidade:

rclone size supabase:NOME_DO_BUCKET
rclone size .\migracao\storage\NOME_DO_BUCKET

15.3 Envie os arquivos para a VPS

Compacte:

tar -czf .\migracao\storage.tar.gz -C .\migracao storage

Envie:

scp .\migracao\storage.tar.gz root@IP_DA_VPS:/root/migracao/

Na VPS:

cd /root/migracao
tar -xzf storage.tar.gz

15.4 Copie para o volume persistente da aplicação

Depois que o container web estiver criado:

docker cp \
  /root/migracao/storage/. \
  CONTAINER_DA_APLICACAO:/app/uploads/

Confirme:

docker exec -it CONTAINER_DA_APLICACAO \
  find /app/uploads -type f | head

O caminho /app/uploads precisa estar ligado a um volume persistente no Coolify ou no Compose. Caso contrário, os arquivos desaparecerão em um redeploy.

15.5 Atualize URLs antigas

Procure URLs do Supabase no banco:

select *
from public.arquivos
where url like '%supabase%';

Prefira armazenar no banco apenas uma chave relativa:

noticias/2026/07/imagem.webp

Em vez de:

https://projeto.supabase.co/storage/v1/object/public/...

A URL pública pode ser construída pela aplicação.

Antes de executar qualquer UPDATE, faça backup e teste em homologação.

Exemplo conceitual:

update public.arquivos
set url = replace(
  url,
  'https://PROJETO.supabase.co/storage/v1/object/public/imagens/',
  '/uploads/imagens/'
)
where url like 'https://PROJETO.supabase.co/storage/v1/object/public/imagens/%';

Adapte à estrutura real.


16. Publique a aplicação no Coolify

16.1 Com PostgreSQL nativo do Coolify

No projeto:

Production
→ Add New Resource
→ Application
→ selecione o repositório Git

Configuração:

Build Pack: Dockerfile
Dockerfile: ./Dockerfile
Porta interna: 8000

Cadastre as variáveis:

APP_ENV=production
SECRET_KEY=SEGREDO_FORTE

DB_HOST=HOST_INTERNO
DB_PORT=5432
DB_NAME=appdb
DB_USER=app_user
DB_PASSWORD=SENHA_FORTE

UPLOAD_DIR=/app/uploads

GUNICORN_WORKERS=2
GUNICORN_THREADS=4
GUNICORN_TIMEOUT=120

Gere uma SECRET_KEY:

python -c "import secrets; print(secrets.token_urlsafe(64))"

16.2 Volume persistente

No recurso da aplicação:

Persistent Storage
→ Add Volume
→ Destination Path: /app/uploads

O volume deve existir antes de copiar os arquivos definitivos.

16.3 Domínio

Atribua:

https://seudominio.com.br:8000

Nesse campo, 8000 informa ao Coolify a porta interna do container. O acesso público continuará em HTTPS na porta 443.

16.4 Health check

Configure:

Path: /health
Expected status: 200
Port: 8000

O Dockerfile também contém um HEALTHCHECK. Em uma pilha Compose, o health check deve estar no próprio compose.yaml ou no Dockerfile.

16.5 Deploy

Execute o deploy e acompanhe:

Deployments
Logs

Na VPS, se necessário:

docker ps
docker logs --tail 200 CONTAINER_DA_APLICACAO

Teste:

curl -i https://seudominio.com.br/health
curl -i https://seudominio.com.br/ready

17. Se usar Docker Compose no Coolify

No Coolify:

Production
→ Add New Resource
→ Docker Compose
→ repositório Git
→ compose.yaml

Depois que o Coolify carregar o arquivo:

  1. preencha todas as variáveis obrigatórias;
  2. atribua o domínio ao serviço web;
  3. informe a porta interna 8000;
  4. confirme os volumes;
  5. faça o deploy;
  6. restaure o banco;
  7. copie os uploads;
  8. teste /health e /ready.

Não adicione:

ports:
  - "5432:5432"

Não é necessário para a aplicação, pois web acessa:

db:5432

pela rede interna do Compose.


18. Migrações futuras do banco

Se o projeto usa Flask-Migrate/Alembic:

flask db upgrade

ou:

alembic upgrade head

Durante a migração inicial:

  • não execute migrations automaticamente antes de restaurar o dump;
  • primeiro restaure a cópia do banco;
  • valide a versão das migrations;
  • só depois configure o comando de pós-deploy.

Para os próximos deploys, o Coolify pode executar um comando de pós-deploy, por exemplo:

flask db upgrade

Faça isso apenas quando as migrations forem idempotentes e estiverem testadas.


19. Estratégia segura de virada

19.1 Faça um ensaio completo

Antes da produção:

[ ] criar PostgreSQL de teste
[ ] restaurar dump
[ ] publicar aplicação em domínio provisório
[ ] migrar uma cópia dos arquivos
[ ] testar login
[ ] testar leitura e gravação
[ ] testar upload
[ ] testar tarefas agendadas
[ ] testar envio de email
[ ] testar integrações
[ ] comparar registros
[ ] testar backup e restauração

19.2 Virada final

Sequência recomendada:

  1. reduzir TTL do DNS;
  2. publicar a aplicação nova em domínio provisório;
  3. validar tudo;
  4. colocar a aplicação antiga em manutenção ou bloquear gravações;
  5. gerar o dump final;
  6. copiar o delta final dos arquivos;
  7. limpar o banco de destino de teste, se necessário;
  8. restaurar o dump final;
  9. comparar contagens;
  10. executar testes de fumaça;
  11. alterar DNS;
  12. acompanhar logs e métricas;
  13. manter o Supabase disponível como fallback por alguns dias;
  14. cancelar somente depois de confirmar a migração.

19.3 Evite gravação simultânea

Depois do dump final, não permita que usuários gravem no Supabase antigo enquanto outros já gravam no PostgreSQL novo.

Caso contrário, os dois bancos divergem.

19.4 Rollback

Defina antes da virada:

[ ] como voltar o DNS
[ ] como voltar as variáveis antigas
[ ] quem pode decidir o rollback
[ ] até que momento o rollback é simples
[ ] como tratar dados gravados no ambiente novo

Se houver gravações no ambiente novo, voltar apenas o DNS pode causar perda de dados. Nesse caso será necessário reconciliar registros.


20. Backups obrigatórios

20.1 Backup do PostgreSQL no Coolify

O Coolify permite agendar backups de PostgreSQL em armazenamento S3 compatível.

Sugestão inicial:

Backup diário
Retenção diária: 7 a 14 cópias
Backup semanal: 4 a 8 cópias
Backup mensal: conforme necessidade
Destino: Cloudflare R2, S3 ou outro storage fora da VPS

O comando lógico usado para PostgreSQL é baseado em:

pg_dump --format=custom --no-acl --no-owner

20.2 Backup manual

mkdir -p /root/backups
chmod 700 /root/backups

Exemplo:

docker exec CONTAINER_POSTGRES_DA_APLICACAO \
  pg_dump \
  --username=app_user \
  --dbname=appdb \
  --format=custom \
  --no-owner \
  --no-acl \
  > "/root/backups/appdb_$(date +%F_%H-%M-%S).dump"

Confira:

ls -lh /root/backups

20.3 Backup dos uploads

O backup da configuração do Coolify não substitui o backup dos volumes da aplicação.

Exemplo com rclone para destino remoto:

rclone sync \
  /CAMINHO_REAL_DO_VOLUME_DE_UPLOADS \
  remoto:backups/minha-app/uploads \
  --progress

20.4 Teste a restauração

Um backup só é confiável depois de uma restauração testada.

Periodicamente:

  1. crie um banco temporário;
  2. restaure o último dump;
  3. confira tabelas;
  4. compare contagens;
  5. execute consultas críticas;
  6. valide os uploads.

21. Segurança mínima

[ ] Flask debug desativado
[ ] servidor de produção Gunicorn
[ ] PostgreSQL sem porta pública
[ ] SECRET_KEY forte
[ ] cookies Secure, HttpOnly e SameSite
[ ] HTTPS obrigatório
[ ] ProxyFix configurado conscientemente
[ ] service_role do Supabase removida
[ ] segredos fora do Git
[ ] credenciais antigas rotacionadas
[ ] SSH por chave
[ ] painel Coolify protegido
[ ] firewall validado
[ ] backups fora da VPS
[ ] dependências atualizadas
[ ] logs sem senhas ou tokens
[ ] upload com limite de tamanho
[ ] nomes de arquivo tratados
[ ] tipos MIME validados
[ ] CORS restrito
[ ] autenticação e autorização testadas

Não use em produção:

app.run(debug=True)

Não coloque a service_role do Supabase em código frontend.

Depois da migração, rotacione:

senha do banco
chaves Supabase
service_role
anon key, quando não for mais usada
SECRET_KEY
chaves S3
tokens SMTP/OAuth

22. Diagnóstico de problemas comuns

pg_dump: server version mismatch

Causa:

o cliente pg_dump é mais antigo que o servidor Supabase

Solução:

instale um cliente PostgreSQL de versão igual ou superior

role "authenticated" does not exist

Causa:

políticas RLS ou grants ainda dependem das roles do Supabase

Solução:

  • não crie roles fictícias apenas para ocultar o erro;
  • reimplemente a autorização no Flask;
  • remova/adapte políticas;
  • ou hospede a pilha Supabase compatível.

function auth.uid() does not exist

Causa:

a função pertence ao modelo de autenticação Supabase

Solução:

mover a regra para o Flask ou manter Auth/PostgREST compatível

connection refused

Verifique:

DB_HOST não deve ser localhost quando PostgreSQL está em outro container
DB_HOST deve ser o hostname interno correto
PostgreSQL deve estar saudável
aplicação e banco precisam compartilhar uma rede acessível

No Compose deste tutorial:

DB_HOST=db

could not translate host name

Causa provável:

hostname interno incorreto ou recursos em redes diferentes

password authentication failed

Verifique:

usuário
senha
nome do banco
caracteres especiais
variáveis do Coolify

Ao usar DB_PASSWORD separadamente, você evita erros de percent-encoding na URL.

relation does not exist

Verifique:

show search_path;

E confirme:

select table_schema, table_name
from information_schema.tables
where table_name = 'NOME_DA_TABELA';

Talvez o código precise usar:

public.noticias

extension ... is not available

A imagem PostgreSQL do destino não contém a extensão.

Opções:

  1. usar imagem que inclua a extensão;
  2. instalar a extensão;
  3. remover a dependência;
  4. escolher outra implementação.

Coolify mostra 404, No available server ou 502

Verifique:

Gunicorn está ouvindo em 0.0.0.0:8000
domínio aponta para a porta interna 8000
/health responde 200
container está saudável
Dockerfile usa o ponto de entrada correto

Logs:

docker logs --tail 200 CONTAINER_DA_APLICACAO

Gunicorn: Failed to find attribute 'app'

O ponto de entrada está errado.

Exemplos:

app.py + variável app         → app:app
main.py + variável app        → main:app
wsgi.py + variável app        → wsgi:app
factory create_app            → pacote:create_app()

Uploads desaparecem após deploy

Causa:

/app/uploads não está em volume persistente

Solução:

criar volume no Coolify ou no compose.yaml

Redirecionamento infinito para HTTPS

Verifique:

ProxyFix
X-Forwarded-Proto
Cloudflare SSL mode
Force HTTPS do Coolify

Na Cloudflare, evite configurações que façam HTTPS no navegador e HTTP forçado de forma incompatível até validar o proxy.

Aplicação ainda tenta acessar Supabase

Procure:

rg -n "supabase\.co|SUPABASE_|create_client|supabase\." .

Verifique também:

variáveis no Coolify
JavaScript gerado
templates HTML
configuração frontend
tarefas em background
webhooks
URLs no banco

23. Checklist final

Aplicação

[ ] framework confirmado
[ ] ponto de entrada confirmado
[ ] Dockerfile testado
[ ] Gunicorn funcionando
[ ] debug desativado
[ ] /health funcionando
[ ] /ready funcionando
[ ] logs no stdout/stderr

Banco

[ ] versão identificada
[ ] extensões identificadas
[ ] schemas identificados
[ ] dump criado
[ ] checksum validado
[ ] restauração concluída
[ ] contagens comparadas
[ ] sequences validadas
[ ] funções e triggers testadas
[ ] RLS revisada
[ ] PostgreSQL sem exposição pública

Supabase

[ ] uso de supabase-py revisado
[ ] Auth tratado
[ ] Storage migrado
[ ] URLs atualizadas
[ ] Realtime tratado
[ ] Edge Functions tratadas
[ ] service_role removida
[ ] tokens antigos rotacionados

Coolify e VPS

[ ] domínio configurado
[ ] HTTPS válido
[ ] health check ativo
[ ] volume persistente ativo
[ ] backups agendados
[ ] backup externo configurado
[ ] firewall validado
[ ] SSH por chave
[ ] painel Coolify protegido

Virada

[ ] ensaio completo realizado
[ ] janela de manutenção definida
[ ] dump final gerado
[ ] delta de arquivos copiado
[ ] DNS alterado
[ ] logs acompanhados
[ ] rollback documentado
[ ] Supabase mantido temporariamente como fallback

24. Informações necessárias para adaptar este tutorial ao projeto real

Para transformar os exemplos em comandos exatos, levante:

1. Estrutura de pastas do projeto
2. requirements.txt ou pyproject.toml
3. arquivo que cria a aplicação Flask
4. ponto de entrada atual
5. uso de SQLAlchemy, Psycopg ou supabase-py
6. lista de schemas
7. lista de tabelas
8. versão do PostgreSQL
9. extensões instaladas
10. uso de Supabase Auth
11. uso de Storage e nomes dos buckets
12. uso de RLS
13. uso de Realtime
14. uso de Edge Functions
15. tamanho do banco
16. tamanho total dos arquivos
17. domínio escolhido
18. repositório Git utilizado pelo Coolify

Com essas informações, é possível gerar:

Dockerfile definitivo
compose.yaml definitivo
config.py definitivo
comandos exatos de dump e restore
plano de migração do Auth
script de migração do Storage
script de atualização das URLs
checklist específico de homologação e produção

25. Fontes oficiais consultadas


26. Resumo da ordem prática

1. Confirmar que é Flask
2. Identificar todas as dependências do Supabase
3. Criar branch de migração
4. Preparar Dockerfile e Gunicorn
5. Criar VPS Hostinger com Coolify
6. Configurar DNS e HTTPS
7. Criar PostgreSQL privado
8. Exportar somente os schemas da aplicação
9. Revisar RLS, roles e extensões
10. Restaurar banco de teste
11. Reescrever acessos supabase-py
12. Migrar Auth, se usado
13. Migrar Storage, se usado
14. Publicar a aplicação
15. Testar em domínio provisório
16. Fazer dump final com sistema em manutenção
17. Restaurar o dump final
18. Copiar o delta de arquivos
19. Virar DNS
20. Monitorar
21. Configurar e testar backups
22. Desativar o Supabase somente depois da validação
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment