Испытание: миграция с Scaleway на Raspberry Pi 4

Здесь документация о моих ошибках и о том, как я в итоге успешно мигрировал экземпляр Discourse с Scaleway на Raspberry Pi 4 с Cloudflare перед ним.

Создал резервную копию экземпляра Discourse на Scaleway, выполнил ./launcher stop app и выключил машину.

Установил Ubuntu Server 23.10 на подключённый через USB SATA SSD, который питает Raspberry Pi 4.
Установил LXD, создал пул хранения btrfs с файлом loopback объёмом 100 ГиБ.
Обновил профиль default до следующего:

config:
  cloud-init.user-data: |
    #cloud-config
    ssh_pwauth: false
    package_update: true
    package_upgrade: true
    packages:
      - openssh-server
      - vim
      - git
      - rsync
    users:
      - name: root
        lock_passwd: true
        ssh_import_id: gh:balupton
description: Default LXD profile
devices:
  eth0:
    name: eth0
    network: lxdbr0
    type: nic
  root:
    path: /
    pool: default
    type: disk
name: default

Добавил профиль discourse со следующим содержимым:

config:
  limits.memory: 1GiB
  limits.memory.enforce: soft
  security.nesting: 'true'
description: Configuration for Discourse instances
devices: {}
name: discourse

Создал образ Ubuntu 23.10 Minimal Server с этими профилями. Подключился к нему через настройки в файле ~/.ssh/config:

Host LXD_DISCOURCE_INSTANCE
	ProxyJump LXD_HOST
	User REDACTED
	IdentityFile ~/.ssh/REDACTED.pub

Следовал инструкциям по облачной установке Discourse и восстановил конфигурацию моего экземпляра Discourse с машины Scaleway:

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/cloudflare.template.yml"
  - "templates/web.ssl.template.yml"  # https
  - "templates/web.letsencrypt.ssl.template.yml"  # https
  # - "templates/web.ratelimited.template.yml" # not needed with cloudflare

expose:
  - "80:80"
  - "443:443"  # https

params:
  db_default_text_search_config: "pg_catalog.english"

env:
  LANG: en_US.UTF-8

  ## HTTPS configuration for: templates/web.letsencrypt.ssl.template.yml
  LETSENCRYPT_ACCOUNT_EMAIL: "redacted"  # https

  ## The domain name this Discourse instance will respond to
  DISCOURSE_HOSTNAME: "redacted"

  ## List of comma delimited emails that will be made admin and developer
  ## on initial signup example 'user1@example.com,user2@example.com'
  DISCOURSE_DEVELOPER_EMAILS: "redacted"

  ## The mailserver this Discourse instance will use
  DISCOURSE_SMTP_ADDRESS: "redacted"
  DISCOURSE_SMTP_PORT: redacted
  DISCOURSE_SMTP_USER_NAME: "redacted"
  DISCOURSE_SMTP_PASSWORD: "redacted"
  #DISCOURSE_SMTP_DOMAIN: discourse.example.com    # (required by some providers)
  #DISCOURSE_NOTIFICATION_EMAIL: nobody@discourse.example.com    # (address to send notifications from)
  #DISCOURSE_MAXMIND_LICENSE_KEY: 1234567890123456

## Any custom commands to run after building
run:
  - exec: rails r "SiteSetting.contact_email='redacted'"
  - exec: rails r "SiteSetting.notification_email='redacted'"

## The Docker container is stateless; all data is stored in /shared
volumes:
  - volume:
      host: /var/discourse/shared/standalone
      guest: /shared
  - volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log

## Plugins
## https://meta.discourse.org/t/19157
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/discourse-adplugin.git
          - git clone https://github.com/discourse/discourse-affiliate.git
          - git clone https://github.com/discourse/discourse-assign.git
          - git clone https://github.com/discourse/discourse-docs.git
          - git clone https://github.com/discourse/discourse-topic-voting.git
          - git clone https://github.com/discourse/discourse-github.git
          - git clone https://github.com/discourse/discourse-saved-searches.git
          - git clone https://github.com/discourse/discourse-shared-edits.git
          - git clone https://github.com/discourse/discourse-solved.git
          # - git clone https://github.com/discourse/discourse-encrypt.git
          # - git clone https://github.com/discourse/discourse-reactions.git
          # - git clone https://github.com/discourse/discourse-subscriptions.git

Однако перед восстановлением резервной копии мне пришлось пересобрать контейнер. К сожалению, пул хранения btrfs зависал на этапе установки yarn на несколько часов и в итоге истекало время ожидания, при этом нагрузка на машину была практически нулевой.

Прочитав несколько статей, я решил попробовать использовать пул хранения ZFS. Это позволило продвинуться дальше, но процесс всё равно зависал бесконечно после сообщения Background saving terminated with success, при этом нагрузка на машину оставалась практически нулевой.

(У меня есть скриншоты, но загрузить их сюда не удаётся.)

Тогда я решил отказаться от LXD и попробовать запустить всё напрямую на экземпляре Ubuntu Server на Raspberry Pi 4.
Впервые мне удалось успешно пересобрать контейнер, однако все попытки доступа к нему приводили к бесконечному циклу перенаправлений (redirect loop).

Цикл перенаправлений был вызван двумя причинами…

Если в конфигурации Discourse было указано следующее:

expose:
  - "8080:80"
  - "8081:443"  # https

то происходило бесконечное перенаправление, всегда стремящееся перенаправить на https://hostname.
Решением было возвращение к следующему:

expose:
  - "80:80"
  - "443:443"  # https

Во-вторых, любой доступ через туннель Cloudflare также приводил к бесконечному перенаправлению на самого себя. Оказалось, что причиной было наличие туннеля как для HTTP, так и для HTTPS. Решение состояло в изменении туннеля так, чтобы он использовался только для HTTPS.

Другие действия, которые я предпринял, но сейчас не уверен, повлияли ли они:

  1. Я отказался от Let’s Encrypt, используя вместо этого сертификат Origin от Cloudflare.
  2. В туннеле HTTPS я настроил Origin Server Name равным ожидаемому имени хоста.

Возможные улучшения:

  1. HTTPS от Origin до Cloudflare можно было бы избежать, если ограничить доступ к машине только подключениями от Cloudflare и настроить SSH-туннель. Однако я не уверен, работает ли Discourse лучше при наличии собственного HTTPS (например, HTTP/2 и т. д.).
  2. Работает ли Let’s Encrypt с туннелем Cloudflare (мне не удалось это проверить, так как при использовании Let’s Encrypt я сталкивался с циклами перенаправлений).

Как я отлаживал циклы перенаправлений:

  • Для отладки цикла перенаправлений в Discourse: я добавил запись в /etc/hosts, чтобы имя хоста Discourse указывало напрямую на IP-адрес, затем использовал curl -k --head 'https://hostname:8081 и т. д. для тестирования.
  • Для отладки цикла перенаправлений в туннеле Cloudflare: я удалил эту запись из /etc/hosts, чтобы имя хоста разрешалось через DNS, затем использовал curl -k --head 'https://hostname и т. д. для тестирования.

Есть ещё много интересных вещей и уроков, полученных по пути, но это можно отложить на потом.

Обратная связь для проекта Discourse:

  • Процесс пересборки должен быть более прозрачным в том, что именно происходит. Слишком часто наблюдаются длительные задержки без видимых действий.
  • Выяснить, почему указание разных портов вызывает цикл перенаправлений.
  • С момента появления Let’s Encrypt документация о том, как указать собственный SSL-сертификат, стала труднодоступной. Кроме того, похоже, что можно использовать только один сертификат, так как он зафиксирован по пути /var/discourse/shared/standalone/ssl/ssl.key вместо, например, /var/discourse/shared/standalone/ssl/CONTAINER_ID.key, скажем, /var/discourse/shared/standalone/ssl/app.key. Cloudflare предоставляет сертификаты Origin, что является хорошим вариантом для пользователей Cloudflare.
  • Публикация исчерпывающего пошагового руководства по связке Cloudflare + Raspberry Pi 4 очень помогла бы. В настоящее время такие руководства перекладывают слишком много информации на третьи стороны, которые не знают о существовании друг друга, а вся сложность и отладка заключаются в том, как работают разные части вместе, а не по отдельности.

Другие задачи на будущее:

  • Выяснить, почему процесс зависал в LXD.
  • Проверить, работает ли это в LXD на Raspberry Pi 5, или в Multipass на macOS, или в LXD, если пул хранения — это раздел/диск, а не файл loopback: тогда мне не нужно выделять под это целую машину.
  • Посмотреть, можно ли заставить Docker и launcher работать без sudo.
3 лайка