← Blog

Fazendo deploy de uma aplicação Rails na DigitalOcean

Railyard

14 de abril de 2026 · 11 min de leitura

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

Você tem uma aplicação Rails funcionando no seu laptop e precisa dela rodando num servidor de verdade, com um domínio de verdade, um banco de dados de verdade, e HTTPS que se renova por conta própria. A maioria dos guias ou para no “docker build” e deixa você descobrir o resto, ou te entrega uma plataforma hospedada com um formulário de cartão de crédito. Este te leva do começo ao fim, de uma conta DigitalOcean vazia até uma aplicação Rails em produção: um droplet, Docker Compose rodando sua aplicação e o Postgres, Caddy terminando o HTTPS, um job de backup, e um jeito repetível de lançar sua próxima release.

O que você vai precisar

  • Uma conta DigitalOcean e um Droplet Basic no tamanho s-1vcpu-2gb (1 vCPU, 2 GB de memória, 50 GB SSD), atualmente US$12,00/mês.
  • Um nome de domínio ao qual você pode adicionar um registro de DNS.
  • Um par de chaves SSH na sua máquina (ssh-keygen -t ed25519 se você não tiver um).
  • Uma aplicação Rails, ou uma nova: este guia constrói uma do zero com rails new, então você consegue acompanhar mesmo que ainda não tenha uma aplicação.
  • Quinze minutos pro droplet e o DNS se acomodarem, a maior parte gasta esperando em vez de digitando.

Passo 1: criar o droplet

No painel de controle da DigitalOcean, escolha Create, depois Droplets. Escolha Ubuntu 24.04 (LTS) x64 como imagem, Basic como plano, SSD Regular, e o tamanho s-1vcpu-2gb (US$12,00/mês, 2 GB de memória, 1 vCPU, 50 GB SSD). Escolha uma região próxima dos seus usuários, Frankfurt ou Amsterdã se você precisa manter os dados na UE. Em Authentication, escolha SSH key e adicione a chave pública da sua máquina em vez de uma senha, depois crie o droplet.

Quando ele iniciar, a DigitalOcean te mostra o IP público. Conecte pra confirmar o acesso:

ssh root@YOUR_DROPLET_IP

Você deve ver o banner de boas-vindas do Ubuntu e cair num shell root. Se a conexão for recusada, confira se a chave SSH que você adicionou combina com a chave privada na sua máquina.

Passo 2: apontar seu domínio pro droplet

Adicione um registro A no seu provedor de DNS pro hostname que você quer usar, apontando pro IP do droplet:

Type: A
Name: myapp (or @ for the bare domain)
Value: YOUR_DROPLET_IP
TTL: 300

A propagação de DNS pode levar alguns minutos. Confira a partir da sua máquina antes de seguir, já que o Caddy vai precisar disso já resolvendo corretamente numa etapa posterior:

dig +short myapp.example.com

Você deve ver o IP do seu droplet impresso de volta. Se não aparecer nada, espere alguns minutos e tente de novo antes de continuar.

Passo 3: instalar o Docker no droplet

O Dockerfile gerado pelo Rails precisa do Docker Engine e do plugin do Compose no servidor. O próprio script de instalação do Docker cuida dos dois numa máquina Ubuntu nova:

apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker

Também abra o firewall pra SSH, HTTP e HTTPS, e mais nada:

ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable

Cheque a instalação:

docker --version
docker compose version

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

Passo 4: gerar a aplicação Rails com um Dockerfile e Postgres

Na sua própria máquina, gere uma aplicação nova configurada pro Postgres em vez do SQLite padrão:

gem install rails -v 8.1.4
rails new myapp --database=postgresql
cd myapp

rails new cria um Dockerfile por padrão (é um dos arquivos listados quando o gerador roda, junto do .dockerignore), ajustado pra produção: ele constrói suas gems e assets num único estágio, depois copia só o necessário pra uma imagem de runtime magra. O Rails 8 também inclui o Thruster, um pequeno proxy HTTP na frente do Puma que trata HTTP/2, compressão e X-Sendfile, e é por isso que a imagem gerada expõe a porta 80 internamente e inicia com ./bin/thrust ./bin/rails server em vez de iniciar o Puma direto. Você não vai acessar essa porta diretamente pela internet, o Caddy vai ficar na frente dela.

Porque você passou --database=postgresql, o config/database.yml já está configurado pra gem pg em vez do SQLite, e seu Gemfile.lock agora lista pg, que é exatamente o que diz a um host que essa aplicação quer um banco Postgres em vez de MySQL ou SQLite.

Confirme que os arquivos estão lá:

ls Dockerfile .dockerignore config/database.yml

Você deve ver os três caminhos impressos sem nenhum erro.

Passo 5: configurar a conexão com o banco de dados e os secrets

Abra config/database.yml e aponte a seção production pra uma única connection string em vez de chaves separadas de host/usuário/senha, já que é isso que você vai injetar a partir do Docker Compose:

production:
  <<: *default
  url: <%= ENV.fetch("DATABASE_URL") %>

O Rails criptografa o config/credentials.yml.enc com o config/master.key, um arquivo que está no .gitignore de propósito. Você não vai enviar esse arquivo pro servidor. Em vez disso, leia o valor dele uma vez:

cat config/master.key

No droplet, crie um diretório pra aplicação e um arquivo .env com essa chave e uma senha de banco de dados que você mesmo gera:

mkdir -p /opt/myapp
cat > /opt/myapp/.env <<'EOF'
RAILS_MASTER_KEY=paste-the-value-from-config-master-key-here
POSTGRES_PASSWORD=a-long-random-password
EOF
chmod 600 /opt/myapp/.env

Confira:

cat /opt/myapp/.env

Você deve ver as duas variáveis com valores reais, não placeholders, e o arquivo não deve ser legível por todo mundo (-rw------- no ls -l).

Passo 6: escrever o arquivo do Docker Compose

Copie sua aplicação pro droplet (scp, rsync, ou um git clone do seu repositório em /opt/myapp), depois adicione compose.yml ao lado dele:

services:
  app:
    build: .
    image: myapp:latest
    restart: unless-stopped
    env_file: .env
    environment:
      RAILS_ENV: production
      DATABASE_URL: postgres://myapp:${POSTGRES_PASSWORD}@db:5432/myapp_production
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - storage:/rails/storage
    expose:
      - "80"

  db:
    image: postgres:16.4-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: myapp_production
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 5s
      timeout: 5s
      retries: 10

  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:
  pgdata:
  storage:
  caddy_data:
  caddy_config:

O serviço app conecta ao db pelo nome do serviço, não localhost, já que o Compose coloca os dois containers na mesma rede privada. A linha depends_on: condition: service_healthy evita que a aplicação inicie antes do Postgres de fato poder aceitar conexões, não só antes do container dele existir. Volumes nomeados mantêm seu banco de dados e seus arquivos do Active Storage em disco entre restarts e rebuilds.

Cheque se o arquivo é lido sem erros antes de seguir:

docker compose config

Você deve ver a configuração totalmente resolvida impressa sem nenhuma linha de erro.

Passo 7: adicionar o Caddy como reverse proxy com HTTPS

Adicione um Caddyfile ao lado do compose.yml:

myapp.example.com {
	reverse_proxy app:80
}

Troque myapp.example.com pelo domínio que você apontou pro droplet no passo 2. O Caddy lê isso e faz duas coisas por conta própria: redireciona via proxy toda requisição pra porta 80 do container app, e solicita um certificado TLS gratuito do Let’s Encrypt pra esse hostname exato, renovando ele automaticamente antes que expire. Isso só funciona se o domínio já resolver pra esse droplet, que é por isso que o DNS veio antes do Caddy neste guia.

Passo 8: iniciar o stack

A partir de /opt/myapp:

docker compose build
docker compose up -d db
docker compose run --rm app bin/rails db:prepare
docker compose up -d

bin/rails db:prepare cria o banco de dados se ele ainda não existir e roda qualquer migration pendente, que é o mesmo comando que o Railyard roda antes de cada release mais adiante neste guia.

Cheque o stack:

docker compose ps
curl -I https://myapp.example.com

Você deve ver os três serviços como Up (e db como healthy), e o curl deve retornar HTTP/2 200 sem nenhum aviso de certificado.

Passo 9: fazer backup do Postgres

Um dump feito fora do volume de dados é o que realmente te protege se o droplet desaparecer. Adicione um script de backup:

cat > /opt/myapp/backup.sh <<'EOF'
#!/bin/sh
cd /opt/myapp
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U myapp myapp_production | gzip > /opt/myapp/backups/$TIMESTAMP.sql.gz
find /opt/myapp/backups -name '*.sql.gz' -mtime +14 -delete
EOF
mkdir -p /opt/myapp/backups
chmod +x /opt/myapp/backup.sh

Agende com cron, diariamente às 3h:

(crontab -l 2>/dev/null; echo "0 3 * * * /opt/myapp/backup.sh") | crontab -

Rode uma vez à mão e cheque que funcionou:

/opt/myapp/backup.sh
ls -lh /opt/myapp/backups

Você deve ver um arquivo .sql.gz com o timestamp de hoje e um tamanho diferente de zero. Pra qualquer coisa além de um projeto pessoal, copie esses arquivos pra fora do droplet também, com rclone pro DigitalOcean Spaces ou outro armazenamento de objetos, pra que um droplet perdido não signifique um backup perdido também.

Passo 10: lançar uma atualização

Toda mudança futura segue o mesmo formato: trazer o código novo pro droplet, reconstruir a imagem, rodar as migrations pendentes, depois substituir o container em execução.

git pull
docker compose build app
docker compose run --rm app bin/rails db:prepare
docker compose up -d app

docker compose up -d app só recria o container app, não db nem caddy, então o Postgres continua rodando e o estado do certificado do Caddy fica intocado. Existe um intervalo curto, um segundo ou dois, enquanto o container antigo para e o novo inicia, já que essa configuração manual roda um único container de aplicação. Cheque que foi aplicada:

docker compose logs --tail=50 app
curl -I https://myapp.example.com

Você deve ver sua aplicação inicializar limpa nos logs e o curl ainda retornando 200 na nova versão.

O caminho mais curto: fazer isso com o Railyard

Tudo acima, dimensionamento do droplet, Docker, Postgres, Caddy, backups, releases sem downtime, também é o que o Railyard automatiza pra uma aplicação Rails na DigitalOcean, sem você escrever nenhum dos arquivos acima à mão.

Cole a URL do seu repositório em https://app.railyard.run/deploy?repo= e escolha a DigitalOcean como provedor. Na primeira vez, você vai gerar um token no painel de controle da DigitalOcean (abra API, depois gere um token de acesso pessoal com acesso de leitura e escrita) pra que o Railyard consiga provisionar o servidor em seu nome. O Railyard usa por padrão essa combinação com o mesmo droplet s-1vcpu-2gb usado neste guia, e você pode escolher qualquer região da DigitalOcean, incluindo Frankfurt ou Amsterdã se residência de dados na UE importa pra você.

A partir daí, o Railyard lê sua aplicação do mesmo jeito que este guia fez à mão: ele vê pg no seu Gemfile.lock e provisiona um banco Postgres gerenciado em vez do SQLite, roda bin/rails db:prepare antes de cada release do mesmo jeito que o passo 8 fez manualmente, e se você está no Rails 8 ele dá aos bancos de cache, queue e cable suas próprias connection strings em vez de compartilhar uma. O Active Storage em disco recebe um volume persistente automaticamente, combinando com o volume storage do passo 6. SECRET_KEY_BASE é gerado uma vez e guardado, então você nunca gerencia ele à mão. Se seu Gemfile.lock inclui Sidekiq, Solid Queue, GoodJob, Resque ou Delayed Job, o Railyard oferece ele como um processo worker ao lado do seu processo web, sem nenhum serviço de compose separado pra escrever. O HTTPS e o reverse proxy do passo 7 são tratados do mesmo jeito, apontados pro seu domínio quando você o adicionar.

Leia mais em deploying from a repository, databases and backups, o botão de deploy em um clique, getting started, o guia de referência de Rails na DigitalOcean, e comparações de custo de hospedagem se você quiser ver como o custo só-do-droplet aqui se compara a um caminho gerenciado. O plano Free do Railyard cobre um servidor mais um servidor de banco de dados e três aplicações sem cartão necessário, o Pro é US$19/mês por equipe, e o Team é US$49/mês por equipe.

Resolução de problemas

Porta já em uso. Se docker compose up falha com algo vinculado à porta 80 ou 443, o Apache ou Nginx padrão do Ubuntu pode já estar instalado e rodando. Cheque com ss -tlnp | grep -E ':80|:443' e pare ou remova o serviço que estiver ocupando elas antes do Caddy tentar se vincular.

RAILS_MASTER_KEY ou credentials errados. Um erro como ActiveSupport::MessageEncryptor::InvalidMessage na inicialização quase sempre significa que o RAILS_MASTER_KEY em /opt/myapp/.env não combina com a chave que criptografou o config/credentials.yml.enc. Rode cat config/master.key de novo localmente e compare caractere por caractere com o que está no .env no droplet.

Conexão com o Postgres recusada. Se os logs da aplicação mostrarem could not connect to server: Connection refused, cheque se DATABASE_URL usa db como hostname, não localhost ou 127.0.0.1. Dentro da rede do Compose, db é o único endereço que resolve pro container do Postgres; localhost significa o próprio container app, que não tem nenhum banco de dados rodando nele.

HTTPS não é emitido. Se docker compose logs caddy mostra erros repetidos de certificado ou NXDOMAIN, seu registro DNS do passo 2 ainda não propagou ou está apontando pro IP errado. Rode dig +short myapp.example.com de novo, confirme que combina com o droplet, depois reinicie o Caddy com docker compose restart caddy quando combinar.

Falta de memória durante o precompile de assets. Num droplet de 2 GB, o docker compose build pode ser morto na metade da compilação de assets numa aplicação maior. Adicione um arquivo de swap de 2 GB antes de construir: fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile, depois torne isso permanente adicionando ao /etc/fstab.

Perguntas frequentes

Um droplet de 2 GB é suficiente pra produção? Pra uma aplicação Rails pequena a média com tráfego modesto, sim, já que o Puma e o Postgres juntos cabem com conforto, e a correção de swap da resolução de problemas acima cobre o único caso (builds de assets grandes) em que 2 GB fica apertado.

Eu posso pular o Postgres e manter o SQLite? Você pode, o SQLite funciona bem num servidor único desde que você monte o arquivo do banco dele num volume persistente pra que ele sobreviva a restarts de container, mas o Postgres te dá um caminho mais limpo se você algum dia adicionar um segundo servidor de aplicação.

Como eu adiciono Redis ou Sidekiq? Adicione um serviço redis ao compose.yml usando a imagem oficial redis com seu próprio volume nomeado, adicione REDIS_URL ao ambiente do app apontando pra redis://redis:6379/0, e adicione um serviço worker que roda bundle exec sidekiq contra a mesma imagem em vez do comando do servidor Rails.

Isso sobrevive a um reboot do droplet? Sim, restart: unless-stopped em cada serviço significa que o Docker traz o stack todo de volta automaticamente depois que o droplet reinicia, desde que o próprio serviço systemd do Docker esteja habilitado, o que o script de instalação no passo 3 faz por padrão.

Como isso é diferente de usar Heroku ou Railway? Essas plataformas gerenciam o servidor por você a um preço por aplicação; este guia te dá o mesmo resultado num servidor que você paga diretamente à DigitalOcean, ao custo de rodar os comandos de atualização e backup você mesmo em vez de um dashboard fazer isso.

Essa é uma configuração completa de produção: um droplet, Docker Compose rodando sua aplicação e o Postgres, Caddy cuidando do HTTPS, um backup noturno, e uma release repetível de três comandos. Mantenha esse compose.yml e Caddyfile no seu repositório junto da aplicação, e todo deploy futuro é o mesmo git pull, build, migrate, restart que você acabou de rodar no passo 10.

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 a URL do seu repositório no Railyard e tenha Postgres, um worker, HTTPS e backups configurados pra você.

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