← Blog

Autoalojar Ghost con MySQL

Railyard

27 de abril de 2026 · 8 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

El instalador de una sola línea de Ghost te da un blog funcionando rápido, pero asume en silencio SQLite y una sola máquina que nunca vas a mover. En el momento en que quieres MySQL, un segundo servidor para tareas en segundo plano, o simplemente un archivo de Docker Compose que puedas leer y respaldar tú mismo, estás por tu cuenta. Esta guía construye esa configuración de principio a fin: Ghost y MySQL como dos contenedores, Caddy al frente para el HTTPS, respaldos reales y un proceso de actualización que no implica cruzar los dedos. Al final vas a tener una publicación corriendo sobre infraestructura que entiendes, más la versión de un clic si prefieres saltarte el YAML.

Esta guía usa la línea estable actual de Ghost (Ghost 6.x) para la instalación manual de abajo, y MySQL 8.0.

Qué necesitas

  • Un servidor. Un Droplet Basic de DigitalOcean con 2 vCPU y 2 GB de RAM corre Ghost y MySQL sin problemas por 18 USD al mes; el droplet de 1 vCPU y 1 GB a 6 USD al mes queda muy justo una vez que MySQL y Node están los dos en memoria. Cualquier máquina Ubuntu 22.04 o 24.04 funciona igual.
  • Un nombre de dominio, con su registro DNS A ya apuntando a la IP del servidor antes del paso de HTTPS.
  • Un proveedor SMTP (Postmark, SES, Mailgun, o similar) para correo transaccional. Ghost lo necesita para invitaciones de equipo, restablecimiento de contraseñas y enlaces de inicio de sesión de miembros; no tiene nada que ver con el envío de newsletters, que Ghost maneja por su cuenta una vez configurado el correo.

Paso 1: apuntar el DNS y preparar el servidor

Crea primero el registro A de tu dominio, ya que la emisión del certificado más adelante depende de que ya resuelva:

dig +short blog.example.com

Deberías ver la IP de tu servidor una vez que termine la propagación, normalmente en minutos con la mayoría de los registradores.

Conéctate por SSH e instala Docker Engine y el plugin de Compose:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Cierra sesión y vuelve a entrar para que el cambio de grupo tenga efecto, y confirma:

docker compose version

Deberías ver una cadena de versión de Compose (v2.x), no un error de “comando no encontrado”.

Paso 2: escribir el archivo de Docker Compose

Crea un directorio de proyecto y un docker-compose.yml con tres servicios: Ghost, MySQL, y Caddy como proxy inverso.

services:
  ghost:
    image: ghost:6-alpine
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      NODE_ENV: production
      url: https://${DOMAIN:?set DOMAIN in .env}
      database__client: mysql
      database__connection__host: db
      database__connection__port: "3306"
      database__connection__user: ghost
      database__connection__password: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
      database__connection__database: ghost
      mail__transport: SMTP
      mail__options__host: ${SMTP_HOST}
      mail__options__port: "465"
      mail__options__secure: "true"
      mail__options__auth__user: ${SMTP_USER}
      mail__options__auth__pass: ${SMTP_PASSWORD}
      mail__from: ${MAIL_FROM}
    volumes:
      - ghost_content:/var/lib/ghost/content
    networks:
      - ghost_net

  db:
    image: mysql:8.0.44
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD in .env}
      MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD:?set DATABASE_ROOT_PASSWORD in .env}
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-p${DATABASE_ROOT_PASSWORD}"]
      interval: 5s
      start_period: 30s
      retries: 20
    networks:
      - ghost_net

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      - ghost
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - ghost_net

networks:
  ghost_net:

volumes:
  ghost_content:
  mysql_data:
  caddy_data:
  caddy_config:

Las variables de entorno con doble guion bajo (database__connection__host) son la convención propia de Ghost para sobrescribir su configuración JSON anidada sin tocar un archivo, documentada en la referencia de configuración de Ghost. url tiene que ser la dirección real https:// desde el principio: Ghost la incrusta en los enlaces generados y en los temas, y cambiarla después requiere un ghost config completo y reconstruir la caché en lugar de una simple edición.

Junto al archivo de Compose, crea un .env con valores reales:

DOMAIN=blog.example.com
DATABASE_PASSWORD=una-cadena-larga-aleatoria
DATABASE_ROOT_PASSWORD=otra-cadena-larga-aleatoria
SMTP_HOST=smtp.postmarkapp.com
SMTP_USER=tu-usuario-smtp
SMTP_PASSWORD=tu-contraseña-smtp
MAIL_FROM="Tu Blog <hello@blog.example.com>"

Genera las dos contraseñas en lugar de escribirlas a mano:

openssl rand -hex 24

Deberías obtener una cadena hexadecimal de 48 caracteres cada vez que lo corras; usa una para DATABASE_PASSWORD y otra distinta para DATABASE_ROOT_PASSWORD.

Paso 3: escribir el Caddyfile

Caddy solicita y renueva el certificado automáticamente en cuanto puede responder en los puertos 80 y 443 para tu dominio:

blog.example.com {
	reverse_proxy ghost:2368
}

Eso es todo el archivo. El desafío ACME de Caddy necesita que el puerto 80 esté abierto al internet público aunque el tráfico final se mueva a HTTPS, así que no lo bloquees en el firewall.

Paso 4: levantar la pila

docker compose up -d
docker compose logs -f ghost

Deberías ver que el healthcheck de MySQL pasa, y luego Ghost registra una línea como Ghost is running in production... seguida de la URL en la que está sirviendo. Visita https://blog.example.com/ghost para llegar al asistente de configuración y crear tu primera cuenta de administrador.

Paso 5: respaldar MySQL y el volumen de contenido

Los datos reales de Ghost viven en dos lugares: MySQL (artículos, miembros, ajustes) y el volumen ghost_content (temas, imágenes, archivos subidos). Respalda ambos juntos para que una restauración quede consistente:

#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p backups
docker compose exec -T db mysqldump -u root -p"$DATABASE_ROOT_PASSWORD" ghost | gzip > "backups/ghost-db-$STAMP.sql.gz"
docker run --rm -v ghost_content:/content -v "$(pwd)/backups:/backup" alpine \
  tar czf "/backup/ghost-content-$STAMP.tar.gz" -C /content .

Corre esto desde el mismo directorio que tu archivo de Compose (para que ghost_content resuelva al volumen correcto del proyecto) y ponlo en una tarea cron diaria. Copia el directorio backups/ fuera del propio servidor, un rsync programado hacia otra máquina o un pequeño bucket de almacenamiento de objetos basta; un respaldo que solo vive en el servidor que se supone que protege no es un respaldo.

Deberías ver dos archivos nuevos en backups/ después de cada corrida, y su tamaño debería seguir más o menos el crecimiento de tu contenido, no quedarse en cero.

Paso 6: actualizaciones

Trae la imagen nueva y recrea solo el contenedor de Ghost; MySQL y su volumen de datos quedan intactos:

docker compose pull ghost
docker compose up -d ghost

Ghost corre sus propias migraciones de base de datos al arrancar, así que revisa los logs para confirmar que la migración terminó antes de dar la actualización por hecha:

docker compose logs --tail=50 ghost

Toma un respaldo (paso 5) antes de cada actualización, sobre todo entre versiones mayores como de la 5 a la 6, ya que es exactamente el tipo de cambio en el que una migración puede fallar a la mitad.

El atajo: hazlo con Railyard

Todo lo anterior, los dos contenedores, el cableado entre ellos, el proxy inverso y el certificado, es lo que la plantilla Ghost + MySQL de Railyard configura por ti desde el catálogo de plantillas. Corre ghost:5.130.6-alpine con un mysql:8.0.39 como sidecar: RAILYARD_URL y la cadena de conexión de MySQL se conectan automáticamente, así que no hay un database__connection__host que escribir a mano, y se crean y respaldan un volumen ghost-content y un volumen ghost-db-data sin una tarea cron que tengas que escribir tú mismo.

Abre https://app.railyard.run/deploy?stack=ghost-mysql y elige Ghost + MySQL en el catálogo. Igual agregas tus propias credenciales SMTP para el correo (Railyard no provee un transporte de correo), pero la base de datos, los volúmenes y el certificado HTTPS ya están resueltos. Mira Botón de despliegue a Railyard si prefieres publicar el mismo flujo de un clic desde tu propio README, y Bases de datos y respaldos para ver cómo funcionan los respaldos programados y las restauraciones una vez que está corriendo.

Solución de problemas

El puerto 80 o 443 ya está en uso. Algo más (a menudo una instalación por defecto de Apache o nginx) ya lo está usando. sudo ss -tlnp | grep -E ':80|:443' te muestra qué es, y hay que detenerlo o moverlo antes de que Caddy pueda usar esos puertos.

El certificado nunca se emite. Los logs de Caddy van a mostrar fallos de ACME repetidos. La causa casi universal es el DNS: dig +short blog.example.com tiene que devolver la IP del servidor antes de que Caddy pueda demostrarle la propiedad a Let’s Encrypt. Revisa también que nada río arriba (un firewall de nube, un grupo de seguridad por defecto del proveedor) bloquee el puerto 80 entrante.

Ghost se reinicia en bucle y los logs muestran un error de conexión a MySQL. database__connection__host tiene que coincidir con el nombre del servicio de Compose (db en el archivo de arriba), y la contraseña en el entorno de Ghost tiene que ser exactamente la misma cadena que MYSQL_PASSWORD en el servicio db. Una contraseña con un $ o un # necesita ir entre comillas de forma consistente en ambos lugares, o escaparse, ya que tanto Compose como el shell intentan interpretar esos caracteres.

Permiso denegado al escribir en el volumen de contenido. Esto aparece después de restaurar un respaldo tomado con tar como root en un volumen que el contenedor de Ghost espera poseer con su propio usuario. Restaura en un volumen nombrado nuevo con docker run --rm -v ghost_content:/content alpine tar xzf /backup/ghost-content-TIMESTAMP.tar.gz -C /content en lugar de copiar archivos con tu propia cuenta de usuario.

MySQL se queda sin memoria en un servidor de 1 GB. Los valores por defecto del buffer pool de MySQL escalan con la máquina, y en un droplet de 1 GB deja muy poco para Ghost y Node. Por eso el droplet de 18 USD al mes y 2 GB de arriba es el mínimo real, no el tamaño más barato de la lista.

Preguntas frecuentes

¿Puedo usar SQLite en lugar de MySQL? Sí, la instalación por CLI por defecto de Ghost todavía usa SQLite, y está genuinamente bien para un sitio único de bajo tráfico. MySQL se gana su complejidad cuando quieres correr más de una instancia de Ghost contra los mismos datos, o vienes de un hosting que ya te daba MySQL administrado.

¿Esto soporta las newsletters y las funciones de membresía de Ghost? Sí, una vez que el SMTP (para correo transaccional) y un proveedor de envío masivo (Ghost se integra con Mailgun específicamente para el envío de newsletters) están configurados los dos. Son dos ajustes separados en el admin de Ghost, fáciles de confundir porque ambos son “correo”.

¿Puedo correr Ghost sin Docker? Ghost soporta una instalación nativa de Node.js en Ubuntu documentada en docs.ghost.org, con MySQL instalado como paquete del sistema en lugar de un contenedor. La ruta de Docker de esta guía es más fácil de respaldar y mover entre servidores, por eso la usamos aquí.

¿Cómo muevo un sitio Ghost existente a esta configuración? Usa el export/import propio de Ghost (Settings → Migration options) para el contenido, o restaura un mysqldump directamente en la base de datos del contenedor db si vienes de otra instalación con MySQL, y luego copia el directorio viejo content/images al nuevo volumen ghost_content.

¿Qué pasa si el servidor se reinicia? restart: unless-stopped en cada servicio hace que Docker vuelva a levantar los contenedores automáticamente en cuanto arranca el propio demonio de Docker, lo que pasa al iniciar por defecto en una distribución basada en systemd como Ubuntu.

Cualquiera de los dos caminos te deja con un sitio Ghost que de verdad controlas: el manual si quieres ver cada pieza en movimiento, la plantilla si prefieres empezar a escribir hoy mismo.

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.

Lanza Ghost + MySQL en un clic

Elige Ghost + MySQL en el catálogo de plantillas de Railyard y pasa directo a escribir tu primer artículo.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.