Обычный путь для личного блога — WordPress: PHP, MySQL, обновления каждую неделю и пять плагинов, чтобы получить подсветку кода. Мне нужно было другое: писать статьи в markdown, держать всё на своём сервере и понимать каждую строчку того, что там крутится.

В итоге получился hashpen — движок на Flask и SQLite, на котором работает этот сайт. Ниже — как поднять такой же у себя. Установка занимает минут двадцать, из них девятнадцать ждёшь, пока ставятся пакеты.

Что понадобится

  • VPS с Ubuntu. Подойдёт самый дешёвый: один процессор, гигабайт памяти. Блог — это отдача готового HTML, нагрузки там нет.
  • Домен. Без него сайт откроется по IP, но HTTPS выпустить не получится.
  • SSH-доступ к серверу и терминал на своём компьютере. В Windows подойдёт PowerShell, ssh и scp в нём есть из коробки.

База — SQLite, то есть один файл на диске. Отдельный сервер базы данных не нужен, и это не компромисс: для блога с тысячей статей SQLite быстрее любого Postgres, потому что данные лежат рядом с приложением.

Шаг 1. Настроить домен

В панели управления доменом сделайте A-запись на IP сервера — для @ и для www.

Теперь важное, на чём я потерял полчаса. Проверьте, нет ли лишних AAAA-записей — это адреса IPv6. Если домен раньше где-то парковался, такая запись может указывать на чужой сервер. Let's Encrypt предпочитает IPv6 и пойдёт проверять домен именно туда, а в ответ получит чужую страницу и откажется выдавать сертификат с загадочной ошибкой про 404.

Проверить можно с любого компьютера:

bashdig +short A example.ru
dig +short AAAA example.ru

Первая команда должна вернуть IP вашего сервера, вторая — пустоту, если у сервера нет IPv6. Всё лишнее удалите в панели домена и подождите, пока разойдётся DNS: обычно минуты, но по правилам может занять до суток.

Шаг 2. Подготовить сервер

Заходим по SSH и обновляем систему:

bashssh root@ВАШ_IP
apt update && apt upgrade -y

Больше ничего руками ставить не нужно — об остальном позаботится установщик.

Шаг 3. Поставить движок

Скачиваем проект и запускаем установку:

bashapt install -y git
git clone https://github.com/xor0x1/hashpen.git /opt/hashpen
cd /opt/hashpen
sudo bash install.sh

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

  • ставит Python, nginx и sqlite3;
  • заводит отдельного системного пользователя — приложение не будет работать от root;
  • создаёт виртуальное окружение и ставит зависимости (их четыре штуки);
  • генерирует .env со случайным ключом сессий;
  • создаёт базу;
  • пишет службу systemd, чтобы сайт поднимался сам после перезагрузки;
  • настраивает nginx: статику и картинки он отдаёт напрямую, всё остальное передаёт приложению;
  • заводит ежедневный бэкап базы и загруженных картинок;
  • проверяет, что сайт отвечает.

В конце скрипт печатает два оставшихся шага. Открывайте http://ваш-домен — сайт уже работает, просто пока пустой.

Шаг 4. Настроить вход

Блог — штука персональная, поэтому вход сразу с двумя факторами: пароль и код из приложения-аутентификатора.

bashcd /opt/hashpen
sudo -u hashpen .venv/bin/python setup-auth.py
systemctl restart hashpen

Скрипт спросит пароль дважды (ввод не отображается, это нормально), покажет QR-код для Google Authenticator, Aegis или 2FAS и выдаст восемь резервных кодов.

Резервные коды выпишите и положите не на тот телефон, где стоит аутентификатор. Это единственный способ попасть в админку, если телефон утонет. Каждый код работает один раз.

Терминал после этого закройте: в нём остался ключ.

Шаг 5. Выпустить сертификат

bashapt install -y certbot python3-certbot-nginx
certbot --nginx -d example.ru -d www.example.ru

Certbot сам поправит конфиг nginx и настроит автопродление. После этого допишите в .env:

SITE_URL=https://example.ru
SESSION_COOKIE_SECURE=1

и перезапустите службу.

Вторая строка означает, что cookie сессии передаются только по HTTPS. Пока сайт работал по http, там должен был стоять 0 — иначе браузер просто выбрасывает cookie, и вход молча возвращает вас на форму ввода пароля без всякой ошибки. Симптом ровно такой: «ввожу правильный пароль, а ничего не происходит».

SITE_URL нужен для RSS, карты сайта и canonical-ссылок: без него поисковик может решить, что у вас два разных сайта по двум адресам.

Шаг 6. Написать первую статью

Заходите на https://example.ru/login, вводите пароль и шесть цифр из приложения. В шапке появится кнопка «Написать статью».

Текст пишется в markdown. Заголовки разделов делаются решётками, и из них автоматически собирается оглавление. Для кода указывайте язык сразу после кавычек:

```bash
systemctl restart hashpen
```

Тогда команды, флаги и строки раскрасятся, а в углу блока появится метка языка. Если язык не указать, блок останется обычным моноширинным текстом — автоопределение намеренно выключено, потому что на двух строчках оно чаще ошибается, чем угадывает.

Картинку можно перетащить прямо в поле текста, вставить из буфера через Ctrl+V или выбрать кнопкой. Она загрузится на сервер, а в текст подставится ссылка.

Галочка «Черновик» внизу оставляет статью видимой только вам: удобно писать в несколько заходов. Ctrl+S сохраняет, не отрывая рук от клавиатуры.

Если статьи уже написаны и лежат файлами, их можно загрузить пачкой:

bashsudo -u hashpen .venv/bin/flask --app wsgi import-md ./articles

Шаг 7. Настроить внешний вид

После входа в меню слева появляются «Настройки сайта». Там меняются название, подзаголовок, описание для поисковиков, цвет акцента отдельно для тёмной и светлой темы, тема по умолчанию и номер счётчика Яндекс.Метрики.

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

Счётчик Метрики, кстати, загружается только после того, как посетитель нажмёт «Принять» в баннере о cookies. Это не перестраховка, а требование 152-ФЗ: аналитика ставит свои cookie, а значит, нужно согласие.

Шаг 8. Проверить бэкапы

Установщик уже завёл ежедневное копирование в /srv/backups, но убедиться стоит сразу, а не в день, когда что-то сломается:

bash/etc/cron.daily/hashpen-backup && ls -lh /srv/backups

Копии лежат на том же сервере — это защита от кривых рук, но не от смерти диска. Раз в месяц имеет смысл забирать их к себе:

bashscp root@ВАШ_IP:/srv/backups/* .

Частые ошибки

Сайт открывается, но выглядит как голый HTML без стилей. nginx не может прочитать папку со статикой. Чаще всего — из-за прав на домашнюю папку пользователя, под которым работает приложение. Смотрим лог и правим:

bashtail -5 /var/log/nginx/error.log
chmod 751 /opt/hashpen

Команды импорта выполняются, а сайт статей не видит. Скорее всего, команда запускалась от root и создала свою базу рядом. Все команды движка нужно запускать от имени его пользователя, через sudo -u hashpen.

Вход не работает по http. См. шаг 5: SESSION_COOKIE_SECURE=0, пока нет сертификата.

Certbot не выдаёт сертификат с ошибкой 404. См. шаг 1: лишняя AAAA-запись.

Что в итоге

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

Движок лежит на GitHub под лицензией MIT: берите, меняйте, ставьте на свои сайты. Если что-то не заведётся — пишите в Issues, отвечу.