7 min

CMS Ghost en Docker con Nginx Proxy Manager (1)

Un fantasmita sonrojado
Imagen de Anthony Hortin / Unsplash

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 
Una vez que termine de levantar los contenedores (el de Ghost y el MySQL), por lo menos yo, he tenido que exponer el puerto 2368 del contenedor con el de la máquina real manualmente ya que no me lo hizo de manera automática.

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).

Detalle del proxy host para blog.genscorp.es. Se observa que el subdominio blog.genscorp.es apunta a la IP local y al puerto 2368
Detalle del proxy host para blog.genscorp.es

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í:

CMS Ghost en Docker con Nginx Proxy Manager (2)
Continuando con el artículo de instalación de Ghost, en esta segunda parte vamos a abordar la configuración del envío de las newsletters en Ghost. Configuración de la Newsletter Por desgracia, Ghost no permite utilizar un servidor de email propio para el envío de estos correos mencionando un riesgo de seguridad

CC BY NC SA