Создаем блог на Ghost

Рассказываю о преимуществах движка для блогов Ghost. Также делюсь пошаговой инструкцией по настройке своего Ghost сайта.

· 11 мин.
Создаем блог на Ghost

Раньше я был адептом статических генераторов сайтов, таких как Hugo. Подробнее о преимуществах статических генераторов сайтов я писал в отдельной статье.

Но есть существенный минус таких генераторов: Нет визуального редактора для статей, их приходится оформлять в формате Markdown через "блокнот".

Я использовал IDE WebStorm для написания статей.

Через год я утомился писать все в IDE, поэтому начал искать замену для Hugo с удобным визуальным редактором. На просторах интернета мне посоветовали обратить внимание на движок Ghost. Теперь этот блог работает на нем 😄

Короткий обзор Ghost

Коротко расскажу о возможностях Ghost. Если вы и так все знаете, то переходите к установке.

Удобный редактор статей

Редактор постов поддерживает Markdown и визуальное редактирование текста.

С помощью горячих клавиш или всплывающих меню можно легко менять шрифты, вёрстку и добавлять нужные элементы.

Редактор выглядит так. Произошла рекурсия 👁

Интеграции

Сначала я был рад интеграциям, думал что они расширяют функционал и это круто. По факту большинство нужных мне интеграций доступны только через сервис Zapier. А он платный и дорогой.

Бесплатно в Zapier можно сделать только одну интеграцию, например автоматический постинг в твиттер после публикации новой статьи. Но сделать постинг в телеграм уже не получится, нужно покупать подписку.

При желании и навыках программирования, можно бесплатно написать любые интеграции с использованием Webhook.

Ограничение доступа и монетизация

Для контент мейкеров будет отдичная новость, что Ghost поддерживает из коробки подписочную модель. Вы можете делать статьи только для зарегистрированных пользователей, или только для платных подписчиков.

Имеется возможность прикрутить для оплаты Stripe, Patreon или PayPal. Сама платформа Ghost не берёт никаких комиссий.

Прочие плюсы

  • Возможность публикации по рассписанию
  • Командная работа. Вы можете организовать небольшую редакуцию. Присутствует разграничение по ролям: редактор, автор и прочие.
  • RSS лента
  • Теги для постов
  • SEO Оптимизация сайта из коробки
  • Активно развивающийся. Пока я писал статью, вышло несколько мажорных обновлений.

Минусы Ghost

Пожалуй единственный спорный момент это отсутствие кластеризации и ее не будет. Буквально официальная позиция создателей Ghost по этому вопросу: "Если вам понадобилась кластеризация, значит вы делает что-то не так. Оптимизируйте ваш Nginx."

Я решил прогнать простенькое нагрузочное тестирование. В целом результаты хорошие, блог на Hugo показывал примено такие же результаты под такой же нагрузкой.

На этом заканчиваем короткий обзор и переходим к установке. Более подробный обзор возможностей смотрите на официальном сайте Ghost или вот в этой статье.

Есть два варианта создания сайта Ghost:

  • Самый простой и самый дорогой – это Ghost Pro. Создатели Ghost предлагают все настроить за вас, вам останется только писать статьи. Если программирование это не про вас, то это лучший вариант.
  • Если вы технически подкованный специалист, или не боитесь попробовать самостоятельно, то ниже вы найдете поэтапную инстркцию, как запустить Ghost на своем сервере.

Что мы получим

В процессе статьи, мы запустим свой Ghost сайт на арендованном сервере. Для этого мы разберемся как зпустить Ghost, Nginx, CertBot и MarinaDB в Docker-Compose.

Nginx мы будем использовать в качестве веб-сервера. MarinaDB для хранения наших статей, а CertBot позволит нам получать ssl сертификат, чтобы наш сайт работал на https.

Также с помощью cron и небольшого скрипта, мы настроим автоматичекое обновления бесплатных сертификатов LetEncript.

Настройка Ghost на сервере

Для начала нужно арендовать сервер. Я пользуюсь услугами хостинг-провайдера TimeWeb и полностью ими доволен.

По этой реферальной ссылке вам дадут 200 рублей, этого хватит на месяц базового тарифа.

Я рассматриваю CentOS 8, поэтому все инструкции пишу для него, если вы выберете другую ОС, то можете столкнуться с проблемами. Первым делом после входа в систему обновляем пакеты.

yum update
Гайд по защите Linux сервера от взлома
Базовая защита от взлома вашего linux сервера. Запрет входа для root пользователя, смена стандартного порта и другие способы защиты.
Более подробно о защите сервера, я писал в этой статье

Установка Docker

Обновление займет какое-то время. После обновления необходимо установить docker и docker-compose.

docker:

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli
sudo systemctl start docker
sudo systemctl enable --now docker

docker-compose:

curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

Проверяем, что все прошло успешно:

docker-compose --version
docker-compose version 1.29.2, build 5becea4c

Настройка DNS

Для нашего сайта нам нужен домен. Если вы его еще не купили, то рекомендую REG.RU. А вот промокод на скидку 5%: E403-2760-B801-2B3B

После покупки нам необходимо добавить ресурсные записи DNS для домена.

Если вы использовали REG.RU, то необходимо зайти в список ваших доменов, нажать на необходимый домен и там вы увидите

Нажмите изменить, удалите все что там есть, часто там установлены ip регистратора для рекламы услуг. Теперь вам нужно добавить две записи, чтобы наш домен ассоциировался с нашим сервером.

Тип записи: A
Subdomain: @
Ip addres: ip_адрес_вашего_сервера

Тип записи: A
Subdomain: www
Ip addres: ip_адрес_вашего_сервера

Результат будет выглядить как-то так:

Обновление ресурсных записей может занять от 15 минут до 24 часов. Все зависит от регистратора. На reg.ru обновление занимает обычно минут 15.

Сертификат SSL

Пока ресурсные записи обновляются возвращаемся к нашему серверу. В папке root создаем папку site и перейдем в нее.

mdkir /root/site
cd /root/site

В 2021 году без https никуда, поэтому нужно получить ssl сертификат для нашего домена. Для этого создадим специальный docker-compose и nginx конфигурацию, которые нужны исключительно для получения сертификата.

Создаем новый файл docker-compose.ssl.yaml

nano docker-composer.ssl.yaml

Содержимое файла:

version: '3.3'

services:
  nginx:
    image: nginx
    container_name: blog-nginx
    hostname: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.ssl.conf:/etc/nginx/nginx.conf
      - ./temp:/site
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt

  certbot:
    image: certbot/certbot
    container_name: blog-certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - ./temp:/var/www/html
    depends_on:
      - nginx
    command: certonly --webroot --webroot-path=/var/www/html --email you@domain.com --agree-tos --no-eff-email --staging -d youdomain.com -d www.youdomain.com
volumes:
  certbot-etc:
  certbot-var:

В строке 27 укажите ваш домен вместо youdomain.com и вашу почту вместо you@domain.com.

Этот композ содержит контейнер с nginx и certbot для получения и сохранения ssl сертификата. Полученный сертификат мы сохраним в docker volume certbot-var. Более подробно мы рассмотрим второй композ, который будет запускать сайт.

Теперь создадим конфигурацию nginx.

mkdir nginx.ssl.conf

Содержимое файла:

events {
    worker_connections 1024;
}

http {
    include    mime.types;
    sendfile on;

    server {
	    listen 80;
        listen [::]:80;
        server_name youdomain.com www.youdomain.com;

        location / {
            root /blog;
        }

	    location ~ /.well-known/acme-challenge {
            allow all;
            root /blog;
        }
    }
}

В 12 строке не забудьте поменять youdomain.com на ваш домен.

Запускаем композ:

sudo docker-compose -f docker-compose.ssl.yaml up

Если все пройдет успешно, то вы увидите вот эту надпись

blog-certbot | 
blog-certbot | Successfully received certificate.
blog-certbot | Certificate is saved at: /etc/letsencrypt/live/youdomain.com/fullchain.pem
blog-certbot | Key is saved at:         /etc/letsencrypt/live/youdomain.com/privkey.pem
blog-certbot | This certificate expires on 2021-12-08.
blog-certbot | These files will be updated when the certificate renews.
blog-certbot | 
blog-certbot | NEXT STEPS:
blog-certbot | - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
blog-certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log
blog-certbot exited with code 0

Останавливаем композ сочетанием Ctrl+C и убиваем контейнеры командой:

sudo docker-compose -f docker-compose.ssl.yaml down

На самом деле мы не получили сертификат, а лишь проверили возможность его получения. Это нужно чтобы не превысить лимит получения сертификатов, который устанавливает LetEncript. Теперь мы точно уверены, что нам дадут сертификат.

Чтобы получить действующие сертификаты нужно заменить --staging на --force-renewal в docker-compose.ssl.yaml

Если все пройдет успешно, то увидите вот это

blog-certbot | 
blog-certbot | Successfully received certificate.
blog-certbot | Certificate is saved at: /etc/letsencrypt/live/youdomain.com/fullchain.pem
blog-certbot | Key is saved at:         /etc/letsencrypt/live/youdomain.com/privkey.pem
blog-certbot | This certificate expires on 2021-12-08.
blog-certbot | These files will be updated when the certificate renews.
blog-certbot | 
blog-certbot | NEXT STEPS:
blog-certbot | - The certificate will need to be renewed before it expires. Certbot can automatically renew the certificate in the background, but you may need to take steps to enable that functionality. See https://certbot.org/renewal-setup for instructions.
blog-certbot | Saving debug log to /var/log/letsencrypt/letsencrypt.log
blog-certbot | 
blog-certbot | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
blog-certbot | If you like Certbot, please consider supporting our work by:
blog-certbot |  * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
blog-certbot |  * Donating to EFF:                    https://eff.org/donate-le
blog-certbot | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
blog-certbot exited with code 0

Мы получили сертификат и сохранили его в volume докера. Снова убиваем запущенные контейнеры Ctrl+C:

docker-compose -f docker-compose.ssl.yaml down

Создание docker-compose

Теперь создаем новый файл композа, который будет запускать окружение для нашего сайта: Ghost, Nginx, MarinaDB и CertBot.

nano docker-compose.yaml

Содержимое файла:

version: '3.3'

services:
  mariadb:
    container_name: site-mariadb
    image: mariadb:${MARIADB_IMAGE_TAG}
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: ghost_production
    restart: always
    volumes:
      - ${MYSQL_HOST_PATH}:/var/lib/mysql

  nginx:
    image: nginx
    container_name: site-nginx
    hostname: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - ghost
    volumes:
      - ./nginx/sites:/etc/nginx/conf.d
      - ./nginx/ssl:/etc/nginx/ssl
      - ./temp:/site
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt

  ghost:
    container_name: site-ghost
    image: ghost:${GHOST_IMAGE_TAG}
    hostname: ghost
    environment:
      url: ${BLOG_URL}
      database__client: mysql
      database__connection__host: mariadb
      database__connection__database: ghost_production
      database__connection__user: ghost
      database__connection__password: ${MYSQL_PASSWORD}
    depends_on:
      - mariadb
    restart: always
    volumes:
      - ${GHOST_HOST_PATH}:/var/lib/ghost/content
      
  certbot:
    image: certbot/certbot
    container_name: blog-certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - ./temp:/var/www/html
    depends_on:
      - nginx
    command: certonly --webroot --webroot-path=/var/www/html --email you@domain.com --agree-tos --no-eff-email --staging -d yourdomain.com -d www.yourdomain.com

volumes:
  certbot-etc:
  certbot-var:
networks:
  default:
    name: "site"

В 57 строке не забываем заменить значения домена и email на свои.

Мы создаем несколько контейнеров:

  • MarinaDB – там хранятся наши посты
  • Nginx в качестве веб-сервера
  • Ghost как движок для сайта
  • CertBot для обновления сертификата каждые 90 дней.

Также создаем файл с переменными для docker-compose:

nano .env

Содержимое файла:

export GHOST_IMAGE_TAG=4.21.0
export MARIADB_IMAGE_TAG=latest
export BLOG_URL=https://youbomain.com
export MYSQL_ROOT_PASSWORD=password_root
export MYSQL_PASSWORD=password
export MYSQL_HOST_PATH=./mariadb
export GHOST_HOST_PATH=./www
  • GHOST_IMAGE_TAG – версия Ghost. На момент написания статьи актуальная 4.21.0. Посмотреть текущую актуальную можно в Docker Hub.
  • MARIADB_IMAGE_TAG – версия MarinaDB. Берется последняя доступная версия.
  • BLOG_URL – адрес вашего сайта. Заменить на свой домен.
  • MYSQL_ROOT_PASSWORD – пароль для root пользователя БД.
  • MYSQL_PASSWORD – пароль БД для пользователя ghost.
  • MYSQL_HOST_PATH и GHOST_HOST_PATH – пути к файлам в контейнере для БД и Ghost.

Теперь создаем папки для конфигурации nginx:

mkdir nginx
cd nginx
mkdir ssl
mkdir sites

Для безопасного SSL соединения сгенерируем ключ DH:

openssl dhparam -out ./ssl/dhparam.pem 4096

Генерация может занять продолжительное время. У меня она заняла минут десять.

Так выглядит процесс генерации

Теперь создадим конфигурацию nginx:

nano ./sites/site.conf

Содержимое файла:

server {
    listen 80;
    listen [::]:80;

    server_name yourdomain.com www.yourdomain.com;

    location ~ /.well-known/acme-challenge {
            allow all;
            root /site;
    }

    location / {
        return 301 https://yourdomain.com$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    ssl_buffer_size 8k;

    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    ssl_protocols SSLv3 TLSv1.2;
    ssl_prefer_server_ciphers on;

    ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;

    ssl_ecdh_curve secp384r1;
    ssl_session_tickets off;

    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Proto   $scheme;
        proxy_connect_timeout                 90;
        proxy_send_timeout                    90;
        proxy_read_timeout                    90;
        proxy_buffering off;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
    
        client_max_body_size                  10m;
        client_body_buffer_size               128k;

        proxy_pass http://ghost:2368;
     }
}

В строчках 5,13,20,22,23 не забудьте заменить значение на свой домен.

Переходим обратно в папку site и запускаем наш новый композ:

docker-compose up

Переходим по адресу нашего домена и смотрим на результат

Эта тема установлена по умолчанию

Видим что все получилось. Наш сайт запустился с дефолтной Ghost темой. Также на сайте уже есть стандартные обучающие статьи, которые вы можете удалить из Панели Управления (ПУ).

Иногда при попытке зайти на сайт после запуска композа все виснет, показывается белая страница и загрузка, хотя в логах все чисто. Решение этой проблемы это остановка композа и запуск его снова.

Мы убедились что все работает. Теперь запустим композ в фоновом режиме, для этого останавливаем текущий запуск Ctrl+C и запускаем по новой с флагом  -d:

docker-compose up -d

Теперь нам нужно создать администратора для панели управления Ghost. Для этого нужно перейти по адресу https://yourdomain.com/ghost.

Сайт запущен и вроде бы можно писать статьи, но остался один важный момент.

Обновление сертификата

Сертификаты Let’s Encrypt действительны в течение 90 дней, поэтому вам нужно будет настроить автоматический процесс обновления.

Один из способов — создание задания с помощью утилиты планирования cron. В нашем случае мы настроим задание для cron с помощью скрипта, который будет обновлять наш сертификат и перезагружать конфигурацию Nginx.

Создадим скрипт для обновления сертификата:

nano ssl_renew.sh

Добавьте следующий код в скрипт, чтобы обновить ваши сертификаты и перезагрузить конфигурацию веб-сервера:

#!/bin/bash

/usr/local/bin/docker-compose -f /root/site/docker-compose.yaml run certbot renew --dry-run && /usr/local/bin/docker-compose -f /root/site/docker-compose.yaml kill -s SIGHUP nginx

Мы будем использовать команду docker-compose run для запуска контейнера certbot и переопределения команды command на другую команду — субкоманду renew, которая будет обновлять сертификаты, срок действия которых близок к окончанию. Мы включили параметр --dry-run, чтобы протестировать наш скрипт.

Закройте файл после завершения редактирования. Сделайте его исполняемым:

chmod +x ssl_renew.sh

Проверяем, что скрипт отработает нормально:

sh ssl_renew.sh

Симуляция прошла успешно. Теперь создаем задачу cron:

crontab -e

По умолчанию открывается редактор vi. Чтобы редактировать файл вам нужно нажать клавишу i после чего вставить:

*/5 * * * * /root/site/ssl_renew.sh >> /var/log/cron.log 2>&1

После этого нажмите esc , а далее клавишу :. После этого введите wq и нажмите Enter. Таким образом мы установили задачу на выполнение каждые 5 минут. Через 5 минут проверяем результат выполнения:

tail -f /var/log/cron.log

Вы должны увидеть вывод, подтверждающий успешное обновление сертификата:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for sadtech.org and www.sadtech.org

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded: 
  /etc/letsencrypt/live/sadtech.org/fullchain.pem (success)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Killing site-nginx ... 
Killing site-nginx ... done

Теперь вы можете изменить файл crontab для настройки ежедневного интервала. Чтобы запускать скрипт каждый день в полночь, например, вы должны изменить последнюю строку файла, которая должна выглядеть следующим образом:

0 0 * * * /root/site/ssl_renew.sh >> /var/log/cron.log 2>&1

Не забудьте удалить флаг --dry-run из скрипта ssl_renew.sh. Если этого не сделать, то сертификат не будет обновляться.

Заключение

Мы разобрались как запустить сайт Ghost на своем сервере в docker-compose. Теперь вы можете писать свои статьи, в следующей статье я расскажу, как прикрутить к вашему сайту систему комментариев Remark42.