← Blog

Fazendo deploy de Flask na DigitalOcean

Railyard

22 de julho de 2026 · 7 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

flask run é um servidor de desenvolvimento por design, os próprios docs do Flask dizem isso diretamente, e não é feito pra servir tráfego real. Colocar uma aplicação Flask em produção significa escolher um servidor WSGI, apontar ele pro módulo certo, provisionar um banco de dados, e decidir como as migrations rodam, decisões que um framework minimalista como o Flask deliberadamente deixa pra aplicação em vez de assumir uma única convenção do jeito que um framework maior poderia fazer. Este guia faz tudo isso em Docker num Droplet da DigitalOcean, com o Caddy cuidando do HTTPS, e termina com uma aplicação Flask no seu próprio domínio que você sabe como atualizar.

O que você vai precisar

  • Um Droplet da DigitalOcean. O plano Basic com 2 GB de memória, 1 vCPU, 50 GB de disco (US$12/mês) roda uma única aplicação Flask com o Postgres ao lado com conforto; o nível de 1 GB (US$6/mês) funciona pra uma aplicação de baixo tráfego sem muita folga de sobra.
  • Um domínio, com o DNS já apontado pro Droplet antes de solicitar o HTTPS.
  • Uma conta DigitalOcean.
  • Uma aplicação Flask num repositório Git. Este guia usa o Flask 3.1.3, que exige Python 3.9 ou mais novo; o Dockerfile abaixo fixa o Python 3.13.
  • Docker e o plugin do Compose, localmente pra testar o build primeiro ou direto no Droplet.

Passo 1: criar o Droplet

No painel de controle da DigitalOcean, Create → Droplets: escolha Ubuntu 24.04, a região mais próxima dos seus usuários, o plano Basic de 2 GB/1 vCPU, adicione sua chave SSH, e crie ele.

ssh root@your-droplet-ip

Passo 2: instalar o Docker

curl -fsSL https://get.docker.com | sh
docker --version
docker compose version

Passo 3: escrever o Dockerfile

FROM python:3.13-slim

WORKDIR /app

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

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:app"]

Os próprios docs de deploy do Flask apontam direto pro gunicorn: “ele tende a se integrar facilmente com plataformas de hospedagem” e não precisa de nenhuma dependência extra pra instalar. --workers 4 é um ponto de partida razoável (uma regra prática comum dos próprios docs do gunicorn é algo como 2 * núcleos de CPU + 1); ajuste pro número real de vCPUs do Droplet. Se sua aplicação usa uma application factory em vez de uma variável app no nível do módulo, o comando se torna gunicorn --bind 0.0.0.0:8000 'app:create_app()' no lugar.

Um requirements.txt mínimo:

flask>=3.1
gunicorn>=23.0
psycopg[binary]>=3.2
flask-sqlalchemy>=3.1
flask-migrate>=4.0

Passo 4: escrever o arquivo do Compose

services:
  app:
    build: .
    restart: unless-stopped
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    expose:
      - "8000"

  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  db-data:
  caddy-data:
  caddy-config:

Seu arquivo .env:

DB_NAME=app
DB_USER=app
DB_PASSWORD=change-this
SECRET_KEY=change-this-too
FLASK_ENV=production
DATABASE_URL=postgresql+psycopg://app:change-this@db:5432/app

Valide antes de iniciar:

docker compose config

Passo 5: escrever o Caddyfile

app.example.com {
    reverse_proxy app:8000
}

Passo 6: primeiro boot e migrations

docker compose up -d
docker compose exec app flask db upgrade
docker compose ps

Você deve ver app, db e caddy todos running, e https://app.example.com deve responder com um certificado válido.

Passo 7: fazer deploy de novas versões

git pull
docker compose build app
docker compose up -d --no-deps app
docker compose exec app flask db upgrade

Reconstruir só o app deixa db e caddy intocados, então o certificado e o banco de dados ficam no ar durante o deploy.

Backups

docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz

Copie o arquivo pra fora do Droplet num cron job, e teste uma restauração num banco descartável de vez em quando pra saber que ela de fato funciona.

Servindo arquivos estáticos

O Flask consegue servir sua própria pasta static/ diretamente, mas o Caddy já está na frente de toda requisição e é mais adequado pra isso do que pedir pros workers do gunicorn gastarem tempo servindo arquivos em vez de tratar a lógica da aplicação. Adicione um matcher antes do proxy genérico:

app.example.com {
    handle_path /static/* {
        root * /srv/static
        file_server
    }
    reverse_proxy app:8000
}

Monte o diretório estático da aplicação num volume que o Caddy também consiga ler, e rode flask ... como qualquer comando que seu projeto usa pra coletar ou copiar assets estáticos pra esse caminho durante o build do Docker, pra que os arquivos existam antes do container do Caddy começar a procurá-los.

O caminho mais curto: fazer isso com o Railyard

Deploy Flask to DigitalOcean roda a mesma receita a partir de uma URL de repositório. Conecte uma conta DigitalOcean com um token que tenha acesso de escrita a Droplets, ou traga um servidor que você já provisionou. O Railyard detecta o Flask a partir do requirements.txt ou pyproject.toml, serve app:app na $PORT com gunicorn (apontado pro seu próprio módulo se ele viver em outro lugar), e reconhece psycopg como uma dependência pra provisionar o Postgres com DATABASE_URL injetado automaticamente. Defina flask db upgrade ou alembic upgrade head como o comando de release pra migrar em cada release, o mesmo que o Passo 6 acima mas rodado automaticamente antes de cada versão nova receber tráfego. O RQ é oferecido como worker quando é uma dependência, e qualquer outro processo do Procfile também se torna um. Veja os docs do Flask pras regras exatas de detecção.

Resolução de problemas

  • Connection refused no primeiro boot: o app iniciou antes do Postgres terminar de inicializar; a condição de healthcheck no Passo 4 deveria prevenir isso, mas um loop curto de retry em volta do primeiro comando de migration é uma salvaguarda extra razoável.
  • O gunicorn inicializa mas as requisições ficam travadas: geralmente uma contagem de workers alta demais pra CPU real do Droplet, ou uma chamada bloqueante dentro de um handler de requisição sufocando os outros workers; cheque docker stats sob carga e reduza o --workers antes de aumentar o tamanho do Droplet no lugar.
  • sqlalchemy.exc.OperationalError mesmo com o container do banco rodando: confirme que DATABASE_URL no .env realmente combina com as credenciais do container db; um .env desatualizado sobrando do desenvolvimento local é a causa mais comum.
  • O HTTPS nunca é emitido, o Caddy continua tentando de novo: o DNS ainda não está apontando pro Droplet. dig +short app.example.com deve bater com o IP do Droplet.
  • flask db upgrade falha com “Can’t locate revision”: a pasta migrations/ não foi commitada no repositório, ou a imagem foi construída a partir de um checkout desatualizado faltando um arquivo de migration recente; confirme que git status está limpo no servidor antes de reconstruir.
  • Assets estáticos retornam 404 pelo Caddy mas funcionam acessando o gunicorn direto: o matcher handle_path na seção de arquivos estáticos acima não bateu com o caminho da requisição, geralmente porque o prefixo real de URL estática da aplicação é diferente de /static/; cheque qual prefixo a própria aplicação gera no HTML renderizado antes de assumir que o Caddy está mal configurado.

Perguntas frequentes

Por que gunicorn em vez do servidor embutido do Flask? Os próprios docs do Flask são explícitos de que o servidor de desenvolvimento não foi feito pra ser particularmente seguro, estável ou eficiente; o gunicorn é um servidor WSGI de produção construído exatamente pra isso, sem dependências extras pra compilar.

E as rotas async do Flask? O Flask suporta funções de view async def desde a versão 2.0, mas o worker sync padrão do gunicorn não roda um event loop; se a aplicação realmente precisa de tratamento de requisição async em escala, a classe de worker gevent do gunicorn ou trocar pra um framework baseado em ASGI são as duas opções reais, não algo que o worker padrão deste guia trata bem.

Postgres ou MySQL? O Flask-SQLAlchemy é agnóstico a banco de dados; este guia usa Postgres, mas trocar a imagem do db e o esquema da URI de conexão do SQLAlchemy funciona do mesmo jeito pro MySQL.

Eu realmente preciso de um Droplet de 2 GB? Um Droplet de 1 GB roda Flask e Postgres juntos, mas com muito pouca folga de memória pra picos de tráfego ou um segundo processo; 2 GB é uma base mais realista no momento em que usuários reais aparecem.

E se minha aplicação precisa de um worker em segundo plano, não só do processo web? Adicione um processo estilo Procfile (worker: rq worker --url $REDIS_URL por exemplo) como seu próprio serviço no arquivo do Compose, construído a partir da mesma imagem do app, com seu próprio command sobrescrito, em vez de rodá-lo dentro do mesmo container que o processo web.

Como eu vejo logs e métricas básicas sem entrar via SSH toda vez? O próprio dashboard de Droplets da DigitalOcean mostra gráficos de CPU, memória e disco de fábrica sem nenhuma configuração extra; pra logs da aplicação, docker compose logs -f app via SSH continua sendo a opção mais direta, a menos que você encaminhe eles pra um agregador de logs externo, o que vale a pena fazer assim que mais de uma pessoa precisa checar logs regularmente.

Uma aplicação Flask num Droplet da DigitalOcean que você mesmo provisionou custa US$12 por mês, com gunicorn, Postgres e migrations explícitos em arquivos que você controla em vez de assumidos. Lançar uma versão nova depois que eles existem é um rebuild de um container e um comando de migration, nada mais pra lembrar.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Pule a configuração do servidor.

Suba um repositório Flask pro Railyard e ele provisiona o Postgres, serve com gunicorn, e faz deploy em cada push.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.