Фото важных семейных событий и видео из путешествий, копии важных документов, музыка, фильмы, которых не найти на стримингах — многие задумывались, как сохранить все самое важное так, чтобы ситуация с не вовремя сломавшейся флешкой не обернулась потерей ценных данных навсегда. Кому-то для спокойствия достаточно 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 ТБ на 10 лет обойдется от 23 000 до 55 000 рублей, и это без учета того, что стоимость может расти, а доступ ограничиваться из-за всяких событий в мире.
Покупка 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 и при нагрузке пропадала сеть. А самый кастомный и бюджетный способ — самому выбрать корпус, питание и системную плату.

Я пришел к гибриду — сочетаю готовые решения для хранения и кастомное решение для запуска приложений и программного роутера. Вот какие компоненты я в итоге подружил между собой:
Для резервных копий выбрал не самую новую модель 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 порт.
Настройка
Для обеспечения безопасности и удобства доступа к домашнему облаку важно правильно настроить сеть. Если коротко резюмировать, в основе сети следующие решения:
Разделение сети на VLAN: размещать приложения, данные, Wi-Fi гостей в одной LAN сети непрактично — могут возникать конфликты и сложно уследить за порядком. А еще обычно сеть небольшая: с маской 24 ее хватит на 254 устройства, но не получится выдать красивую нумерацию. А может быть вы вообще не ограничены домашней сетью и расширяете ее где-то за городом, в облаке или в пределах семейных локаций. Поэтому, в общем случае стоит выбрать понравившуюся сеть и разделить ее.

Как автоматически разделить сеть на 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-блокнота, которым вы сможете так же разделить любую понравившуюся сеть.
Прокси 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 я настроил группы безопасности прямо через интерфейс платформы.
Конфигурации Kubernetes-хоста и приложений. Я использовал контейнеры на Synology и Docker-контейнеры, но функционала в конечном счете не хватило — хотел выдавать отдельный VLAN под каждый контейнер, чтобы они получали адрес по DHCP, а также хранить постоянные данные на CIFS папках. Этот функционал поддерживается CSI- и CNI-драйверами Kubernetes, поэтому я решил его попробовать в этой части и остался очень доволен! Теперь я точно знаю, что данные надежно хранятся на готовых хранилищах Synology, а настройки контейнеров мне полностью понятны — дальше приведу пример.
Конфигурация VyOS-роутера. Этот роутер заведует всей информацией о сети. На нем настроены все VLAN-сети, и в каждой заданы DHCP-опции сервера для того, чтобы каждое устройство, контейнер, приложение получало индивидуальные сетевые настройки.
Перенаправление портов (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
В итоге приложения доступны через веб-браузер и со смартфона. Осталось только их настроить и можно пользоваться!


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


В приложениях, которые я использую, есть такие функции, которые я не видел ни в каких других. Например, Nextcloud сразу отображает размер каждой папки, что сильно упрощает управление файлами. В Immich мне нравится функция архивирования фотографий — я могу добавить их в альбомы, но скрыть из общей ленты. Кроме того, фото можно удалять в режиме альбома и находить дубликаты при загрузке, что хорошо помогает наводить и поддерживать порядок.
Я позаботился о безопасности, проведя эксперимент с составлением списка IP-подсетей для доступа извне, а также защитил подключения TLS-сертификатами с помощью прокси Caddy.
На всё решение я потратил 130 000 рублей при общем объеме хранения 22 ТБ, используя как готовые, так и кастомные решения. Облако потребляет 454 рубля в месяц при стоимости 7,45 рубля за 1 кВт. Виртуалка в облаке и статический IP стоит 300 рублей в месяц. По моим расчетам, это выгоднее, чем хранение в облачных дисках при долгосрочном использовании в течение 10 лет.
В таблице подробно расписал, сколько «ест» каждый компонент домашнего облака:

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