Ir para o conteúdo
Ferramenta para Plataforma1523 palavras

Supabase com Docker: local e produção com docker compose

Introdução

Se você já trabalhou com Supabase, provavelmente começou pelo plano gratuito na nuvem. É o caminho natural: criar um projeto, pegar a URL e a chave anon, e sair codando. Mas chega um momento em que você precisa de mais controle. Talvez queira rodar testes de integração sem depender da internet. Talvez precise customizar o esquema de autenticação. Ou talvez sua empresa exija que os dados fiquem em infraestrutura própria. É nesse ponto que Docker e Supabase se encontram.

O Supabase é, no fundo, um conjunto de ferramentas open source orquestradas: PostgreSQL, GoTrue (autenticação), PostgREST (API REST automática), Realtime (WebSockets), Storage (armazenamento de arquivos) e Kong (API gateway). Cada um desses componentes roda em seu próprio container Docker. Quando você entende essa arquitetura, ganha a capacidade de personalizar, depurar e escalar cada peça de forma independente.

Neste guia, vou mostrar como configurar o Supabase localmente com Docker, como integrar sua aplicação TypeScript a essa infraestrutura, e quais práticas de produção fazem diferença real no dia a dia de quem opera sistemas baseados nessa stack.

Se você já tem familiaridade com conceitos de Docker e CI/CD, recomendo também o post DevOps para Devs: Docker, CI/CD, Kubernetes e AWS em Produção como leitura complementar.

Por que usar Docker com Supabase

Controle total sobre o ambiente

O Supabase Cloud abstrai toda a infraestrutura. Isso é excelente para começar, mas se torna limitante quando você precisa, por exemplo, instalar extensões PostgreSQL específicas, configurar políticas de rede customizadas ou integrar com serviços internos que não estão expostos à internet pública.

Com Docker, cada componente do Supabase vira um container que você controla: versão, variáveis de ambiente, volumes de dados, rede. Você decide tudo.

Paridade entre desenvolvimento e produção

Um dos problemas mais clássicos em engenharia de software é o "na minha máquina funciona". Quando seu ambiente local roda exatamente a mesma stack que a produção (PostgreSQL 15 com as mesmas extensões, mesma versão do GoTrue, mesmo Kong), essa classe inteira de bugs simplesmente desaparece.

Casos de uso reais onde essa combinação brilha

Existem cenários em que rodar Supabase com Docker não é apenas conveniente, mas essencial:

  1. Testes de integração em pipelines CI/CD: você sobe toda a stack com docker compose, roda os testes contra um banco real e derruba tudo ao final. Sem custos, sem efeitos colaterais.

  2. Ambientes regulados (saúde, financeiro): empresas que precisam manter dados em infraestrutura própria por questões de compliance podem usar o Supabase self-hosted.

  3. Desenvolvimento offline: times que trabalham em contextos com conectividade limitada (consultorias em campo, por exemplo) mantêm produtividade total.

  4. Multi-tenancy com isolamento real: cada cliente pode ter sua própria instância Supabase rodando em containers separados, com bancos de dados completamente isolados.

Setup do Supabase com Docker

Pré-requisitos

Antes de começar, certifique-se de ter instalado:

  • Docker Engine 20.10 ou superior
  • Docker Compose v2
  • Git
  • Node.js 18+ (para os exemplos em TypeScript)

Clonando e configurando o repositório

O Supabase mantém um repositório oficial com todos os arquivos de configuração para self-hosting:

Bash
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/docker
cp .env.example .env

Configurando variáveis de ambiente

O arquivo .env contém todas as configurações sensíveis. Nunca use os valores padrão em produção. Aqui estão as variáveis mais críticas que você precisa alterar:

Bash
# .env
 
# Senhas e chaves (OBRIGATÓRIO trocar em produção)
POSTGRES_PASSWORD=sua-senha-forte-aqui-minimo-32-caracteres
JWT_SECRET=seu-jwt-secret-com-pelo-menos-32-caracteres
ANON_KEY=gere-com-supabase-cli-ou-jwt-io
SERVICE_ROLE_KEY=gere-com-supabase-cli-ou-jwt-io
 
# Configuração do site
SITE_URL=http://localhost:3000
API_EXTERNAL_URL=http://localhost:8000
 
# SMTP para emails de autenticação
SMTP_HOST=smtp.seuservidor.com
SMTP_PORT=587
SMTP_USER=[email protected]
SMTP_PASS=senha-do-email
SMTP_SENDER_NAME=SuaApp
SMTP_ADMIN_EMAIL=[email protected]
 
# Configurações do Studio (dashboard web)
STUDIO_DEFAULT_ORGANIZATION=SuaOrganizacao
STUDIO_DEFAULT_PROJECT=SeuProjeto
STUDIO_PORT=3000

Para gerar as chaves JWT corretamente, você pode usar o seguinte comando com Node.js:

Bash
npx @supabase/cli@latest init

Ou gerar manualmente no site jwt.io usando o payload correto com a role anon ou service_role.

Subindo a stack completa

Com o arquivo .env configurado, basta executar:

Bash
docker compose up -d

Esse comando vai iniciar todos os serviços. Você pode verificar o status com:

Bash
docker compose ps

A saída esperada mostra os seguintes containers rodando: supabase-db, supabase-auth, supabase-rest, supabase-realtime, supabase-storage, supabase-kong, supabase-studio e supabase-meta.

Para verificar os logs de um serviço específico:

Bash
docker compose logs -f supabase-db
docker compose logs -f supabase-auth

Após tudo estar rodando, acesse o Studio (dashboard visual) em http://localhost:3000.

Exemplo prático: aplicação TypeScript conectada ao Supabase local

Primeiro exemplo: CRUD com client tipado

Vamos criar uma aplicação que se conecta ao Supabase rodando em Docker e realiza operações básicas com tipagem completa.

Primeiro, instale as dependências:

Bash
npm init -y
npm install @supabase/supabase-js
npm install -D typescript @types/node tsx

Agora crie o arquivo de tipos e o client:

TYPESCRIPT
// src/database.types.ts
export interface Database {
  public: {
    Tables: {
      produtos: {
        Row: {
          id: number;
          nome: string;
          preco: number;
          estoque: number;
          criado_em: string;
        };
        Insert: {
          nome: string;
          preco: number;
          estoque: number;
        };
        Update: {
          nome?: string;
          preco?: number;
          estoque?: number;
        };
      };
    };
  };
}
 
// src/supabase-client.ts
import { createClient } from "@supabase/supabase-js";
import type { Database } from "./database.types";
 
const SUPABASE_URL = process.env.SUPABASE_URL || "http://localhost:8000";
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "sua-anon-key";
 
export const supabase = createClient<Database>(
  SUPABASE_URL,
  SUPABASE_ANON_KEY
);
 
// src/produtos.ts
import { supabase } from "./supabase-client";
import type { Database } from "./database.types";
 
type ProdutoInsert = Database["public"]["Tables"]["produtos"]["Insert"];
 
export async function criarProduto(produto: ProdutoInsert) {
  const { data, error } = await supabase
    .from("produtos")
    .insert(produto)
    .select()
    .single();
 
  if (error) {
    console.error("Erro ao criar produto:", error.message);
    throw error;
  }
 
  console.log("Produto criado:", data);
  return data;
}
 
export async function listarProdutosEmEstoque(estoqueMinimo: number = 1) {
  const { data, error } = await supabase
    .from("produtos")
    .select("id, nome, preco, estoque")
    .gte("estoque", estoqueMinimo)
    .order("preco", { ascending: true });
 
  if (error) {
    console.error("Erro ao listar produtos:", error.message);
    throw error;
  }
 
  console.log(`Encontrados ${data.length} produtos com estoque >= ${estoqueMinimo}`);
  return data;
}
 
export async function atualizarEstoque(produtoId: number, novoEstoque: number) {
  const { data, error } = await supabase
    .from("produtos")
    .update({ estoque: novoEstoque })
    .eq("id", produtoId)
    .select()
    .single();
 
  if (error) {
    console.error("Erro ao atualizar estoque:", error.message);
    throw error;
  }
 
  console.log(`Estoque do produto ${produtoId} atualizado para ${novoEstoque}`);
  return data;
}
 
// Execução principal
async function main() {
  const novoProduto = await criarProduto({
    nome: "Teclado Mecânico",
    preco: 349.90,
    estoque: 50,
  });
 
  await atualizarEstoque(novoProduto.id, 48);
  await listarProdutosEmEstoque(10);
}
 
main().catch(console.error);

Para executar: npx tsx src/produtos.ts.

Segundo exemplo: health check automatizado da stack Docker

Este exemplo é útil em ambientes de produção e pipelines de CI/CD. Ele verifica se todos os serviços do Supabase estão respondendo corretamente:

TYPESCRIPT
// src/health-check.ts
import { createClient } from "@supabase/supabase-js";
 
interface HealthStatus {
  servico: string;
  status: "ok" | "falha";
  latenciaMs: number;
  detalhes?: string;
}
 
const SUPABASE_URL = process.env.SUPABASE_URL || "http://localhost:8000";
const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY || "sua-service-key";
 
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY);
 
async function verificarPostgres(): Promise<HealthStatus> {
  const inicio = Date.now();
  try {
    const { error } = await supabase.rpc("version" as never);
    if (error) {
      const { error: fallbackError } = await supabase
        .from("_health_check")
        .select("*")
        .limit(1);
 
      // Se o erro for "relation does not exist", o banco está ok
      // apenas a tabela não existe
      const estaFuncionando =
        !fallbackError || fallbackError.message.includes("does not exist");
 
      return {
        servico: "PostgreSQL (via PostgREST)",
        status: estaFuncionando ? "ok" : "falha",
        latenciaMs: Date.now() - inicio,
        detalhes: estaFuncionando
          ? "Banco respondendo normalmente"
          : fallbackError?.message,
      };
    }
    return {
      servico: "PostgreSQL (via PostgREST)",
      status: "ok",
      latenciaMs: Date.now() - inicio,
    };
  } catch (err) {
    return {
      servico: "PostgreSQL (via PostgREST)",
      status: "falha",
      latenciaMs: Date.now() - inicio,
      detalhes: err instanceof Error ? err.message : "Erro desconhecido",
    };
  }
}
 
async function verificarAuth(): Promise<HealthStatus> {
  const inicio = Date.now();
  try {
    const response = await fetch(`${SUPABASE_URL}/auth/v1/health`, {
      headers: {
        apikey: SUPABASE_SERVICE_KEY,
      },
    });
    return {
      servico: "GoTrue (Auth)",
      status: response.ok ? "ok" : "falha",
      latenciaMs: Date.now() - inicio,
      detalhes: response.ok
        ? "Serviço de autenticação operacional"
        : `HTTP ${response.status}`,
    };
  } catch (err) {
    return {
      servico: "GoTrue (Auth)",
      status: "falha",
      latenciaMs: Date.now() - inicio,
      detalhes: err instanceof Error ? err.message : "Erro desconhecido",
    };
  }
}
 
async function verificarStorage(): Promise<HealthStatus> {
  const inicio = Date.now();
  try {
    const { data, error } = await supabase.storage.listBuckets();
    return {
      servico: "Storage",
      status: error ? "falha" : "ok",
      latenciaMs: Date.now() - inicio,
      detalhes: error
        ? error.message
        : `${data?.length || 0} bucket(s) encontrado(s)`,
    };
  } catch (err) {
    return {
      servico: "Storage",
      status: "falha",
      latenciaMs: Date.now() - inicio,
      detalhes: err instanceof Error ? err.message : "Erro desconhecido",
    };
  }
}
 
async function executarHealthCheck(): Promise<void> {
  console.log("Iniciando health check da stack Supabase...\n");
  console.log(`URL alvo: ${SUPABASE_URL}\n`);
 
  const verificacoes = await Promise.all([
    verificarPostgres(),
    verificarAuth(),
    verificarStorage(),
  ]);
 
  for (const resultado of verificacoes) {
    const indicador = resultado.status === "ok" ? "[OK]" : "[FALHA]";
    console.log(
      `${indicador} ${resultado.servico} (${resultado.latenciaMs}ms)`
    );
    if (resultado.detalhes) {
      console.log(`      ${resultado.detalhes}`);
    }
  }
 
  const falhas = verificacoes.filter((v) => v.status === "falha");
  console.log(
    `\nResultado: ${verificacoes.length - falhas.length}/${verificacoes.length} serviços operacionais`
  );
 
  if (falhas.length > 0) {
    console.error(`\nATENÇÃO: ${falhas.length} serviço(s) com falha.`);
    process.exit(1);
  }
}
 
executarHealthCheck();

Execute com npx tsx src/health-check.ts. Em uma pipeline CI/CD, o process.exit(1) faz o job falhar caso algum serviço esteja fora do ar.

Boas práticas para Docker com Supabase em produção

Segurança das credenciais

Nunca versione o arquivo .env com sen

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias