← Blog

Fazendo deploy de Django na DigitalOcean

Railyard

1 de maio de 2026 · 8 min de leitura

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

Você tem uma aplicação Django que funciona no seu laptop, e uma conta na DigitalOcean. O que falta é tudo que fica no meio: um servidor que fica no ar, um banco Postgres em vez de SQLite, um gerenciador de processos pra que o gunicorn reinicie quando travar, arquivos estáticos servidos corretamente, e um certificado pra que o site responda em https:// em vez de um IP puro. Nada disso é difícil por si só, mas é fácil deixar uma peça quase certa e passar uma noite inteira brigando com ALLOWED_HOSTS.

Este guia constrói tudo isso com Docker Compose: um container pro Postgres, um pra sua aplicação Django por trás do gunicorn, e um pro Caddy cuidando do HTTPS e dos arquivos estáticos. Ao final você vai ter um droplet servindo sua aplicação no seu próprio domínio, com um jeito repetível de fazer deploy de novas versões e de fazer backup do banco. Depois vamos ver o caminho mais curto: subir o mesmo repositório pro Railyard e deixar ele ler o requirements.txt por você.

O que você vai precisar

  • Um droplet da DigitalOcean. O plano Basic com 1 vCPU / 2 GB de RAM roda Postgres, gunicorn e Caddy juntos com conforto e custa atualmente US$12/mês; o plano de 1 GB (US$6/mês) funciona pra aplicações bem pequenas, mas deixa pouca folga quando o Postgres também está rodando.
  • Um nome de domínio, com acesso pra adicionar um registro A.
  • Um repositório no GitHub (ou qualquer host Git) contendo seu projeto Django.
  • Uma conta DigitalOcean com faturamento configurado, e uma chave SSH adicionada à sua conta.

Passo 1: criar o droplet

No dashboard da DigitalOcean, crie um novo droplet: escolha a imagem Ubuntu 24.04 LTS, o plano Basic de 2 GB, uma região próxima dos seus usuários, e sua chave SSH. Quando ele estiver rodando, anote o IP público e aponte o registro A do seu domínio pra ele:

myproject.example.com.  A  203.0.113.10

A propagação de DNS pode levar de alguns minutos a algumas horas. Você pode continuar configurando o servidor nesse meio tempo; o Caddy só precisa que o registro DNS esteja correto no momento em que ele tenta emitir um certificado.

Entre via SSH como root e instale o Docker:

ssh root@203.0.113.10
curl -fsSL https://get.docker.com | sh
apt-get install -y docker-compose-plugin

Confira se funcionou:

docker compose version

Você deve ver uma string de versão do plugin do Compose, não um erro de “comando não encontrado”.

Passo 2: preparar a aplicação Django pra produção

Os padrões do Django assumem um desenvolvedor rodando manage.py runserver localmente, não um servidor. Algumas configurações precisam mudar pra produção. Em settings.py:

import os

DEBUG = False
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["POSTGRES_DB"],
        "USER": os.environ["POSTGRES_USER"],
        "PASSWORD": os.environ["POSTGRES_PASSWORD"],
        "HOST": "db",
        "PORT": "5432",
    }
}

STATIC_URL = "/static/"
STATIC_ROOT = "/app/staticfiles"

Fixe os pacotes que você precisa no requirements.txt. Este guia usa Django 6.1, gunicorn 26.2 como servidor WSGI, e psycopg2-binary 2.9.13 pra falar com o Postgres:

django==6.1.2
gunicorn==26.2.0
psycopg2-binary==2.9.13

Se você já usa django-environ ou dj-database-url pra ler o DATABASE_URL como uma única string, isso também funciona; as variáveis de ambiente acima são só a versão mais simples.

Passo 3: escrever o Dockerfile

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

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

COPY . .
RUN python manage.py collectstatic --noinput

CMD ["sh", "-c", "python manage.py migrate --no-input && gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3"]

Troque myproject.wsgi pelo pacote real do seu projeto. Rodar migrate antes do gunicorn iniciar significa que cada deploy aplica as migrations pendentes automaticamente; pra uma configuração de servidor único como essa, isso é mais simples do que uma etapa de release separada. Três workers do gunicorn são um ponto de partida razoável pra um droplet de 2 GB; observe a memória sob carga e ajuste.

Passo 4: escrever o docker-compose.yml

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

  web:
    build: .
    restart: unless-stopped
    env_file: .env
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - static_files:/app/staticfiles
    expose:
      - "8000"

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - static_files:/srv/static
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - web

volumes:
  postgres_data:
  static_files:
  caddy_data:
  caddy_config:

Só o Caddy publica portas pro mundo externo; web e db ficam alcançáveis só por containers na mesma rede do Compose, o que é uma coisa menos exposta à internet por acidente.

Passo 5: escrever o Caddyfile

myproject.example.com {
	encode gzip

	handle_path /static/* {
		root * /srv/static
		file_server
	}

	reverse_proxy web:8000
}

O Caddy solicita e renova um certificado Let’s Encrypt pra myproject.example.com automaticamente na primeira vez que inicia, desde que o DNS do domínio já aponte pra esse servidor e as portas 80 e 443 cheguem até ele. Arquivos estáticos em /static/ são servidos diretamente pelo Caddy a partir do volume compartilhado; tudo o mais é redirecionado via proxy pro gunicorn.

Passo 6: definir suas variáveis de ambiente e secrets

Crie um arquivo .env ao lado do docker-compose.yml, e mantenha ele fora do controle de versão:

POSTGRES_DB=myproject
POSTGRES_USER=myproject
POSTGRES_PASSWORD=a-long-random-password
DJANGO_ALLOWED_HOSTS=myproject.example.com
SECRET_KEY=a-different-long-random-value

Gere os dois valores aleatórios com openssl rand -base64 32 em vez de digitar algo memorável; esse banco e essa instância do Django só precisam ser impossíveis de adivinhar, não fáceis de lembrar.

Passo 7: abrir o firewall e fazer o deploy

ufw allow 22
ufw allow 80
ufw allow 443
ufw enable

Depois, no diretório com seu docker-compose.yml:

docker compose up -d --build
docker compose logs -f caddy

Você deve ver o Caddy registrar que obteve um certificado pro seu domínio, e aí docker compose logs -f web deve mostrar o gunicorn inicializando e as migrations sendo aplicadas. Acesse https://myproject.example.com num navegador: você deve ver sua aplicação sobre HTTPS com um certificado válido, não um aviso do navegador.

Backups e atualizações

Faça backup do Postgres com um dump agendado, escrito fora do container pra que um volume perdido não leve o backup junto:

docker compose exec -T db pg_dump -U myproject myproject > backup-$(date +%F).sql

Coloque isso num cron job e copie o resultado pra fora do droplet (DigitalOcean Spaces, S3, ou simplesmente scp pra outra máquina). Pra atualizar a aplicação, puxe seu código mais recente, reconstrua, e suba os containers de novo; a etapa de migrate no CMD do Dockerfile roda de novo automaticamente:

git pull
docker compose up -d --build web

O caminho mais curto: fazer isso com o Railyard

Tudo acima (o Dockerfile, o gunicorn, a etapa de migration, o DATABASE_URL, o proxy e o certificado do Caddy) é o que o Railyard configura automaticamente quando você aponta ele pro mesmo repositório. Pelas próprias regras de detecção dele: o Django é reconhecido a partir do requirements.txt, do pyproject.toml, ou de um manage.py que o importa; a etapa de release roda python manage.py migrate --no-input antes da nova versão receber tráfego; o processo web roda no gunicorn, vinculado à porta que o Railyard atribui; e uma dependência psycopg/psycopg2 provisiona um banco Postgres automaticamente, com DATABASE_URL injetado como variável de ambiente, sem precisar de Dockerfile ou arquivo do Compose. O collectstatic também roda automaticamente durante o build, então o WhiteNoise ou uma configuração padrão de STATIC_ROOT funciona sem você escrever essa etapa. Veja os docs do Django pras regras exatas, e Databases and backups pra como a instância gerenciada do Postgres é salva em backup e restaurada.

Pra fazer deploy desse jeito: crie um droplet ou servidor pelo Railyard (a DigitalOcean é um dos provedores suportados), depois cole a URL Git do seu repositório, ou adicione isto ao seu README pra um clique:

[![Deploy to Railyard](https://app.railyard.run/deploy/badge)](https://app.railyard.run/deploy?repo=https://github.com/you/your-django-app)

O caminho manual acima ainda é o certo pra conhecer. É o que o Railyard está fazendo por debaixo dos panos, e entender isso significa que você nunca fica travado se algo precisar de uma mudança que o dashboard ainda não expõe.

Resolução de problemas

O Caddy nunca consegue um certificado. Confira se o registro A do domínio realmente resolve pro droplet (dig myproject.example.com) e se as portas 80 e 443 estão abertas tanto no firewall do droplet quanto no firewall de nuvem da DigitalOcean, se você adicionou um. O Let’s Encrypt valida via HTTP puro primeiro, então a porta 80 precisa estar acessível mesmo que o site final seja só HTTPS.

“Bad Request (400)” do Django. Isso é quase sempre o ALLOWED_HOSTS não combinando com o domínio que você está acessando. Confirme que DJANGO_ALLOWED_HOSTS no .env combina exatamente, faça o deploy de novo com docker compose up -d web, e cheque docker compose logs web.

Arquivos estáticos retornam 404. O collectstatic precisa rodar antes que o Caddy consiga servir qualquer coisa de /static/, e o volume static_files precisa estar montado nos serviços web e caddy nos caminhos que cada um espera. Se você mudou o STATIC_ROOT, atualize o Dockerfile e os caminhos de volume no docker-compose.yml pra combinar.

O container do Postgres não inicia, ou os dados parecem apagados depois de um restart. Cheque docker compose logs db procurando erros de permissão em /var/lib/postgresql/data; isso geralmente significa que o volume nomeado foi removido e recriado (por exemplo, por docker compose down -v, que apaga volumes). Nunca rode essa flag a menos que você realmente queira apagar os dados.

A porta 80 ou 443 já está em uso. Outro servidor web (geralmente um nginx instalado por uma imagem de um clique da DigitalOcean) já está vinculado a ela. Pare e desative ele: systemctl stop nginx && systemctl disable nginx.

Os workers do gunicorn estão sendo matados, ou o droplet parece lento sob carga. Num droplet de 1 GB, Postgres, três workers do gunicorn e o Caddy podem competir por memória. Reduza pra dois workers, adicione um arquivo de swap como rede de segurança, ou suba pra 2 GB se a aplicação tiver tráfego real.

Perguntas frequentes

Eu preciso de Docker, ou posso rodar isso com systemd e um virtualenv? O Docker Compose mantém o Postgres, a aplicação e o Caddy isolados e fáceis de mover entre servidores, mas um virtualenv com gunicorn rodado pelo systemd e um Postgres instalado no sistema funcionam do mesmo jeito; o comando do gunicorn e a configuração do Caddy acima não mudam de nenhum dos dois jeitos.

Posso usar SQLite em vez de Postgres? Pra um único droplet com baixo volume de escrita, sim, embora você perca a capacidade de rodar mais de um container web depois, e os backups passam a significar copiar um único arquivo em vez de um dump. O Postgres não custa nada extra aqui já que é só outro container.

E o Celery ou jobs em segundo plano? Adicione um serviço worker ao docker-compose.yml rodando celery -A myproject worker --loglevel=info contra a mesma imagem e arquivo .env; ele precisa de Redis ou RabbitMQ como broker, que é outro container do mesmo jeito que o Postgres é.

Eu preciso de um droplet maior se adicionar mais aplicações depois? Só quando você realmente estiver ficando sem memória ou CPU. Um droplet pode rodar várias aplicações por trás da mesma instância do Caddy em subdomínios diferentes, cada uma com seu próprio certificado, do mesmo jeito que roda uma só.

Uma aplicação Django num droplet não é frágil depois que as peças estão conectadas corretamente, e nenhuma das peças aqui é específica do Django: o mesmo container do Postgres, o mesmo proxy do Caddy e o mesmo cron job de backup funcionam pra próxima aplicação que você colocar nesse servidor também.

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 droplet

Cole seu repositório Django no Railyard e o gunicorn, as migrations e o Postgres ficam conectados pra você.

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