CMS Ghost en Docker con Nginx Proxy Manager (1)
Como ya sabéis (y si no lo sabéis deberías leer mi anterior post) para escribir esto estoy usando una instancia de Ghost autoalojada en mi servidor y Nginx Proxy Manager como proxy inverso para llegar a él.
En un primer momento iba a explicar en esta entrada tooooodo el proceso de instalación de Ghost y la configuración para que funcionen los correos transaccionales (para el login de los usuarios), la newsletter, estadísticas y la conexión con el fediverso. Explicar esto me llevaría a crear un post extremadamente largo y soporífero, así que toca dividirlo y empezar por la instalación.
Siguiendo la documentación de Ghost, tenemos que clonar los archivos de instalación (que se copiarán en /opt/ghost) en nuestro equipo:
sudo git clone https://github.com/TryGhost/ghost-docker.git /opt/ghost
cd /opt/ghost
Dentro de /opt/ghost hay un archivo .env.example que renombraremos a .env
sudo cp .env.example .env
Esto es lo que encontramos en el archivo .env tal cual lo descargamos:
# Use the below flags to enable the Analytics or ActivityPub containers as well
# COMPOSE_PROFILES=analytics,activitypub
# Ghost domain
# Custom public domain Ghost will run on
DOMAIN=example.com
# Ghost Admin domain
# If you have Ghost Admin setup on a separate domain uncomment the line below and add the domain
# You also need to uncomment the corresponding block in your Caddyfile
# ADMIN_DOMAIN=
# Database settings
# All database settings must not be changed once the database is initialised
DATABASE_ROOT_PASSWORD=reallysecurerootpassword
# DATABASE_USER=optionalusername
DATABASE_PASSWORD=ghostpassword
# ActivityPub
# If you'd prefer to self-host ActivityPub yourself uncomment the line below
# ACTIVITYPUB_TARGET=activitypub:8080
# Tinybird configuration
# If you want to run Analytics, paste the output from `docker compose run --rm tinybird-login get-tokens` below
# TINYBIRD_API_URL=https://api.tinybird.co
# TINYBIRD_TRACKER_TOKEN=p.eyJxxxxx
# TINYBIRD_ADMIN_TOKEN=p.eyJxxxxx
# TINYBIRD_WORKSPACE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Ghost configuration (https://ghost.org/docs/config/)
# SMTP Email (https://ghost.org/docs/config/#mail)
# Transactional email is required for logins, account creation (staff invites), password resets and other features
# This is not related to bulk mail / newsletter sending
mail__transport=SMTP
mail__options__host=smtp.example.com
mail__options__port=465
mail__options__secure=true
mail__options__auth__user=postmaster@example.com
mail__options__auth__pass=1234567890
# Advanced customizations
# Force Ghost version
# You should only do this if you need to pin a specific version
# The update commands won't work
# GHOST_VERSION=6-alpine
# Port Ghost should listen on
# You should only need to edit this if you want to host
# multiple sites on the same server
# GHOST_PORT=2368
# Data locations
# Location to store uploaded data
UPLOAD_LOCATION=./data/ghost
# Location for database data
MYSQL_DATA_LOCATION=./data/mysql
Vamos a verlo por partes...
Al descomentar COMPOSE_PROFILES, la instalación de Ghost creará 2 nuevos servicios, uno para alojar un servidor ActivityPub y conectar con el fediverso, y otro para habilitar las estadísticas configurando el servicio externo de TinyBird. Esta última opción la veremos en un próximo artículo así que, de momento, lo dejamos comentado.
# Use the below flags to enable the Analytics or ActivityPub containers as well
# COMPOSE_PROFILES=analytics,activitypub
En el siguiente bloque indicamos nuestro dominio para el blog y nos recomiendan utilizar uno distinto para la administración del mismo:
# Ghost domain
# Custom public domain Ghost will run on
DOMAIN=blog.genscorp.es
# Ghost Admin domain
# If you have Ghost Admin setup on a separate domain uncomment the line below and add the domain
# You also need to uncomment the corresponding block in your Caddyfile
ADMIN_DOMAIN=ghost.genscorp.es
Ahora, la configuración de la base de datos:
# Database settings
# All database settings must not be changed once the database is initialised
DATABASE_ROOT_PASSWORD=*UNA_CONTRASEÑA_SUPERSEGURA*
DATABASE_USER=NOMBRE_DE_USUARIO
DATABASE_PASSWORD=*OTRA_CONTRASEÑA_SUPERSEGURA*
Si autoalojamos un servidor de ActivityPub (o hemos descomentado la línea de ActivityPub anteriormente), en este siguiente bloque es donde podemos indicar en qué dirección puede encontrarlo (en mi caso, al usar el servicio propio de Ghost lo he dejado comentado):
# ActivityPub
# If you'd prefer to self-host ActivityPub yourself uncomment the line below
# ACTIVITYPUB_TARGET=activitypub:8080
Para configurar el apartado de estadísticas de Ghost se descomentará el siguiente bloque. Por ahora lo dejo comentado, ya nos meteremos en su configuración en un próximo artículo:
# Tinybird configuration
# If you want to run Analytics, paste the output from `docker compose run --rm tinybird-login get-tokens` below
# TINYBIRD_API_URL=https://api.tinybird.co
# TINYBIRD_TRACKER_TOKEN=p.eyJxxxxx
# TINYBIRD_ADMIN_TOKEN=p.eyJxxxxx
# TINYBIRD_WORKSPACE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
El bloque de configuración del SMTP se utiliza para el envío de correos transaccionales, es decir, los que se envían para el alta, el acceso o cambios de contraseña de los usuarios... es decir, todo lo que no tenga que ver con la newsletter.
En mi caso, utilizo protonmail, que ofrece un servicio de envío de correos a través de su SMTP (si tienes una cuenta de gmail, google también lo ofrece). En mi caso quedaría algo tal que así:
# SMTP Email (https://ghost.org/docs/config/#mail)
# Transactional email is required for logins, account creation (staff invites), password resets and other features
# This is not related to bulk mail / newsletter sending
mail__transport=SMTP
mail__options__host=smtp.protonmail.ch
mail__options__port=587
mail__options__secure=false
mail__options__auth__user=*USUARIO_SMTP*
mail__options__auth__pass=*CONTRASEÑA_USUARIO_SMTP*
Los siguientes bloques los he dejado comentados ya que utilizo los datos predeterminados:
# Advanced customizations
# Force Ghost version
# You should only do this if you need to pin a specific version
# The update commands won't work
# GHOST_VERSION=6-alpine
# Port Ghost should listen on
# You should only need to edit this if you want to host
# multiple sites on the same server
# GHOST_PORT=2368
En el último bloque indicamos la ubicación de los datos del servicio. Podemos indicar la carpeta de nuestro sistema que queramos. Lo más cómodo es dejarlos tal cual:
# Data locations
# Location to store uploaded data
UPLOAD_LOCATION=./data/ghost
# Location for database data
MYSQL_DATA_LOCATION=./data/mysql
Terminamos de editar el .env y ahora editaremos el archivo compose.yml para comentar las secciones que incluyen la creación del contenedor Caddy (solo las del contenedor Caddy, las de ActivityPub y Tinybird no las comentaremos pero ya el propio Ghost no las tendrá en cuenta al tenerlas sus secciones comentadas en el archivo .env). No queremos utilizar Caddy ya que vamos a utilizar el Nginx Proxy Manager que ya tenemos en activo, por lo que el compose.yml nos quedaría así:
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/main/schema/compose-spec.json
services:
# caddy:
# image: caddy:2.10.0-alpine@sha256:e2e3a089760c453bc51c4e718342bd7032d6714f15b437db7121bfc2de2654a6
# restart: always
# ports:
# - "80:80"
# - "443:443"
# environment:
# DOMAIN: ${DOMAIN:?DOMAIN environment variable is required}
# ADMIN_DOMAIN: ${ADMIN_DOMAIN:-}
# ACTIVITYPUB_TARGET: ${ACTIVITYPUB_TARGET:-https://ap.ghost.org}
# volumes:
# - ./caddy:/etc/caddy
# - caddy_data:/data
# - caddy_config:/config
# depends_on:
# - ghost
# networks:
# - ghost_network
ghost:
# Do not alter this without updating the Tinybird Sync container as well
image: ghost:${GHOST_VERSION:-6-alpine}
restart: always
expose:
- "127.0.0.1:${GHOST_PORT:-2368}:2368"
# This is required to import current config when migrating
env_file:
- .env
environment:
NODE_ENV: production
url: https://${DOMAIN:?DOMAIN environment variable is required}
admin__url: ${ADMIN_DOMAIN:+https://${ADMIN_DOMAIN}}
database__client: mysql
database__connection__host: db
database__connection__user: ${DATABASE_USER:-ghost}
database__connection__password: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
database__connection__database: ghost
tinybird__tracker__endpoint: https://${DOMAIN:?DOMAIN environment variable is required}/.ghost/analytics/api/v1/page_hit
tinybird__adminToken: ${TINYBIRD_ADMIN_TOKEN:-}
tinybird__workspaceId: ${TINYBIRD_WORKSPACE_ID:-}
tinybird__tracker__datasource: analytics_events
tinybird__stats__endpoint: ${TINYBIRD_API_URL:-https://api.tinybird.co}
volumes:
- ${UPLOAD_LOCATION:-./data/ghost}:/var/lib/ghost/content
depends_on:
db:
condition: service_healthy
tinybird-sync:
condition: service_completed_successfully
required: false
tinybird-deploy:
condition: service_completed_successfully
required: false
activitypub:
condition: service_started
required: false
networks:
- ghost_network
db:
image: mysql:8.0.42@sha256:4445b2668d41143cb50e471ee207f8822006249b6859b24f7e12479684def5d9
restart: always
expose:
- "3306"
environment:
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD:?DATABASE_ROOT_PASSWORD environment variable is required}
MYSQL_USER: ${DATABASE_USER:-ghost}
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
MYSQL_DATABASE: ghost
MYSQL_MULTIPLE_DATABASES: activitypub
volumes:
- ${MYSQL_DATA_LOCATION:-./data/mysql}:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d
healthcheck:
test: mysqladmin ping -p$$MYSQL_ROOT_PASSWORD -h 127.0.0.1
interval: 1s
start_period: 30s
start_interval: 10s
retries: 120
networks:
- ghost_network
traffic-analytics:
image: ghost/traffic-analytics:1.0.15@sha256:8d98e9f4eb623d1c7953d5a60b944db1850bc61ac4a6f637055d05b4a2be798f
restart: always
expose:
- "3000"
volumes:
- traffic_analytics_data:/data
environment:
NODE_ENV: production
PROXY_TARGET: ${TINYBIRD_API_URL:-https://api.tinybird.co}/v0/events
SALT_STORE_TYPE: ${SALT_STORE_TYPE:-file}
SALT_STORE_FILE_PATH: /data/salts.json
TINYBIRD_TRACKER_TOKEN: ${TINYBIRD_TRACKER_TOKEN:-}
LOG_LEVEL: debug
profiles: [analytics]
networks:
- ghost_network
activitypub:
image: ghcr.io/tryghost/activitypub:1.1.0@sha256:39c212fe23603b182d68e67d555c6b9b04b1e57459dfc0bef26d6e4980eb04d1
restart: always
expose:
- "8080"
volumes:
- ${UPLOAD_LOCATION:-./data/ghost}:/opt/activitypub/content
environment:
# See https://github.com/TryGhost/ActivityPub/blob/main/docs/env-vars.md
NODE_ENV: production
ACTIVITYPUB_COLLECTION_PAGE_SIZE: 20
MYSQL_HOST: db
MYSQL_USER: ${DATABASE_USER:-ghost}
MYSQL_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}
MYSQL_DATABASE: activitypub
LOCAL_STORAGE_PATH: /opt/activitypub/content/images/activitypub
LOCAL_STORAGE_HOSTING_URL: https://${DOMAIN}/content/images/activitypub
depends_on:
db:
condition: service_healthy
activitypub-migrate:
condition: service_completed_successfully
profiles: [activitypub]
networks:
- ghost_network
# Suporting Services
tinybird-login:
build:
context: ./tinybird
dockerfile: Dockerfile
working_dir: /home/tinybird
command: /usr/local/bin/tinybird-login
volumes:
- tinybird_home:/home/tinybird
- tinybird_files:/data/tinybird
profiles: [analytics]
networks:
- ghost_network
tty: false
restart: no
tinybird-sync:
# Do not alter this without updating the Ghost container as well
image: ghost:${GHOST_VERSION:-6-alpine}
command: >
sh -c "
if [ -d /var/lib/ghost/current/core/server/data/tinybird ]; then
rm -rf /data/tinybird/*;
cp -rf /var/lib/ghost/current/core/server/data/tinybird/* /data/tinybird/;
echo 'Tinybird files synced into shared volume.';
else
echo 'Tinybird source directory not found.';
fi
"
volumes:
- tinybird_files:/data/tinybird
depends_on:
tinybird-login:
condition: service_completed_successfully
networks:
- ghost_network
profiles: [analytics]
restart: no
tinybird-deploy:
build:
context: ./tinybird
dockerfile: Dockerfile
working_dir: /data/tinybird
command: >
sh -c "
tb-wrapper --cloud deploy
"
volumes:
- tinybird_home:/home/tinybird
- tinybird_files:/data/tinybird
depends_on:
tinybird-sync:
condition: service_completed_successfully
profiles: [analytics]
networks:
- ghost_network
tty: true
activitypub-migrate:
image: ghcr.io/tryghost/activitypub-migrations:1.1.0@sha256:b3ab20f55d66eb79090130ff91b57fe93f8a4254b446c2c7fa4507535f503662
environment:
MYSQL_DB: mysql://${DATABASE_USER:-ghost}:${DATABASE_PASSWORD:?DATABASE_PASSWORD environment variable is required}@tcp(db:3306)/activitypub
networks:
- ghost_network
depends_on:
db:
condition: service_healthy
profiles: [activitypub]
restart: no
volumes:
# caddy_data:
# caddy_config:
tinybird_files:
tinybird_home:
traffic_analytics_data:
networks:
ghost_network:
Finalizamos haciendo los docker-compose pull y up para levantar los contenedores:
docker compose pull
docker compose up
Ya solo resta crear la entrada en Nginx Proxy Manager para acceder a nuestro blog a través del dominio (sin olvidarnos de añadir la entrada en nuestro servicio DNS).

Ya tenemos nuestro Ghost disponible a través del subdominio que hayamos configurado y con la posibilidad de registro de nuevos usuarios. La newsletter, las estadísticas de visualización y la federación no las tendremos operativas, pero ya nos pueden leer en internet y a través del RSS 😉
Este artículo tiene una continuación aquí: