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.
Há uma diferença importante entre:
- usar o Supabase apenas como PostgreSQL;
- usar o cliente
supabase-pypara consultar tabelas; - usar Supabase Auth;
- usar Supabase Storage;
- usar Realtime;
- usar Edge Functions;
- usar RLS,
auth.uid(),auth.jwt()ou roles comoanoneauthenticated.
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.
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 appOutros 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.
Procure:
Get-ChildItem -Recurse -File -Include app.py,main.py,wsgi.py,run.pyOs 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.
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
| 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-pynão se conecta diretamente a um PostgreSQL comum. Ele consome APIs do Supabase, como PostgREST, Auth e Storage. Se o código usasupabase.table(...), não basta trocar a URL: será necessário usar SQLAlchemy, Psycopg ou hospedar a pilha Supabase completa.
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
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()eauth.jwt().
Essa opção exige mais memória e inclui vários serviços. Não é o caminho principal deste tutorial.
Crie uma pasta de migração:
New-Item -ItemType Directory -Force .\migracaoRegistre:
[ ] 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
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.
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.
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
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.
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;
"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.
Crie uma branch:
git switch -c migracao-hostinger-coolifyConfirme 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=120Nunca coloque no Git:
SUPABASE_SERVICE_ROLE_KEY
senha do PostgreSQL
SECRET_KEY real
chaves S3
credenciais SMTP
tokens OAuth
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.
Na Hostinger, prefira:
Ubuntu 24.04 with Coolify
No template atual, o Coolify já vem instalado.
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 porta8000para acessar o painel, a menos que a sua instalação específica tenha sido configurada de outra forma.
Entre por SSH:
ssh root@IP_DA_VPSAtualize:
apt update
apt upgrade -yDefina o fuso horário:
timedatectl set-timezone America/Sao_PauloConfira:
timedatectlNo 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.
Na sua máquina:
ssh-keygen -t ed25519 -a 100Copie a chave pública para a VPS e valide o login antes de desabilitar login por senha.
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:
- deixe inicialmente em DNS only;
- configure o domínio e o certificado no Coolify;
- teste HTTPS;
- depois ative o proxy da Cloudflare, se desejar.
O servidor de desenvolvimento do Flask não deve ser usado em produção. Use Gunicorn atrás do proxy do Coolify.
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,<2Não substitua cegamente o requirements.txt existente. Apenas adicione o que estiver faltando.
Instale localmente:
python -m pip install -r requirements.txtCrie 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
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 appSe 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.
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 appCrie 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.
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()
.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.
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-flaskTeste:
curl.exe http://localhost:8000/healthResposta esperada:
{"status":"ok"}Há dois modos possíveis.
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:
- copie a conexão interna mostrada pelo Coolify;
- ou anote host interno, porta, banco, usuário e senha;
- 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_FORTEVantagens 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.
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
webpela rede interna; - atribua o domínio ao serviço
webe à porta interna8000; - 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=120Troque POSTGRES_MAJOR pela versão escolhida após verificar a origem e as extensões.
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.
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=publicSe 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=cmsO 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.txtGere checksum:
Get-FileHash .\migracao\app.dump -Algorithm SHA256A 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,cmsPara PostgreSQL simples, normalmente não é necessário exportar as roles do Supabase.
Para dump SQL:
rg -n "anon|authenticated|service_role|auth\.uid|auth\.jwt|storage\.|realtime" `
.\migracao\schema.sqlPara dump custom, gere um SQL temporário para inspeção:
pg_restore `
--file=".\migracao\app_para_inspecao.sql" `
.\migracao\app.dumpDepois:
rg -n "anon|authenticated|service_role|auth\.uid|auth\.jwt|storage\.|realtime" `
.\migracao\app_para_inspecao.sqlSe 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:
- mover as regras de autorização para o Flask;
- criar uma nova estratégia de RLS deliberadamente;
- hospedar Supabase Auth/PostgREST compatível;
- manter o Supabase temporariamente enquanto refatora.
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.
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.
Crie a pasta:
mkdir -p /root/migracao
chmod 700 /root/migracaoNa máquina Windows:
scp .\migracao\app.dump root@IP_DA_VPS:/root/migracao/app.dumpConfira na VPS:
ls -lh /root/migracao/app.dump
sha256sum /root/migracao/app.dumpCompare o SHA-256 com o calculado no Windows.
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.
docker cp \
/root/migracao/app.dump \
CONTAINER_POSTGRES_DA_APLICACAO:/tmp/app.dumpAbra um terminal no PostgreSQL pelo Coolify ou execute:
docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
psql -U app_user -d appdbNo psql:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;Adicione somente as extensões usadas.
Saia:
\q
docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
pg_restore \
--username=app_user \
--dbname=appdb \
--no-owner \
--no-acl \
--exit-on-error \
/tmp/app.dumpPara 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.
docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
psql -U app_user -d appdb -c "ANALYZE;"docker exec -it CONTAINER_POSTGRES_DA_APLICACAO \
psql -U app_user -d appdb -c "\dt public.*"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.
Nada precisa ser migrado.
O caminho mais simples costuma ser:
- criar uma tabela local de usuários administrativos;
- usar sessão Flask, Flask-Login ou JWT;
- criar novas senhas;
- forçar troca de senha no primeiro acesso;
- remover as chaves Supabase do projeto.
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:
- manter Supabase Auth temporariamente;
- auto-hospedar o Auth/GoTrue;
- importar usuários e exigir novo login;
- exigir redefinição de senha;
- usar um provedor OIDC externo.
Hospedar apenas PostgreSQL não substitui esses serviços. Será necessário configurar outro provedor ou hospedar o Supabase Auth.
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.
Os arquivos do Supabase Storage não estão dentro do pg_dump da aplicação.
Você precisa migrar:
- arquivos físicos;
- caminhos usados pela aplicação;
- URLs armazenadas no banco;
- metadados necessários;
- regras de acesso.
Bom para:
- aplicação pequena;
- poucos arquivos;
- custo mínimo;
- simplicidade.
Exemplo:
/app/uploads
Exige backup externo obrigatório.
Bom para:
- muitos arquivos;
- CDN;
- crescimento;
- separação entre aplicação e mídia;
- backup mais simples.
Bom para:
- API compatível com S3;
- controle próprio;
- mais de uma aplicação.
Exige mais memória, operação e backup.
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 configCrie 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 --recursiveBaixe preservando os caminhos:
rclone copy `
supabase:NOME_DO_BUCKET `
.\migracao\storage\NOME_DO_BUCKET `
--progress `
--transfers 4 `
--checkers 8Compare tamanho e quantidade:
rclone size supabase:NOME_DO_BUCKET
rclone size .\migracao\storage\NOME_DO_BUCKETCompacte:
tar -czf .\migracao\storage.tar.gz -C .\migracao storageEnvie:
scp .\migracao\storage.tar.gz root@IP_DA_VPS:/root/migracao/Na VPS:
cd /root/migracao
tar -xzf storage.tar.gzDepois 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 | headO 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.
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.
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=120Gere uma SECRET_KEY:
python -c "import secrets; print(secrets.token_urlsafe(64))"No recurso da aplicação:
Persistent Storage
→ Add Volume
→ Destination Path: /app/uploads
O volume deve existir antes de copiar os arquivos definitivos.
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.
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.
Execute o deploy e acompanhe:
Deployments
Logs
Na VPS, se necessário:
docker ps
docker logs --tail 200 CONTAINER_DA_APLICACAOTeste:
curl -i https://seudominio.com.br/health
curl -i https://seudominio.com.br/readyNo Coolify:
Production
→ Add New Resource
→ Docker Compose
→ repositório Git
→ compose.yaml
Depois que o Coolify carregar o arquivo:
- preencha todas as variáveis obrigatórias;
- atribua o domínio ao serviço
web; - informe a porta interna 8000;
- confirme os volumes;
- faça o deploy;
- restaure o banco;
- copie os uploads;
- teste
/healthe/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.
Se o projeto usa Flask-Migrate/Alembic:
flask db upgradeou:
alembic upgrade headDurante 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 upgradeFaça isso apenas quando as migrations forem idempotentes e estiverem testadas.
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
Sequência recomendada:
- reduzir TTL do DNS;
- publicar a aplicação nova em domínio provisório;
- validar tudo;
- colocar a aplicação antiga em manutenção ou bloquear gravações;
- gerar o dump final;
- copiar o delta final dos arquivos;
- limpar o banco de destino de teste, se necessário;
- restaurar o dump final;
- comparar contagens;
- executar testes de fumaça;
- alterar DNS;
- acompanhar logs e métricas;
- manter o Supabase disponível como fallback por alguns dias;
- cancelar somente depois de confirmar a migração.
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.
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.
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-ownermkdir -p /root/backups
chmod 700 /root/backupsExemplo:
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/backupsO 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 \
--progressUm backup só é confiável depois de uma restauração testada.
Periodicamente:
- crie um banco temporário;
- restaure o último dump;
- confira tabelas;
- compare contagens;
- execute consultas críticas;
- valide os uploads.
[ ] 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
Causa:
o cliente pg_dump é mais antigo que o servidor Supabase
Solução:
instale um cliente PostgreSQL de versão igual ou superior
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.
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
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=dbCausa provável:
hostname interno incorreto ou recursos em redes diferentes
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.
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
A imagem PostgreSQL do destino não contém a extensão.
Opções:
- usar imagem que inclua a extensão;
- instalar a extensão;
- remover a dependência;
- escolher outra implementação.
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_APLICACAOO 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()
Causa:
/app/uploads não está em volume persistente
Solução:
criar volume no Coolify ou no compose.yaml
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.
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
[ ] framework confirmado
[ ] ponto de entrada confirmado
[ ] Dockerfile testado
[ ] Gunicorn funcionando
[ ] debug desativado
[ ] /health funcionando
[ ] /ready funcionando
[ ] logs no stdout/stderr
[ ] 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
[ ] uso de supabase-py revisado
[ ] Auth tratado
[ ] Storage migrado
[ ] URLs atualizadas
[ ] Realtime tratado
[ ] Edge Functions tratadas
[ ] service_role removida
[ ] tokens antigos rotacionados
[ ] 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
[ ] 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
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
- Hostinger — How to use the Coolify VPS template
- Coolify — Docker Compose
- Coolify — Persistent Storage
- Coolify — Health checks
- Coolify — Database Backups
- Supabase — Restore a Platform Project to Self-Hosted
- Supabase — CLI Reference: db dump
- Supabase — Copy Storage Objects from Platform
- Supabase — Download Storage Objects
- Supabase — Password security
- Flask — Deploying to Production
- Flask — Gunicorn
- Flask — Tell Flask it is Behind a Proxy
- PostgreSQL — pg_dump
- PostgreSQL — pg_restore
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