Домашнее облако: как я построил цифровой «бункер» для важных данных

от автора

Фото важных семейных событий и видео из путешествий, копии важных документов, музыка, фильмы, которых не найти на стримингах — многие задумывались, как сохранить все самое важное так, чтобы ситуация с не вовремя сломавшейся флешкой не обернулась потерей ценных данных навсегда. Кому-то для спокойствия достаточно Google Drive или Яндекс.Диска, но я решил пойти чуть дальше и построить собственное домашнее облако с приложениями Immich и Nextcloud.

Кстати, привет, Хабр! Я Денис Петухов, Python-разработчик в Cloud.ru и сегодня я расскажу, как построил облако в шкафу. По ходу дела дам практические рекомендации по архитектуре, выбору оборудования, приложений, настройке сети и даже приведу расчеты того, что выгоднее, и сколько электричества «съедает» домашняя хранилка в месяц.

Почему я решил делать облако
Что почем: считаем стоимость и определяем приоритеты
Выбор накопителей
Выбор железа
Архитектура облака
Настройка
Развертывание приложений
Что в итоге

Почему я решил делать облако

Расскажу немного о себе: я увлекаюсь фотографией, мой архив хранит порядка 60 000 различных снимков. Кстати, снимаю в RAW. Еще люблю играть на музыкальных инструментах и записывать каверы. Катаюсь на горных лыжах и велосипеде, что периодически снимаю на GoPro. Из сентиментальных соображений создал образ своего 20-летнего компа, где хранится все, что я делал с первого класса: от первых сочинений до проекта тюнинга велосипеда. Ну и еще я активно развиваюсь в профессии: видеоуроки, образы разного софта и систем — все это тоже требует места для хранения. А еще я изрядно оброс цифровым наследством: один мой дедушка занимался записью телерадиопередач и сохранил целый архив, а второй увлекался фотографией и оставил много кадров, которые тоже хотелось бы оцифровать и сохранить.

Какое-то время я довольствовался жесткими дисками и флешками, пока однажды в 2013 году у меня внезапно не отказала карта памяти с 32 гигабайтами видео из путешествия. Ее я на всякий случай сохранил, но решил: «хватит это терпеть» и приобрел свой первый NAS для резервных копий внешних дисков и ноутбука. Это был Seagate Central на 4 TБ, и все с ним было радужно, пока в 2016 году он просто не перестал включаться. По совпадению, в следующем году меня предал еще и iPhone, забрав все данные за 2015-2017 год, которые я не успел перенести.

Наученный горьким опытом, я включил копирование фотографий в облако, завел NAS от Synology на 6 ТБ и решил автоматизировать резервное копирование. Однажды на форуме я нашел уникальную программу для GoPro и смог восстановить видео с той карты памяти — она ждала своего часа целых 7 лет. А в 2024 году я разговорился с приятелем, который сделал домашнее облако на NAS от Synology и научил им пользоваться всю семью. Мне понравилась идея использовать отдельные устройства для хранения данных — так же, как роутеры используют для доступа к сети. В общем, так я окончательно вдохновился на создание собственного облака.

А еще — этот проект стал для меня чем-то вроде профессиональной тренировки. Я погрузился в новую область на интересной практической задаче: изучил все тонкости, применил знания и в итоге закрепил навыки, которые в будущем могут пригодиться мне в работе.

Что почем: считаем стоимость и определяем приоритеты

Для начала я прикинул объемы своего цифрового имущества. Получилось примерно следующее: 4 ТБ чего-то полезного, что не хотелось бы потерять ни при каких условиях. Это то, что необходимо бэкапить в первую очередь, а если придерживаться стратегии 3-2-1, копий самых важных данных должно быть три: две на разных носителях и еще одна за пределами дома. Еще 10 ТБ хотелось бы хранить, но бэкапить не обязательно (софт, образы виртуалок, RAW-файлы, цифровое наследство). Итого в общей сложности нужно 22 ТБ дискового пространства. Где его взять? Вариантов было несколько:

  1. Облачное хранилище от какого-нибудь провайдера. Беглый мониторинг показал, что 1 ТБ на 10 лет обойдется от 23 000 до 55 000 рублей, и это без учета того, что стоимость может расти, а доступ ограничиваться из-за всяких событий в мире.

  2. Покупка HDD-дисков. Я рассматривал новые и исправные б/у варианты. Хранение 1 ТБ на своих дисках стоит от 1 000 до 10 000 рублей. Однако, учитывая, что 4 ТБ я планировал хранить по стратегии 3-2-1, стоимость хранения самых важных данных возрастала до 3 000–30 000 за 1 ТБ.

Считаем, во сколько обойдется хранение 22 ТБ данных в течение 10 лет:

  • Облачное хранение: 14 ТБ в облаке на 10 лет обойдется в 322 000–770 000 рублей.

  • Смешанное хранение (облако + диски): 4 ТБ в двух облаках и 10 ТБ на дисках — 234 000–490 000 рублей.

  • Хранение на дисках: 4 ТБ в трех копиях и 10 ТБ на дисках — получается 130 000 рублей за оборудование и диски.

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

  • Кроссплатформенный доступ. Поскольку я пользуюсь и Android, и iOS, важно было, чтобы доступ сохранялся с любых устройств.

  • Обязательно доступ через веб и через приложение для мобильного.

  • Позволяет реализовать резервное копирование по стратегии 3-2-1.

  • Позволяет быстро освободить место в смартфоне.

  • Помещается в шкафу.

  • Не шумнее холодильника.

  • Сами данные хранятся на готовых решениях (NAS).

  • Я не делаю много настроек, которые сложно сделать заново.

Теперь расскажу о самом интересном: архитектуре и реализации.

Выбор накопителей

Для меня главный критерий диска — это бесшумность, потому что облако располагается дома на полке шкафа. Самый бесшумный, но дорогой вариант — SSD. Я пробовал использовать такой диск в NAS-сервере, но не нашел существенных преимуществ в скорости работы для тех данных, которые я храню и в том темпе, в котором пользуюсь своим облаком. Мне был важнее объем, поэтому я остановил выбор на обычных механических HDD, но со скоростью вращения 5 400 RPM, что минимизирует шум.

При выборе таких дисков можно обращать внимание на тип записи — SMR (Shingled Magnetic Recording, черепичная запись) и CMR (Conventional Magnetic Recording, классическая запись). Разница между ними в том, что в CMR данные записываются на магнитные дорожки, которые не пересекаются друг с другом. Это обеспечивает высокую скорость и стабильность чтения и записи, но плотность записи меньше, а сами диски дороже. В SMR дорожки данных перекрываются, как черепица на крыше — это позволяет увеличить плотность записи данных, поскольку дорожки располагаются ближе друг к другу, но плохо сказывается на скорости чтения-записи. SMR диски обычно в два раза дешевле, чем CMR и идеально подходят для накапливаемых данных, когда не нужно удалять старые. Из-за особенности записи они хорошо переносят запись до заполнения объема, но затем сильно теряют в производительности при перезаписи (для восстановления характеристик их необходимо занулить — очистить). Я использую CMR-диски для основного хранилища, где важна производительность, а на SMR-диски отгружаю резервные копии и существенно экономлю.

Некоторые производители не указывают тип записи. Рекомендую использовать CERT Tool Lite — эта утилита поможет и тип определить, и выявить потенциальные проблемы.

Второй момент, на который важно обращать внимание — серия дисков. Есть специальные серии для RAID-массивов, NAS, видеонаблюдения, а также серии для массового применения. Я попробовал разные, но решил поступить рационально, и еще на этапе продумывания надежности хранения отказался от использования RAID-массива в пользу резервного копирования в трех копиях по системе 3-2-1.

Почему я отказался от RAID

Казалось бы, RAID — это довольно популярное и очевидное решение для повышения надежности хранения данных. Почему я его не использую?

  • Нагрузка на диски. RAID предусматривает постоянную синхронизацию данных, нагрузка на диски возрастает, из-за чего они быстрее изнашиваются и могут не пережить те условные 10 лет, на которые я рассчитываю.

  • Физические угрозы. Поскольку RAID физически находится в одном месте, то в случае пожара, затопления, элементарного отключения света дома, вы остаетесь без доступа к своим цифровым сокровищам. В случае с 3-2-1, одна копия всегда вне дома, так что доступ к ней сохраняется.

  • Недостаточная гибкость. RAID ограничивает пользователя использованием дисков в рамках одного массива и RAID-контроллера, а значит диск не получится куда-то переставить и свободно переместить данные между флешками, HDD и SSD.

  • Избыточность. Стратегия 3-2-1 позволяет использовать меньше дисков и при этом обеспечивает достаточную для меня надежность, RAID в моем случае получился бы дороже, а танцев с настройкой потребовалось бы в разы больше.

Выбор железа

Под железом я подразумеваю корпус и начинку, куда будет подключен внутренний накопитель. Есть готовые решения от производителей, наиболее популярные — QNAP, Synology, Asustor, Terramaster. В них можно установить 2,5/3,5 HDD/SSD и также есть некоторые модели с поддержкой NVME SSD. Главное достоинство готовых решений — продуманный веб-интерфейс для работы с файлами и поддержка протоколов совместного доступа к файлам — я использую CIFS. Другие решения являются кастомными и меньше подходят для надежного хранения данных, но их возможно использовать для запуска приложений. Из кастомных решений популярны мини-ПК, например, мне нравится Beelink и MeLE. К сожалению, у меня был неудачный опыт использования мини-ПК, в котором сетевая карта была подключена внутри по шине USB вместо PCI и при нагрузке пропадала сеть. А самый кастомный и бюджетный способ — самому выбрать корпус, питание и системную плату.

Легким движением руки два кейса ЗУБР ОКА-11 (по центру) превращаются в VyOS-роутер и Kubernetes-хост с приложениями в контейнерах в дополнение к трем NAS-серверам Synology (слева и справа)

Легким движением руки два кейса ЗУБР ОКА-11 (по центру) превращаются в VyOS-роутер и Kubernetes-хост с приложениями в контейнерах в дополнение к трем NAS-серверам Synology (слева и справа)

Я пришел к гибриду — сочетаю готовые решения для хранения и кастомное решение для запуска приложений и программного роутера. Вот какие компоненты я в итоге подружил между собой:

  • Для резервных копий выбрал не самую новую модель Synology DS218j — это мой первый NAS, остальные приобрел недавно.

  • Для основного хранения данных с поддержкой контейнеров подошла модель DS216+ с 8 ГБ ОЗУ, а для хранения софта и служб DNS и LDAP, которые помогают приложениям взаимодействовать друг с другом — модель DS220j.

  • Две сборки с материнскими платами Mini-ITX и питанием RGEEK Pico PSU + Gembird 12 V 3А в корпусе ЗУБР ОКА-11 — для программного роутера на операционке VyOS и для запуска приложений в Kubernetes на операционке CoreOS.

  • Среди прочего железа во главе стоит коммутатор TP-Link на 24 порта с веб-управлением и второй Wi-Fi-роутер TP-Link — через него я подключаюсь к интернету и приложениям моего облака без настройки провайдерского роутера.

Также для защиты данных я подключил источник бесперебойного питания с USB в один из Synology. Когда питание отключается, все хранилища обнаруживают сбой и корректно останавливают операции чтения-записи в течение нескольких минут. И, да, все получилось поместить на полке шкафа размером 100 в ширину, 60 в глубину и 40 в высоту.

Архитектура облака

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

Архитектура моего облака

Архитектура моего облака

У моей сети есть несколько принципов работы. Первый из них — возможность подключаться из дома или извне к приложениям облака по зашифрованному протоколу HTTPS, по одному и тому же доменному имени, только с разрешенных IP-адресов и без использования VPN.

Почему я не использую VPN-протоколы

Я не использую VPN-протоколы для подключения к своему облаку— считаю, что это лишняя сложность в настройке и подключении. Также не использую протоколы авторизации OAuth 2.0, OpenID Connect и SSO — пока для меня это тоже из разряда лишних телодвижений, но планирую использовать их в будущем.

Я арендовал статический IP-адрес у провайдера и зарегистрировал для себя доменное имя my.ru.net (настоящее имя другое) на недорогом DNS-хостинге. В него я вношу A-записи, благодаря которым приложения будут доступны по их DNS-именам immich.my.ru.net и nextcloud.my.ru.net как через смартфон, так и через веб-браузер. Я планировал работать с облаком не только через интернет, но и локально дома и поэтому создал такие же A-записи на Synology DS120j. Это примерно, как работа удаленно и из офиса — на экране вы открываете одни и те же ссылки, но в действительности они идут по разным путям. Таким образом вне сети и внутри сети имена не меняются — просто отвечают разные DNS-сервера. Благодаря настройкам DHCP и VLAN на VyOS-роутере, все устройства моей сети разрешают имена через DNS-сервер на Synology.

Для шифрования подключений и ограничения внешнего доступа к приложениям многие используют сервис Cloudflare, но часть функций в нем платные, а настройки шифрования показались мне неочевидными. Поэтому вместо него я развернул виртуалку с помощью free tier и использовал группы безопасности. Эта виртуалка — своего рода КПП (контрольно-пропускной пункт), который невозможно обойти на пути к точке назначения (приложениям). А еще она подсказывает до них дорогу — работает как обратный прокси и перенаправляет все запросы к моему статическому IP, который знает только она.

И на виртуалке и дома для шифрования подключений я использую одинаковые TLS-сертификаты, которые выпускаю для каждого устройства и приложения и храню просто в закрытом репозитории на гитхабе — так их легко скопировать. Обратных прокси, кстати, тоже два — второй находится дома. Поэтому когда я подключаюсь к приложениями из дома, DNS-записи на Synology отправляют меня на локальный прокси. А когда подключаюсь извне, DNS-записи хостинга отправляют меня к прокси на виртуалке. Получается, в приложениях на смартфоне ничего не надо перенастраивать, где бы я ни находился — очень удобно!
Я самостоятельно составил списки доступа разрешенных IP-адресов и внес несколько известных мне диапазонов в группы безопасности виртуалки. На домашнем Wi-Fi-роутере №1 к провайдерскому порту подключено оптоволокно и назначен статический IP-адрес. Он — следующий страж на пути из внешнего интернета в наше уютное домашнее облачко и разрешает подключения по порту 443 только от IP облачной виртуалки, развернутой в Cloud.ru.

Второй принцип моей сети — использование VLAN и DHCP. Программный VyOS-роутер — сердце сети и хранитель настроек. Он обеспечивает гибкость взаимодействия устройств и приложений — каждому выделяется свой номер VLAN. Вручную IP-адреса я не задаю, диапазоны выдаются в каждом VLAN по DHCP автоматически. Для доступа к этой сети я установил Wi-Fi-роутер №2 и соединил через коммутатор с VyOS–роутером. Последний составляет списки «гостей на вход» и передает их на коммутатор, который встречает тех кто в списке и говорит: «Вам за вон тот столик, идите по дорожке VLAN такой-то и никуда не сворачивайте».

Третий принцип сети — не перенастраивать роутер Wi-Fi №1, оставим его на всякий случай для обычного доступа в интернет. Роутер Wi-Fi №2 заведует внутренней кухней и используется для доступа к сервисам: «этот гость заказал свежих фоток с Эльбруса, Immich, неси их сюда»! За официантов у нас приложения: Nextcloud и Immich, чтобы удостовериться, что чаевые вы оставите именно тому, кто вас обслуживал, можно проверить соответствие имени в чеке бейджику официанта (TLS-сертификат). Ну а на кухне сидят повара, семейство NAS-серверов Synology: младшенький заведует данными, средний — для софта, сервисных задач и LDAP, а старший самый взрослый и опытный просто делает бэкапы и следит за электричеством. На случай, если потекла крыша и гостей вместе с поварами залило, есть внештатный повар, которого можно позвать на помощь (внешние HDD отдал на хранение родственникам). Кстати, данные подключены к приложениям через общие папки — всегда можно легко снять копию сетевой папки с любого компьютера на внешний накопитель и сделать ту самую третью копию по правилу бекапа 3-2-1.

Четвертая особенность сети — удаленный рабочий стол, мини-ПК MeLE Quieter 2Q, который работает 24/7. Это такой круглосуточный управляющий и специалист по связям между персоналом и гостями: через него можно вмешаться во внутреннюю кухню, в случае если ваш суп пересолен. Подключаться к рабочему столу я могу двумя способами: с любого устройства, используя веб-интерфейс и приложение Apache Guacamole; через RDP (Remote Desktop Protocol), который работает через 3389 порт.

Настройка

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

  1. Разделение сети на VLAN: размещать приложения, данные, Wi-Fi гостей в одной LAN сети непрактично — могут возникать конфликты и сложно уследить за порядком. А еще обычно сеть небольшая: с маской 24 ее хватит на 254 устройства, но не получится выдать красивую нумерацию. А может быть вы вообще не ограничены домашней сетью и расширяете ее где-то за городом, в облаке или в пределах семейных локаций. Поэтому, в общем случае стоит выбрать понравившуюся сеть и разделить ее.

Разделение сети на VLAN

Разделение сети на VLAN
Как автоматически разделить сеть на VLAN

Я выбрал сеть 192.168.0.0/16 и разделил ее на 16 сетей (так можно выдать непересекающиеся сети в 16 разных локаций — достаточно много для домашнего использования). Получились 16 сетей формата 192.168.X.0/20. Их делим еще на 16 сетей 192.168.X.0/24 — они пойдут под LAN-сеть, сети устройств, приложений и так далее. Каждую из них я разделил еще 8 сетей 192.168.X.0/27 — именно их и можно выдавать под каждый контейнер, устройство — это наиболее мелкое деление. Итого получил 16x16x8=2048 сетей и номер VLAN для каждой из максимально возможного числа 4094. Я сделал скрипт в формате Jupyter-блокнота, которым вы сможете так же разделить любую понравившуюся сеть.

  1. Прокси Caddy: используется для скрытия нескольких сервисов за одним публичным IP и для терминирования TLS-сертификатов. Я использую один в облаке и один дома, это позволяет использовать одни и те же настройки и URL-адреса HTTPS вне зависимости от локации.

Подробнее про прокси Caddy

При внешнем доступе через виртуалку или статический IP возникает проблема: публичный IP-адрес один, а приложений несколько — Nextcloud, Immich — нужно решить, какие IP-адреса будут в A-записях. Можно для приложений использовать разные TCP-порты, а можно использовать TLS-SNI для идентификации имени сервера в URL-запросе. Я использую второй вариант и реализую его с помощью прокси Caddy. Это современный быстрый обратный прокси с более простым запуском, чем Nginx, Apache или HAProxy. A-записи в хостинге настроил на публичный IP-адрес виртуалки, а A-записи дома — на IP-адрес контейнера с прокси Caddy. Кроме этого, для внешней защиты с неизвестных диапазонов IP я настроил группы безопасности прямо через интерфейс платформы.

  1. Конфигурации Kubernetes-хоста и приложений. Я использовал контейнеры на Synology и Docker-контейнеры, но функционала в конечном счете не хватило — хотел выдавать отдельный VLAN под каждый контейнер, чтобы они получали адрес по DHCP, а также хранить постоянные данные на CIFS папках. Этот функционал поддерживается CSI- и CNI-драйверами Kubernetes, поэтому я решил его попробовать в этой части и остался очень доволен! Теперь я точно знаю, что данные надежно хранятся на готовых хранилищах Synology, а настройки контейнеров мне полностью понятны — дальше приведу пример.

  2. Конфигурация VyOS-роутера. Этот роутер заведует всей информацией о сети. На нем настроены все VLAN-сети, и в каждой заданы DHCP-опции сервера для того, чтобы каждое устройство, контейнер, приложение получало индивидуальные сетевые настройки.

  3. Перенаправление портов (DNAT) и ACL: DNAT я использую на провайдерском роутере для перенаправления трафика от виртуалки в облаке к порту VyOS-роутера. На последнем я использую еще одно DNAT-правило для перенаправления трафика к прокси Caddy. Разрешенные IP адреса я ограничиваю доступ с помощью списков доступа ACL на виртуалке в облаке и на провайдерском роутере.

Списки доступа я составил простым способом — провел эксперимент, из какой подсети мне выдается IP-адрес, когда я нахожусь в разных локациях, а также в разных мобильных сетях. Использовал 2ip.ru для определения адреса и ripe.net для определения подсети, в которую он входит. Оказалось, что даже при смене локации в пределах города и даже за его пределами, мне выдается адрес буквально из двух разных подсетей. Что интересно, со второго смартфона в той же сети мне выдавались другие подсети. Рискну предположить, что у провайдеров есть привязка и предпочтение выдачи определенных блоков IP-адресов определенным устройствам (напишите в комментах, по какому принципу это работает, если знаете). Я извлек пользу из этой особенности и составил список из восьми разных подсетей, чтобы уверенно получать доступ к своему облаку, где бы я не находился. Эти списки я внес в группы безопасности. Такой подход также позволяет внести дополнительные подсети в случае, если в новой локации доступа все-таки не будет.

Для шифрования подключения я выпустил TLS-сертификаты через Сertbot и залил их в закрытый git-репозиторий. Благодаря этому можно использовать одни и те же сертификаты в контейнере с Caddy дома и на виртуалке в облаке. Выпускаю способом HTTP-01 Challenge, т. е. заранее открываю порт 80 на виртуалке, чтобы Let’s Encrypt смог выполнить обратный запрос в Сertbot и выдать сертификаты. После выпуска сертификатов порт 80 закрываю. Можно использовать способ DNS-01 Challenge и не открывать порт 80, но я пока не проверил его в своей схеме.

git clone ssh://github.com/myname/letsencrypt /etc/letsencrypt certbot certonly --standalone -d immich.my.ru.net certbot certonly --standalone -d nextcloud.my.ru.net 

Certbot создает символьные ссылки и структуру папок в /etc/letsencrypt, которую использует для обновления сертификатов в будущем. Чтобы можно было в дальнейшем скачать сертификаты и использовать их по тому же пути, я заменяю символьные ссылки на реальные файлы.

cd /etc/letsencrypt/live/immich.my.ru.net cp --remove-destination $(readlink cert.pem) cert.pem cp --remove-destination $(readlink chain.pem) chain.pem cp --remove-destination $(readlink fullchain.pem) fullchain.pem cp --remove-destination $(readlink privkey.pem) privkey.pem cd /etc/letsencrypt/live/nextcloud.my.ru.net cp --remove-destination $(readlink cert.pem) cert.pem cp --remove-destination $(readlink chain.pem) chain.pem cp --remove-destination $(readlink fullchain.pem) fullchain.pem cp --remove-destination $(readlink privkey.pem) privkey.pem  

Теперь можно перейти в /etc/letsencrypt и отправить сертификаты в репозиторий GitHub. При его создании важно использовать именно частный тип репозитория (private).

cd /etc/letsencrypt git add * git commit -am "add certs" git push 

Теперь подготавливаю конфигурацию прокси в облаке. Caddyfile на виртуалке в облаке для подключения TLS-сертификатов краток. Запросы к nextcloud.my.ru.net и immich.my.ru.net он отправляет на статический IP-адрес дома благодаря записи в /etc/hosts

Конфигурация /etc/caddy/Caddyfile

{     http_port 8080     https_port 443 }  https://nextcloud.my.ru.net:443 {     tls /etc/letsencrypt/live/nextcloud.my.ru.net/fullchain.pem /etc/letsencrypt/live/my.mydomain.ru.net/privkey.pem     reverse_proxy https://nextcloud.my.ru.net:443 {         header_up Host {host}     } }  https://immich.my.ru.net:443 {     tls /etc/letsencrypt/live/immich.my.ru.net/fullchain.pem /etc/letsencrypt/live/my.mydomain.ru.net/privkey.pem     reverse_proxy https://nextcloud.mydomain.ru.net:443 {         header_up Host {host}     } }  

В записи /etc/hosts я прописал свой статический IP (пример):

123.45.123.45 nextcloud.my.ru.net immich.my.ru.net

Запускаю Caddy на виртуалке в облаке с помощью контейнера и Podman:

sudo apt install -y podman sudo podman run -d -v /etc/caddy/Caddyfile:/etc/caddy/Caddyfile -v /etc/letsencrypt:/etc/letsencrypt -p 443:443 --name caddy docker.io/caddy:2.9.1-alpine caddy run --config /etc/caddy/Caddyfile  

Для домашнего прокси Caddy я подготовил похожую конфигурацию, используя те же TLS-сертификаты, предварительно загрузив их, но указал IP-адреса приложений, которые развернуты в контейнерах. Напомню, что каждое приложение в контейнере живет в отдельном VLAN и получает свой адрес по DHCP:

{     http_port 8080     https_port 443 }  https://nextcloud.my.ru.net:443 {     tls /etc/letsencrypt/live/nextcloud.my.ru.net/fullchain.pem /etc/letsencrypt/live/nextcloud.my.ru.net/privkey.pem     reverse_proxy http://192.168.33.66:80 }  https://immich.my.ru.net:443 {     tls /etc/letsencrypt/live/immich.my.ru.net/fullchain.pem /etc/letsencrypt/live/immich.my.ru.net/privkey.pem     reverse_proxy http://192.168.33.97:2283 }  

С внешним периметром закончили. Погрузимся во внутреннюю кухню! Настройки роутера VyOS — самые объемные. Я пользуюсь секциями конфигурации для того, чтобы задать подсети каждому VLAN, задать SNAT правило для доступа в интернет через LAN-сеть WiFi-роутера #1 (это нужно, поскольку я зарекся его не настраивать и на нем нет обратных маршрутов к другим устройствам), задать DNAT правило для перенаправления внешних подключений к прокси Caddy и задать опции DHCP-сервера, чтобы выдавать индивидуальные настройки каждому контейнеру и устройству по их MAC адресу:

Конфигурация VyOS-роутера
root@r2:~# show configuration  interfaces {     ethernet eth0 {         address dhcp         vif 1152 {             address dhcp             description w1             dhcp-options {                 default-route-distance 1             }         }         vif 1160 {             address 192.168.33.1/27             description core                 }         vif 1161 {             address 192.168.33.33/27             description data         }         vif 1162 {             address 192.168.33.65/27             description backup         }         vif 1163 {             address 192.168.33.97/27             description vdi         }         vif 1164 {             address 192.168.33.129/27             description wifi2wan         }         vif 1165 {             address 192.168.33.161/27             description switch         }         vif 1167 {             address 192.168.33.225/27             description kubernetes         }         vif 1176 {             address 192.168.35.0/27             description postgresql         }         vif 1177 {             address 192.168.35.33/27             description pgvecto         }         vif 1178 {             address 192.168.35.65/27             description nextcloud         }         vif 1179 {             address 192.168.35.96/27             description immich         }         vif 1180 {             address 192.168.35.128/27             description redis         }         vif 1181 {             address 192.168.35.160/27             description caddy         }     } } nat {      destination {          rule 1 {              destination {                  address 192.168.32.4/32                  port 443              }              inbound-interface {                  name eth0.1152              }              protocol tcp              translation {                  address 192.168.35.162/32                  port 443              }          }      }     source {         rule 1 {             outbound-interface {                 name eth0.1152             }             source {                 address 0.0.0.0/0             }             translation {                 address masquerade             }         }     } } service {     dhcp-server {         shared-network-name core {             authoritative             subnet 192.168.33.0/27 {                 ignore-client-id                 description core                 option {                     default-router 192.168.33.1                     name-server 192.168.33.2                 }                 static-mapping core {                     ip-address 192.168.33.2                     mac AA:BB:CC:DD:EE:01                 }                 subnet-id 1160             }         }         shared-network-name data {             authoritative             subnet 192.168.33.32/27 {                 ignore-client-id                 description data                 option {                     default-router 192.168.33.33                     name-server 192.168.33.2                 }                 static-mapping data {                     ip-address 192.168.33.34                     mac AA:BB:CC:DD:EE:02                 }                 subnet-id 1161             }         }         shared-network-name backup {             authoritative             subnet 192.168.33.64/27 {                 ignore-client-id                 description backup                 option {                     default-router 192.168.33.65                     name-server 192.168.33.2                 }                 static-mapping backup {                     ip-address 192.168.33.66                     mac AA:BB:CC:DD:EE:03                 }                 subnet-id 1162             }         }         shared-network-name vdi {             authoritative             subnet 192.168.33.96/27 {                 ignore-client-id                 description vdi                 option {                     default-router 192.168.33.97                     name-server 192.168.33.2                 }                 static-mapping vdi {                     ip-address 192.168.33.98                     mac AA:BB:CC:DD:EE:04                 }                 subnet-id 1163             }         }         shared-network-name switch {             authoritative             subnet 192.168.33.160/27 {                 ignore-client-id                 description vdi                 option {                     default-router 192.168.33.161                     name-server 192.168.33.2                 }                 static-mapping vdi {                     ip-address 192.168.33.162                     mac AA:BB:CC:DD:EE:05                 }                 subnet-id 1165             }         }         shared-network-name kubernetes {             authoritative             subnet 192.168.33.224/27 {                 ignore-client-id                 description kubernetes                 option {                     default-router 192.168.33.225                     name-server 192.168.33.2                 }                 static-mapping vdi {                     ip-address 192.168.33.227                     mac AA:BB:CC:DD:EE:06                 }                 subnet-id 1167             }         }         shared-network-name postgresql {             authoritative             subnet 192.168.35.0/27 {                 ignore-client-id                 description postgresql                 option {                     default-router 192.168.35.1                     name-server 192.168.33.2                 }                 static-mapping postgresql {                     ip-address 192.168.35.2                     mac AA:BB:CC:DD:EE:07                 }                 subnet-id 1176             }         }         shared-network-name pgvecto {             authoritative             subnet 192.168.35.32/27 {                 ignore-client-id                 description pgvecto                 option {                     default-router 192.168.35.33                     name-server 192.168.33.2                 }                 static-mapping pgvecto {                     ip-address 192.168.35.34                     mac AA:BB:CC:DD:EE:08                 }                 subnet-id 1177             }         }         shared-network-name nextcloud {             authoritative             subnet 192.168.35.64/27 {                 ignore-client-id                 description nextcloud                 option {                     default-router 192.168.35.65                     name-server 192.168.33.2                 }                 static-mapping nextcloud {                     ip-address 192.168.35.66                     mac AA:BB:CC:DD:EE:09                 }                 subnet-id 1178             }         }         shared-network-name immich {             authoritative             description immich             option {                 default-router 192.168.33.97                 name-server 192.168.33.2             }             subnet 192.168.33.96/27 {                 ignore-client-id                 static-mapping immich {                     ip-address 192.168.33.98                     mac AA:BB:CC:DD:EE:0a                 }                 subnet-id 1179             }         }         shared-network-name redis {             authoritative             subnet 192.168.35.128/27 {                 description redis                 option {                     default-router 192.168.35.129                     name-server 192.168.33.2                 }                 static-mapping redis {                     ip-address 192.168.35.130                     mac AA:BB:CC:DD:EE:0b                 }                 subnet-id 1180             }         }         shared-network-name caddy {             authoritative             subnet 192.168.35.160/27 {                 description caddy                 option {                     default-router 192.168.35.161                     name-server 192.168.33.2                 }                 static-mapping caddy {                     ip-address 192.168.35.162                     mac AA:BB:CC:DD:EE:0c                 }                 subnet-id 1181             }         }     } }  

Развертывание приложений

На данный момент я не использую некоторые из штатных приложений, которые мог бы использовать на NAS (Synology Photos) и заменил их на продукты с открытым исходным кодом. Для хранения файлов использую Nextcloud, а для фотографий Immich. Как и говорил, использую гибридный подход. Процессы приложений запущены на отдельных устройствах, а данные они хранят на Synology, подключая папки по протоколу CIFS. Благодаря этому можно не беспокоиться о сохранности данных — резервные копии папок создаются каждый день средствами NAS, а также раз в год я их копирую на внешний накопитель, который храню отдельно.

CIFS-папки также позволяют легко расширять место. Когда место будет заканчиваться, можно установить еще один сервер NAS и разделить данные между CIFS-папками — сделать горизонтальное масштабирование. Кроме того, в моей конфигурации каждое приложение получает свой номер VLAN и соответствующую сеть по DHCP. Я разворачиваю приложения в Kubernetes — именно с помощью него мне удалось подключить к контейнерам CIFS, DHCP и VLAN. Я предлагаю пример Kubernetes-манифеста для развертывания Nextcloud и других приложений в отдельном VLAN и с хранением данных в папках CIFS. Всего я использую 6 манифестов: 1 для прокси Caddy, 2 для приложений Immich и Nextcloud и 3 для баз данных PostgreSQL, PGVecto и Redis, которые нужны приложениям.

Я установил Fedora CoreOS на хост для Kubernetes. Было интересно попробовать современную ОС, ориентированную на запуск контейнеров. Развернуть Kubernetes совсем не сложно — если у вас нет хоста на Linux, можно это попробовать в Evolution Managed Kubernetes. А я развернул кластер локально:

git clone --branch release-2.27 https://github.com/kubernetes-sigs/kubespray.git cd kubespray python3 -m venv .venv source .venv/bin/activate pip3 install -r requirements.txt ansible-playbook -i inventory/local/hosts.ini -b cluster.yml  sudo kubectl cluster-info  

В дополнение вот такими командами установил CSI-драйвер SMB для подключения папок CIFS, CNI-драйвер Multus для поддержки VLAN и DHCP, а также Intel QSV драйверы для поддержки аппаратного ускорения в контейнерах — все это я использую.

curl -skSL https://raw.githubusercontent.com/kubernetes-csi/csi-driver-smb/v1.17.0/deploy/install-driver.sh | bash -s v1.17.0 –  kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd?ref=v0.32.0' kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/nfd/overlays/node-feature-rules?ref=v0.32.0' kubectl apply -k 'https://github.com/intel/intel-device-plugins-for-kubernetes/deployments/gpu_plugin/overlays/nfd_labeled_nodes?ref=v0.32.0'  kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/v4.1.4/deployments/multus-daemonset.yml  

По опыту CNI-DHCP драйвер не запускается автоматически, поэтому я создал вручную соответствующий сервис и сокет — это первое действие после установки Kubernetes.

Команда: sudo systemctl edit —full —force cni-dhcp.service

[Unit] Description=CNI DHCP service Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp After=network.target cni-dhcp.socket Requires=cni-dhcp.socket  [Service] ExecStart=/opt/cni/bin/dhcp daemon  [Install] WantedBy=multi-user.target  

Команда: sudo systemctl edit —full —force cni-dhcp.socket

[Unit] Description=CNI DHCP service socket Documentation=https://github.com/containernetworking/plugins/tree/master/plugins/ipam/dhcp PartOf=cni-dhcp.service  [Socket] ListenStream=/run/cni/dhcp.sock SocketMode=0660 SocketUser=root SocketGroup=root RemoveOnStop=true  [Install] WantedBy=sockets.target  

Затем создал VLAN-интерфейсы — они нужны контейнерам с приложениями. Также с помощью настройки stable фиксирую MAC-адреса, по которым DCHP-сервер будет выдавать соответствующие настройки и отключаю протоколы IPv4 и IPv6 — их автоматически настроит CNI-DHCP драйвер внутри контейнера при его запуске.

sudo nmcli conn add con-name e.1176 type vlan dev enp2s0 id 1176 sudo nmcli conn add con-name e.1177 type vlan dev enp2s0 id 1177 sudo nmcli conn add con-name e.1178 type vlan dev enp2s0 id 1178 sudo nmcli conn add con-name e.1179 type vlan dev enp2s0 id 1179 sudo nmcli conn add con-name e.1180 type vlan dev enp2s0 id 1180 sudo nmcli conn add con-name e.1181 type vlan dev enp2s0 id 1181 sudo nmcli conn mod e.1176 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1177 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1178 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1179 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1180 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1181 802-3-ethernet.cloned-mac-address stable sudo nmcli conn mod e.1176 ipv4.method disabled sudo nmcli conn mod e.1176 ipv6.method disabled sudo nmcli conn mod e.1177 ipv4.method disabled sudo nmcli conn mod e.1177 ipv6.method disabled sudo nmcli conn mod e.1178 ipv4.method disabled sudo nmcli conn mod e.1178 ipv6.method disabled sudo nmcli conn mod e.1179 ipv4.method disabled sudo nmcli conn mod e.1179 ipv6.method disabled sudo nmcli conn mod e.1180 ipv4.method disabled sudo nmcli conn mod e.1180 ipv6.method disabled sudo nmcli conn mod e.1181 ipv4.method disabled sudo nmcli conn mod e.1181 ipv6.method disabled  

Когда закончил с настройкой хоста Kubernetes, на NAS-сервере Synology я создал общую папку docker, в которой создал папки nextcloud, immich, postgres, pgvecto, redis и caddy для каждого контейнера. В этих папках приложения будут хранить данные и их я буду указывать дальше в Kubernetes-манифестах.

Настройка PostgreSQL (для NextCloud)

manifests/postgresql.yml

--- kind: Pod apiVersion: v1 metadata:   name: postgresql   namespace: postgresql   annotations:     k8s.v1.cni.cncf.io/networks: postgresql spec:   restartPolicy: Always   containers:     - image: docker.io/postgres:16.6       env:         - name: POSTGRES_USER           value: postgres         - name: POSTGRES_PASSWORD           value: mypass       name: postgresql       volumeMounts:         - name: postgresql           mountPath: "/var/lib/postgresql/data"   volumes:     - name: postgresql       persistentVolumeClaim:         claimName: postgresql --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: postgresql   namespace: postgresql spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1176",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "postgresql"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }' --- apiVersion: v1 kind: PersistentVolume metadata:   name: postgresql   namespace: postgresql spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0700     - file_mode=0700     - uid=999     - gid=999     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: pg1     volumeAttributes:       source: //data.my.ru.net/docker/postgresql     nodeStageSecretRef:       name: postgresql-cifs       namespace: postgresql --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: postgresql   namespace: postgresql spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: postgresql --- apiVersion: v1 kind: Secret metadata:   name: postgresql-cifs   namespace: postgresql stringData:   username: myuser   password: mypass  
Настройка PGVecto (для Immich)

manifests/pgvecto.yml

--- kind: Pod apiVersion: v1 metadata:   name: pgvecto   namespace: pgvecto   annotations:     k8s.v1.cni.cncf.io/networks: 'pgvecto' spec:   restartPolicy: Always   securityContext:     runAsUser: 999     runAsGroup: 999   containers:     - image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0       env:         - name: POSTGRES_USER           value: postgres         - name: POSTGRES_PASSWORD           value: mypass       name: pgvecto       command:         - "postgres"         - "-c"         - "shared_preload_libraries=vectors.so"         - "-c"         - "search_path=\"$$user\", public, vectors"         - "-c"         - "logging_collector=on"         - "-c"         - "max_wal_size=2GB"         - "-c"         - "shared_buffers=512MB"         - "-c"         - "wal_compression=on"       volumeMounts:         - name: pgvecto           mountPath: "/var/lib/postgresql/data"   volumes:     - name: pgvecto       persistentVolumeClaim:         claimName: pgvecto --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: pgvecto   namespace: pgvecto spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1177",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "pgvecto"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }' --- apiVersion: v1 kind: PersistentVolume metadata:   name: pgvecto   namespace: pgvecto spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0700     - file_mode=0700     - uid=999     - gid=999     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: pgvecto     volumeAttributes:       source: //data.my.ru.net/docker/pgvecto     nodeStageSecretRef:       name: pgvecto-cifs       namespace: pgvecto --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: pgvecto   namespace: pgvecto spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: pgvecto --- apiVersion: v1 kind: Secret metadata:   name: pgvecto-cifs   namespace: pgvecto stringData:   username: myuser   password: mypass  
Настройка Redis (для Immich)

manifests/redis.yml

--- kind: Pod apiVersion: v1 metadata:   name: redis   namespace: redis   annotations:     k8s.v1.cni.cncf.io/networks: redis spec:   restartPolicy: Always   containers:     - image: docker.io/redis:7.4.2-alpine3.21       name: redis --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: redis   namespace: redis spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1180",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "redis"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }'  
Настройка Immich

Я подключил две директории: в папку immich будут загружаться резервные копии фотографий со смартфона и те, которые загружаются через веб-браузер. Для внешних библиотек я подключил папку photo, в которую могу отправлять файлы просто с USB-накопителей и они также будут видны в Immich.

--- kind: Pod apiVersion: v1 metadata:   name: immich   namespace: immich   annotations:     k8s.v1.cni.cncf.io/networks: immich spec:   containers:     - image: ghcr.io/immich-app/immich-server:v1.128.0       name: immich       env:         - name: REDIS_HOSTNAME           value: redis         - name: DB_HOSTNAME           value: postgresql         - name: DB_USERNAME           value: immich         - name: DB_PASSWORD           value: mypass       volumeMounts:         - name: immich           mountPath: '/usr/src/app/upload'         - name: photo           mountPath: '/photo'       resources:         requests:           gpu.intel.com/i915: "1"         limits:           gpu.intel.com/i915: "1"   dnsPolicy: None   dnsConfig:     nameservers:       - 192.168.33.2     searches:       - my.ru.net   volumes:     - name: immich       persistentVolumeClaim:         claimName: immich     - name: photo       persistentVolumeClaim:         claimName: photo --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: immich   namespace: immich spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1179",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "immich"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }' --- apiVersion: v1 kind: PersistentVolume metadata:   name: immich   namespace: immich spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0770     - file_mode=0770     - uid=1000     - gid=1000     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: immich     volumeAttributes:       source: //data.my.ru.net/docker/immich     nodeStageSecretRef:       name: immich-cifs       namespace: immich --- apiVersion: v1 kind: PersistentVolume metadata:   name: photo   namespace: immich spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0770     - file_mode=0770     - uid=1000     - gid=1000     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: photo     volumeAttributes:       source: //data.my.ru.net/photo     nodeStageSecretRef:       name: immich-cifs       namespace: immich --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: immich   namespace: immich spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: immich --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: photo   namespace: immich spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: photo --- apiVersion: v1 kind: Secret metadata:   name: immich-cifs   namespace: immich stringData:   username: myuser   password: mypass  
Настройка Nextcloud

При подключении папок я использовал опцию subPath, которая позволяет уточнить подключаемую директорию. Для пути //data.my.ru.net/docker/nextcloud указываю подпапки config, data, custom_apps, themes и файл nextcloud-init-sync.lock. Таким же образом можно разделять данные между несколькими серверами NAS — можно создать несколько PersistentVolume и с помощью name, subPath и mountPath сделать так, что подпапки в data будут подключены к разным NAS.

manifests/nextcloud.yml

--- kind: Pod apiVersion: v1 metadata:   name: nextcloud   namespace: nextcoud   annotations:     k8s.v1.cni.cncf.io/networks: nextcloud spec:   restartPolicy: Always   containers:     - image: docker.io/nextcloud:30.0.5       name: box       volumeMounts:         - name: nextcloud           subPath: config           mountPath: "/var/www/html/config"         - name: nextcloud           subPath: data           mountPath: "/var/www/html/data"         - name: nextcloud           subPath: custom_apps           mountPath: "/var/www/html/custom_apps"         - name: nextcloud           subPath: themes           mountPath: "/var/www/html/themes"         - name: nextcloud           subPath: nextcloud-init-sync.lock           mountPath: "/var/www/html/nextcloud-init-sync.lock"   dnsPolicy: None   dnsConfig:     nameservers:       - 192.168.33.2     searches:       - my.ru.net   volumes:     - name: nextcloud       persistentVolumeClaim:         claimName: nextcloud --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: nextcloud   namespace: nextcloud spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1178",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "nextcloud"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }' --- apiVersion: v1 kind: PersistentVolume metadata:   name: nextcloud   namespace: nextcloud spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0770     - file_mode=0770     - uid=33     - gid=33     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: nextcloud     volumeAttributes:       source: //data.my.ru.net/docker/nextcloud     nodeStageSecretRef:       name: nextcloud-cifs       namespace: nextcloud --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: nextcloud   namespace: nextcloud spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: box --- apiVersion: v1 kind: Secret metadata:   name: nextcloud-cifs   namespace: nextcloud stringData:   username: myuser   password: mypass  
Настройка домашнего прокси Caddy

Конфигурацию Caddyfile я также разместил на NAS в папке //data.my.ru.net/docker/nextcloud/caddy/Caddyfile. При необходимости могу ее отредактировать прямо через веб-интерфейс моего NAS, что очень удобно.

manifests/caddy.yml

--- kind: Pod apiVersion: v1 metadata:   name: caddy   namespace: caddy   annotations:     k8s.v1.cni.cncf.io/networks: caddy spec:   restartPolicy: Always   containers:     - image: docker.io/caddy:2.9.1-alpine       name: caddyfile       volumeMounts:         - name: caddy           subPath: Caddyfile           mountPath: "/etc/caddy/Caddyfile"         - name: letsencrypt           mountPath: "/etc/letsencrypt"   volumes:     - name: caddy       persistentVolumeClaim:         claimName: caddy     - name: letsencrypt       persistentVolumeClaim:         claimName: letsencrypt --- apiVersion: "k8s.cni.cncf.io/v1" kind: NetworkAttachmentDefinition metadata:   name: caddy   namespace: caddy spec:   config: '{     "cniVersion": "0.3.1",     "type": "macvlan",     "mode": "passthru",     "master": "enp2s0.1181",     "ipam": {       "type": "dhcp",       "provide": [{         "option": "host-name",         "value": "caddy"       }]     },     "dns": {       "nameservers": ["192.168.33.2"]     }   }' --- apiVersion: v1 kind: PersistentVolume metadata:   name: caddy   namespace: caddy spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0700     - file_mode=0700     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: caddy     volumeAttributes:       source: //data.my.ru.net/docker/caddy     nodeStageSecretRef:       name: caddy-cifs       namespace: caddy --- apiVersion: v1 kind: PersistentVolume metadata:   name: letsencrypt   namespace: caddy spec:   capacity:     storage: 1Ti   accessModes:     - ReadWriteMany   persistentVolumeReclaimPolicy: Retain   mountOptions:     - dir_mode=0700     - file_mode=0700     - noperm     - mfsymlinks     - cache=none     - noserverino     - nobrl   csi:     driver: smb.csi.k8s.io     volumeHandle: letsencrypt     volumeAttributes:       source: //data.my.ru.net/docker/letsencrypt     nodeStageSecretRef:       name: caddy-cifs       namespace: caddy --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: caddy   namespace: caddy spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: caddy --- kind: PersistentVolumeClaim apiVersion: v1 metadata:   name: letsencrypt   namespace: caddy spec:   accessModes:     - ReadWriteMany   resources:     requests:       storage: 1Ti   volumeName: letsencrypt --- apiVersion: v1 kind: Secret metadata:   name: caddy-cifs   namespace: caddy stringData:   username: myuser   password: mypass  

Теперь все контейнеры можно последовательно запустить командами:

sudo kubectl apply -f manifests/postgresql.yml sudo kubectl apply -f manifests/pgvecto.yml sudo kubectl apply -f manifests/redis.yml sudo kubectl apply -f manifests/nextcloud.yml sudo kubectl apply -f manifests/immich.yml sudo kubectl apply -f manifests/caddy.yml  

В итоге приложения доступны через веб-браузер и со смартфона. Осталось только их настроить и можно пользоваться!

Веб-версия формы авторизации Nextcloud

Веб-версия формы авторизации Nextcloud
Авторизация в Immich через смартфон

Авторизация в Immich через смартфон

Что в итоге

Я решил отказаться от хранения данных на флешках и в облаках, создав собственное домашнее облако. Используя прошлый опыт, я постарался упростить поддержку системы и сосредоточился на главном — прозрачности и надежности хранения данных. Для этого я выбрал специализированные устройства NAS.

Мне также нужна была гибкость, чтобы заменить стандартные приложения на более современные, такие как Immich и Nextcloud. С этой целью я разделил сеть на VLAN, настроил их на программном роутере и выделил DHCP-диапазоны. Сами приложения я развернул в Kubernetes, подключая их в сети VLAN и к общим CIFS-папкам. Это упростило взаимодействие устройств и приложений. Мне удалось использовать минимум настроек — большинство из них получилось уместить в статье и наверняка я сам буду в нее заглядывать, если что-то подзабуду.

По опыту использования это действительно облако, как Google Drive: я использую веб-доступ и приложения для смартфона из любой локации, но данные хранятся у меня дома.

Nextcloud показывает размер каждой папки как в мобильной, так и в веб-версии

Nextcloud показывает размер каждой папки как в мобильной, так и в веб-версии
В мобильной версии Immich доступны локальные фото со смартфона, фото на моем облаке и изображения, которые есть и там и там (значок облака отображает статус)

В мобильной версии Immich доступны локальные фото со смартфона, фото на моем облаке и изображения, которые есть и там и там (значок облака отображает статус)

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

Я позаботился о безопасности, проведя эксперимент с составлением списка IP-подсетей для доступа извне, а также защитил подключения TLS-сертификатами с помощью прокси Caddy.

На всё решение я потратил 130 000 рублей при общем объеме хранения 22 ТБ, используя как готовые, так и кастомные решения. Облако потребляет 454 рубля в месяц при стоимости 7,45 рубля за 1 кВт. Виртуалка в облаке и статический IP стоит 300 рублей в месяц. По моим расчетам, это выгоднее, чем хранение в облачных дисках при долгосрочном использовании в течение 10 лет.

В таблице подробно расписал, сколько «ест» каждый компонент домашнего облака:

Цена вопроса, оборудование и энергопотребление

Цена вопроса, оборудование и энергопотребление

Я доволен получившимся решением. Для меня оно — долгожданный и наконец случившийся ремонт в цифровом пространстве. Пока нет в планах что-то улучшать или менять, но есть желание наконец-то навести порядок в файлах и окончательно перенести всё в мое домашнее облако.

В комментариях делитесь — хотите тоже повторить такой трюк дома? Каким софтом и железом пользуетесь для сохранения своих данных? А еще — приходите на нашу конференцию GoCloud — там коллеги расскажут про не менее интересные и технологичные решения в облаках.


ссылка на оригинал статьи https://habr.com/ru/articles/895328/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *