Desplegar una app Rails en DigitalOcean
Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta
Tu app Rails funciona en tu laptop y ahora necesitas que corra en un servidor real, con un dominio real, una base de datos real, y HTTPS que se renueve solo. La mayoría de las guías se detienen en “docker build” y te dejan a ti el resto, o te entregan una plataforma gestionada con un formulario de tarjeta de crédito. Esta te lleva desde una cuenta vacía de DigitalOcean hasta una app Rails en producción: un servidor, Docker Compose corriendo tu app y Postgres, Caddy terminando el HTTPS, una tarea de backup, y una forma repetible de lanzar tu próxima versión.
Lo que necesitas
- Una cuenta de DigitalOcean y un Basic Droplet de tamaño
s-1vcpu-2gb(1 vCPU, 2 GB de memoria, 50 GB SSD), actualmente en $12.00/mes. - Un nombre de dominio al que puedas agregar un registro DNS.
- Un par de llaves SSH en tu máquina (
ssh-keygen -t ed25519si no tienes uno). - Una app Rails, o una nueva: esta guía construye una desde cero con
rails new, así que puedes seguirla incluso sin tener una app todavía. - Unos quince minutos para que el servidor y el DNS se estabilicen, sobre todo esperando en vez de escribiendo.
Paso 1: crear el servidor
En el panel de control de DigitalOcean, elige Create, luego Droplets. Elige Ubuntu 24.04 (LTS) x64 como imagen, Basic como plan, Regular SSD, y el tamaño s-1vcpu-2gb ($12.00/mes, 2 GB de memoria, 1 vCPU, 50 GB SSD). Elige una región cercana a tus usuarios, Fráncfort o Ámsterdam si necesitas mantener los datos en la Unión Europea. En Authentication, elige SSH key y agrega la llave pública de tu máquina en vez de una contraseña, luego crea el servidor.
Una vez que arranque, DigitalOcean te muestra su IP pública. Conéctate para confirmar el acceso:
ssh root@YOUR_DROPLET_IP
Deberías ver el banner de bienvenida de Ubuntu y llegar a una shell de root. Si la conexión se rechaza, verifica que la llave SSH que agregaste coincida con la llave privada de tu máquina.
Paso 2: apunta tu dominio al servidor
Agrega un registro A en tu proveedor de DNS para el nombre de host que quieras usar, apuntando a la IP del servidor:
Type: A
Name: myapp (o @ para el dominio raíz)
Value: YOUR_DROPLET_IP
TTL: 300
La propagación de DNS puede tardar unos minutos. Verifícala desde tu máquina antes de seguir, ya que Caddy necesitará que esto ya resuelva correctamente en un paso más adelante:
dig +short myapp.example.com
Deberías ver la IP de tu servidor impresa de vuelta. Si no ves nada, espera unos minutos e intenta de nuevo antes de continuar.
Paso 3: instalar Docker en el servidor
El Dockerfile generado por Rails necesita Docker Engine y el plugin de Compose en el servidor. El script de instalación de Docker se encarga de ambos en una máquina Ubuntu nueva:
apt-get update
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
systemctl enable --now docker
Abre también el firewall para SSH, HTTP y HTTPS, y nada más:
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Verifica la instalación:
docker --version
docker compose version
Deberías ver una versión de Docker y una versión de Compose impresas, no un error de “command not found”.
Paso 4: generar la app Rails con un Dockerfile y Postgres
En tu propia máquina, genera una nueva app configurada para Postgres en vez de SQLite por defecto:
gem install rails -v 8.1.4
rails new myapp --database=postgresql
cd myapp
rails new crea un Dockerfile por defecto (es uno de los archivos listados cuando corre el generador, junto con .dockerignore), ajustado para producción: construye tus gems y assets en una etapa, y luego copia a la imagen final de ejecución solo lo necesario. Rails 8 también incluye Thruster, un pequeño proxy HTTP delante de Puma que maneja HTTP/2, compresión y X-Sendfile, por lo que la imagen generada expone el puerto 80 internamente y arranca con ./bin/thrust ./bin/rails server en vez de lanzar Puma directamente. Nunca llegarás a ese puerto directamente desde internet, Caddy estará delante.
Como pasaste --database=postgresql, config/database.yml queda configurado para la gem pg en vez de SQLite, y tu Gemfile.lock ahora lista pg, que es justo lo que le indica a un hosting que esta app quiere una base de datos Postgres en vez de MySQL o SQLite.
Verifica que los archivos estén ahí:
ls Dockerfile .dockerignore config/database.yml
Deberías ver las tres rutas impresas sin error.
Paso 5: configura la conexión a la base de datos y los secretos
Abre config/database.yml y haz que la sección production apunte a una sola cadena de conexión en vez de claves separadas de host, usuario y contraseña, ya que eso es lo que vas a inyectar desde Docker Compose:
production:
<<: *default
url: <%= ENV.fetch("DATABASE_URL") %>
Rails cifra config/credentials.yml.enc con config/master.key, un archivo que está excluido de git a propósito. No vas a enviar ese archivo al servidor. En cambio, lee su valor una vez:
cat config/master.key
En el servidor, crea un directorio para la app y un archivo .env con esa clave y una contraseña de base de datos que generes tú mismo:
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
Verifica:
cat /opt/myapp/.env
Deberías ver ambas variables con valores reales, no marcadores de posición, y el archivo no debería ser legible por todos (-rw------- con ls -l).
Paso 6: escribe el archivo Docker Compose
Copia tu app al servidor (scp, rsync, o un git clone de tu repositorio dentro de /opt/myapp), y luego agrega compose.yml al lado:
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:
El servicio app se conecta a db por su nombre de servicio, no por localhost, ya que Compose pone a ambos contenedores en la misma red privada. La línea depends_on: condition: service_healthy evita que la app arranque antes de que Postgres pueda realmente aceptar conexiones, no solo antes de que su contenedor exista. Los volúmenes nombrados mantienen tu base de datos y tus archivos de Active Storage en disco entre reinicios y reconstrucciones.
Verifica que el archivo se analice correctamente antes de seguir:
docker compose config
Deberías ver la configuración resuelta impresa sin líneas de error.
Paso 7: agrega Caddy como proxy inverso con HTTPS
Agrega un Caddyfile al lado de compose.yml:
myapp.example.com {
reverse_proxy app:80
}
Reemplaza myapp.example.com con el dominio que apuntaste al servidor en el paso 2. Caddy lee esto y hace dos cosas por su cuenta: reenvía cada solicitud al puerto 80 del contenedor app, y solicita un certificado TLS gratuito de Let’s Encrypt para ese nombre de host exacto, renovándolo automáticamente antes de que expire. Esto solo funciona si el dominio ya resuelve hacia este servidor, por eso el DNS va antes que Caddy en esta guía.
Paso 8: arranca la pila
Desde /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 crea la base de datos si todavía no existe y ejecuta cualquier migración pendiente, el mismo comando que Railyard ejecuta antes de cada release más adelante en esta guía.
Verifica la pila:
docker compose ps
curl -I https://myapp.example.com
Deberías ver los tres servicios como Up (y db como healthy), y el curl debería devolver HTTP/2 200 sin advertencia de certificado.
Paso 9: respalda Postgres
Un dump tomado fuera del volumen de datos es lo que realmente te protege si el servidor desaparece. Agrega un 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
Prográmalo con cron, diariamente a las 3 a. m.:
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/myapp/backup.sh") | crontab -
Ejecútalo una vez manualmente y verifica que funcionó:
/opt/myapp/backup.sh
ls -lh /opt/myapp/backups
Deberías ver un archivo .sql.gz con la marca de tiempo de hoy y un tamaño distinto de cero. Para cualquier cosa más allá de un proyecto personal, copia también estos archivos fuera del servidor, con rclone hacia DigitalOcean Spaces u otro almacenamiento de objetos, para que un servidor perdido no signifique también un backup perdido.
Paso 10: lanza una actualización
Cada cambio futuro sigue la misma forma: llevar el código nuevo al servidor, reconstruir la imagen, ejecutar las migraciones pendientes, y luego reemplazar el contenedor en ejecución.
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 solo recrea el contenedor app, no db ni caddy, así que Postgres sigue corriendo y el estado del certificado de Caddy no se toca. Hay un corte breve, un segundo o dos, mientras el contenedor anterior se detiene y el nuevo arranca, ya que esta configuración manual corre un solo contenedor de app. Verifica que haya quedado bien:
docker compose logs --tail=50 app
curl -I https://myapp.example.com
Deberías ver tu app arrancar limpiamente en los logs y el curl seguir devolviendo 200 en la nueva versión.
El atajo: hazlo con Railyard
Todo lo anterior, el tamaño del servidor, Docker, Postgres, Caddy, los backups, los releases sin tiempo de inactividad, es también lo que Railyard automatiza para una app Rails en DigitalOcean, sin que tengas que escribir a mano ninguno de los archivos de arriba.
Pega la URL de tu repositorio en https://app.railyard.run/deploy?repo= y elige DigitalOcean como proveedor. La primera vez, vas a generar un token en el panel de control de DigitalOcean (abre API, luego genera un token de acceso personal con permisos de lectura y escritura) para que Railyard pueda aprovisionar el servidor en tu nombre. Railyard elige por defecto el mismo servidor s-1vcpu-2gb para esta combinación que el usado en esta guía, y puedes elegir cualquier región de DigitalOcean, incluyendo Fráncfort o Ámsterdam si la residencia de datos en la Unión Europea te importa.
A partir de ahí, Railyard lee tu app de la misma forma en que esta guía lo hizo a mano: ve pg en tu Gemfile.lock y aprovisiona una base de datos Postgres gestionada en vez de SQLite, ejecuta bin/rails db:prepare antes de cada release de la misma forma en que el paso 8 lo hizo manualmente, y si estás en Rails 8, les da a las bases cache, queue y cable cada una su propia cadena de conexión en vez de compartir una. Active Storage en disco obtiene un volumen persistente automáticamente, igual que el volumen storage del paso 6. SECRET_KEY_BASE se genera una sola vez y se conserva, así que nunca lo manejas tú mismo. Si tu Gemfile.lock incluye Sidekiq, Solid Queue, GoodJob, Resque o Delayed Job, Railyard lo ofrece como worker junto a tu proceso web, sin un servicio compose separado que escribir. El HTTPS y el proxy inverso del paso 7 se manejan de la misma forma, apuntando a tu dominio una vez que lo agregas.
Lee más en desplegar desde un repositorio, bases de datos y backups, el botón de despliegue en un clic, guía de inicio rápido, la guía de referencia de Rails en DigitalOcean, y comparaciones de costos de hosting si quieres ver cómo se compara el costo del servidor solo aquí con un camino gestionado. El plan gratuito de Railyard cubre un servidor más un servidor de base de datos y tres apps sin necesidad de tarjeta, Pro es $19/mes por equipo, y Team es $49/mes por equipo.
Solución de problemas
Puerto ya en uso. Si docker compose up falla con algo ligado al puerto 80 o 443, puede que Apache o Nginx por defecto de Ubuntu ya esté instalado y corriendo. Revisa con ss -tlnp | grep -E ':80|:443' y detén o quita el servicio que los ocupe antes de que Caddy intente usarlos.
RAILS_MASTER_KEY o credenciales incorrectas. Un error como ActiveSupport::MessageEncryptor::InvalidMessage al arrancar casi siempre significa que el RAILS_MASTER_KEY en /opt/myapp/.env no coincide con la clave que cifró config/credentials.yml.enc. Vuelve a correr cat config/master.key localmente y compáralo carácter por carácter con lo que está en .env en el servidor.
Conexión a Postgres rechazada. Si los logs de la app muestran could not connect to server: Connection refused, revisa que DATABASE_URL use db como nombre de host, no localhost ni 127.0.0.1. Dentro de la red de Compose, db es la única dirección que resuelve al contenedor de Postgres; localhost se refiere al propio contenedor app, que no tiene ninguna base de datos corriendo adentro.
HTTPS que no se emite. Si docker compose logs caddy muestra errores de certificado repetidos o NXDOMAIN, tu registro DNS del paso 2 todavía no se ha propagado o apunta a la IP equivocada. Vuelve a correr dig +short myapp.example.com, confirma que coincide con el servidor, y luego reinicia Caddy con docker compose restart caddy una vez que sea correcto.
Memoria insuficiente durante la precompilación de assets. En un servidor de 2 GB, docker compose build puede ser matado a mitad de la compilación de assets en una app más grande. Agrega un archivo swap de 2 GB antes de construir: fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile, y luego hazlo permanente agregándolo a /etc/fstab.
Preguntas frecuentes
¿Un servidor de 2 GB alcanza para producción? Para una app Rails de tamaño pequeño a mediano con tráfico moderado, sí, ya que Puma y Postgres caben juntos sin problema, y la solución de swap de la sección de solución de problemas cubre el único caso (builds grandes de assets) donde 2 GB se queda ajustado.
¿Puedo quedarme con SQLite en vez de Postgres? Puedes, SQLite funciona bien en un solo servidor mientras montes su archivo de base de datos en un volumen persistente para que sobreviva a los reinicios de contenedor, pero Postgres te da un camino más limpio si alguna vez agregas un segundo servidor de app.
¿Cómo agrego Redis o Sidekiq? Agrega un servicio redis en compose.yml usando la imagen oficial redis con su propio volumen nombrado, agrega REDIS_URL al entorno de app apuntando a redis://redis:6379/0, y agrega un servicio worker que ejecute bundle exec sidekiq con la misma imagen en vez del comando del servidor Rails.
¿Esto sobrevive a un reinicio del servidor? Sí, restart: unless-stopped en cada servicio hace que Docker levante toda la pila automáticamente después de que el servidor se reinicie, siempre que el propio servicio systemd de Docker esté habilitado, lo que el script de instalación del paso 3 hace por defecto.
¿En qué se diferencia esto de usar Heroku o Railway? Esas plataformas gestionan el servidor por ti a un precio por app; esta guía te da el mismo resultado en un servidor que pagas directamente a DigitalOcean, al costo de ejecutar tú mismo los comandos de actualización y backup en vez de que un panel lo haga.
Esa es una configuración de producción completa: un servidor, Docker Compose corriendo tu app y Postgres, Caddy manejando el HTTPS, un backup nocturno, y un release repetible en tres comandos. Guarda este compose.yml y este Caddyfile en tu repositorio junto con la app, y cada futuro despliegue será el mismo git pull, build, migración, reinicio que acabas de ejecutar en el paso 10.
Por qué eligen Railyard para esto
- Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
- Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
- Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
- Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
- Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.