Сказ о том, как я настраивал сервисы в docker на Raspberry PI и почему это, возможно, не лучшая идея.
Введение (или как всё начиналось)
Началось всё очень давно пару лет назад. Так уж вышло, что я оказался в Китае и надо было как-то связываться с внешним миром. Сторонним VPN и прокси я не очень доверял, поэтому решил поднять DigitalOcean со своим прокси. Так уж вышло, что со временем сервер с прокси оброс разными разностями: от файлового хранилища (Syncthing
) до CI (Jenkins
).
По возвращении в Россию было принято решение уходить с DO на какой-то self-hosting. Покупать для этого отдельный сервер не хотелось — дорого, да и пока незачем, по этой причине взял Raspberry PI 4B. Естественно, пришлось переносить все основные сервисы с DO на эту машинку, о чем и будет данный пост.
Вводные
Необходимо было настроить следующие сервисы:
- Syncthing — синхронизация файлов
- Jenkins — CI
- Telegraf — |
- Influxdb — графики CPU, GPU и прочего такого
- Grafana — |
- Gogs — git
- Radicale — синхронизация календарей/контактов
Кроме того:
- Хранение файлов и данных предполагалось производить на паре флешек
USB3
в зашифрованном виде (LUKS
) - Все веб-интерфейсы были спрятаны за Nginx реверс-прокси
Проблемы и нюансы
Сразу хотелось бы рассказать про проблемы, которые возникали по ходу сборки всего этого дела и/или возникают сейчас:
- Конкретно моя Raspberry PI не захотела корректно запускать хард с требуемым током в 1A, поэтому я решил использовать флешки. Возможно, проблема была в питании, а возможно — в конкретной модели малинки или харде
- На RaspberryPI 4B (на данный момент) нельзя загружаться с USB флешек. Как итог — очень медленная работа относительно чтения/записи файлов внутри системы. Относительно решалось использованием USB3 флешек для хранения файлов
- В силу не самой большой производительности, докер контейнеры не всегда корректно стартуют. Для исправления этого дела (я не нашёл даже информации об этом в гугле) пришлось написать полукостыльный скрипт (описано в разделе "Последние штрихи")
- Поскольку один из сервисов —
syncthing
, то иногда всё сильно начинает подвисать просто оттого, что малинка пишет/читает файлы - В простое без охлаждения малинка может нагреваться до 60 градусов
Так что если решитесь на такой эксперимент — будьте готовы к 502
при доступе через интернет и подвисаниям в локальном ssh
.
Готовка
Само собой, в первую очередь необходимо подготовить флешку Micro SD 16GB (или большего объема) с системой Raspbian. Подробнее об этом есть на официальном сайте.
- Качаем Noobs
- Форматируем Micro SD в Fat32
- Через любое приложение записи на носимые устройства (я использовал uNetbootin) записываем скачанный архив на флешку
- Запускаем Raspberry PI с установленной флешкой
Дальше всё достаточно интуитивно. Во время установки мне предлагалось два основных варианта установки: с GUI и без. Я ставил с GUI, но после настройки ssh
GUI мне больше не понадобился.
Подготовка Флешки(ек)
Флешки я готовил через Elementary OS с GUI (GParted и утилита disks) и небольшим количеством консоли. Вкратце, алгоритм следующий:
ВНИМАНИЕ, ПОДОБНЫЕ МАНИПУЛЯЦИИ УДАЛЯЮТ ВСЕ ФАЙЛЫ С УСТРОЙСТВ. ВЫБИРАЙТЕ ЦЕЛЕВЫЕ УСТРОЙСТВА АККУРАТНО
Вкратце — задача отформатировать флешки в формате ext4
и зашифровать с помощью LUKS.
- Через
GParted
- Выбираем нужное устройство
- Идем по пути
Device
->Create partition table...
- Выбираем
gpt
- Нажимаем
Apply
- Создаём раздел
ext4
- Через утилиту
gnome-disks
(устанавливается черезsudo apt install gnome-disk-utility
) (в меню она называетсяDisks
):
- Находим устройство
- Нажимаем клавишу настройки (шестерёнки)
- Нажимаем
Format partition...
- Выбираем пункт с припиской
Ext 4
- Ставим галочку
Password protect volume
- После нажатия
Next
вам будет предложено ввести пароль для устройства - После форматирования:
- Записываем куда-нибудь
UUID
, который отображается в информации об устройстве - Записываем куда-нибудь
Device
, который отображается в информации об устройстве
- Записываем куда-нибудь
Далее будет достаточно простая процедура добавления файлов для открытия флешек. Это нужно будет, чтобы RaspberryPI могла автоматически подключать устройства. Делаем следующее:
Примечание: Далее /dev/sdb1
— адрес раздела, который мы записывали из Device
dd if=/dev/urandom bs=4M count=1 of=/tmp/usb_decrypt_file
sudo cryptsetup luksAddKey /dev/sdb1 /tmp/usb_decrypt_file
Во время исполнения последней команды вам потребуется ввести пароль.
ВАЖНО: не потеряйте файл во время настройки и НИКОМУ не передавайте его.
Повторяем вышеописанные действия для второй флешки, если необходимо. Обязательно сохраняем файл(ы)-ключи и запоминаем/записываем пароли
Подготовка плацдарма Raspberry PI
После корректной установки системы, необходимо её донастроить:
- Обновляем систему (
sudo apt update && sudo apt -y dist-upgrade
) - Доустановка пакетов:
- Ставим
docker
иdocker-compose
(sudo apt -y install docker docker-compose
) - Ставим
Nginx
(sudo apt -y install nginx
). Будем его использовать для прокидывания reverse-proxy
- Ставим
Настраиваем монтирование флешек
Переносим файлы-ключики для флешек (например, в папку
/root/cryptfiles
; в дальнейшем она будет использована как пример)
В файл
/etc/crypttab
добавляем запись в формате:
usb1_crypt UUID=ДАННЫЕ_ИЗ_ПОЛЯ_UUID /root/cryptfiles/имя_файла-ключа luks
В файл
/etc/fstab
добавляем запись в формате
/dev/mapper/usb1_crypt /media/pi/usb1 ext4 defaults,nofail 0 2
Тут:
/dev/mapper/usb1_crypt
— это/dev/mapper/
+ имя (первое слово) из/etc/crypttab
/media/pi/usb1
— адрес монтирования. Необходимо создать вручную (mkdir /media/pi/usb1
). В теории — может быть любым, но лучше разполагать в/mnt
или/media/$USER
Повторить для второй флешки, если есть
Щепотка скриптов для Nginx
Далее будет представлен ворох скриптов на bash
, которые я использую для формирования конфигурации reverse-proxy до контейнеров.
Примечания:
- Считается, что вы УЖЕ купили и настроили домен, либо планируете обходиться без него в условиях локальной сети
- Считается, что вы используете letsencrypt сертификаты и их бота
Для примера, будем называть домен — mydomain.com
. Если путь до nginx находится по пути /etc/nginx
, то скрипты следует класть по пути /etc/nginx/autocompile
.
#!/bin/bash
# Use "-pl" key in subname to make it like https://hostname/subname.
# E.g.: for https://my.domain/example will be used "-pl example"
APPS=("syncthing" "grafana" "radicale" "git" "jenkins")
APPS_PROXIES=(http://localhost:8880 http://localhost:3000 http://localhost:8882 http://localhost:8883 http://localhost:8884)
HOSTNAMES=(my.domain)
# HOSTNAMES=()
conf_file="/etc/nginx/sites-available/autocompiled.conf"
ln_file="/etc/nginx/sites-enabled/autocompiled.conf"
echo "" > "$conf_file"
for app_index in ${!APPS[*]}
do
app="${APPS[app_index]}"
app_proxy="${APPS_PROXIES[app_index]}"
for host in ${HOSTNAMES[*]}
do
echo "`./compile_config.sh "$host" "$app_proxy" $app`" >> "$conf_file"
echo "" >> "$conf_file"
done
done
ln -s "$conf_file" "$ln_file"
#!/bin/bash
# FIRST ARG IS DOMAIN BASE
DOMAIN_BASE="$1"
shift
# THIRD ARG IS PROXY_PASS
PROXY_PATH=$(echo "$1" | sed -e "s/\//\\\\\//g")
PROXY_LOCATION=""
shift
HOSTNAME="$DOMAIN_BASE"
while [ -n "$1" ]
do
case "$1" in
"-pl") shift; PROXY_LOCATION="$(echo "$1" | sed -e "s/\//\\\\\//g")" ;;
*) HOSTNAME="$1.$HOSTNAME" ;;
esac
shift
done
cat template.conf | sed "s/HOSTNAME_BASE/$DOMAIN_BASE/g" | sed "s/HOSTNAME/$HOSTNAME/g" | sed "s/PROXYPATH/$PROXY_PATH/g" | sed "s/PROXYLOCATION/$PROXY_LOCATION/"
location /PROXYLOCATION {
proxy_pass PROXYPATH;
}
server {
server_name "HOSTNAME";
ssl_certificate /etc/letsencrypt/live/HOSTNAME_BASE/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/HOSTNAME_BASE/privkey.pem;
listen 443 ssl;
keepalive_timeout 60;
ssl on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "HIGH:!RC4:!aNULL:!MD5:!kEDH";
add_header Strict-Transport-Security 'max-age=604800';
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
# set max upload size
client_max_body_size 4000M;
sendfile on;
send_timeout 600s;
proxy_connect_timeout 600;
location /PROXYLOCATION {
proxy_pass_request_headers on;
proxy_pass_request_body on;
proxy_pass PROXYPATH;
}
}
После завершения копипасты и исправления домена в первом файле, запускаем скрипт ./compile_apps_configs.sh
и перезапускаем nginx: sudo systemd reload nginx
.
Подготовка Docker и скриптов
Само собой, в первую очередь нужно поставить docker
и docker-compose
:
sudo apt install docker docker-compose
После успешной установки следует заполнение yml
файлов и не только. На данный момент я остановился на следующей структуре файлов:
+-- doForAll
+-- gogs
¦ +-- docker-compose.yml
¦ +-- Dockerfile
¦ L-- .env
+-- grafana
¦ +-- configs
¦ ¦ +-- influxdb.conf
¦ ¦ L-- telegraf.conf
¦ +-- docker-compose.yml
¦ L-- .env
+-- jenkins
¦ +-- docker-compose.yml
¦ +-- .env
+-- makeFullUpdate
+-- radicale
¦ +-- docker-compose.yml
¦ L-- .env
L-- syncthing
+-- docker-compose.yml
L-- .env
Смысл такого разделения в том, чтобы упростить управление каждым из контейнеров или группой контейнеров (как в случае с grafana
— там поднимается четыре контейнера, один из которых — инициализация базы данных influxdb
). Сами исходники можно найти в репозитории.
После клонирования, нужно будет произвести несколько манипуляций:
- Во всех
.env
проверить значенияDATA_PATH
- Для
Grafana
и её окружения нужно будет также сгенерировать и куда-то записать следующие данные:
- Пароль для
Telegraf
. В файле.env
он лежит под ключомINFLUXDB_WRITE_USER_PASSWORD
, а в файлеconfigs/telegraf.conf
—password
- Пароль для
Grafana
. В файле.env
он лежит под ключомINFLUXDB_READ_USER_PASSWORD
, а кроме того будет использован при настройкеGrafana
- Пароль для
Последние штрихи
Последним штрихом будет создание задачи проверки и перезапуска контейнера.
Сам скрипт должен будет запускаться от пользователя root
. Для его заполнения выполните следующие операции:
# меняем пользователя на root
sudo -i
mkdir -p /root/scripts/
touch "/root/scripts/monitor_startup_docker_container"
chmod 700 "/root/scripts/monitor_startup_docker_container"
nano "/root/scripts/monitor_startup_docker_container"
Далее файл /root/scripts/monitor_startup_docker_container
заполняем следующим:
#!/bin/bash
function log() {
echo `date`: "$@"
}
container_name="$1"
true=1
false=0
function restartContainer() {
docker container restart "$1"
}
function checkContanerExitStatus() {
container_name="$1"
status_line="`docker container ps -a --filter "name=$container_name" --filter "exited=255" | grep "$container_name"`"
[[ -z "$status_line" ]] && echo $false || echo $true
}
function checkContanerStatusIsEqual() {
container_name="$1"
container_dest_status="$2"
status_line=" `docker container ps -a --filter "name=$container_name" --filter "status=$container_dest_status" | grep "$container_name"`"
[[ -z "$status_line" ]] && echo $false || echo $true
}
function isRunning() {
echo "`checkContanerStatusIsEqual "$container_name" "running"`"
}
while [[ "`isRunning`" != "$true" ]]; do
log check cycle "$container_name"
if [ "`checkContanerStatusIsEqual "$container_name" "exited"`" == "$true" -o "`checkContanerStatusIsEqual "$container_name" "dead"`" == "$true" ]; then
log restart "$container_name"
restartContainer "$container_name"
fi
if [[ "`isRunning`" -eq "$false" ]]; then
sleep 5
else
sleep 120
fi
done
log started "$container_name"
Остаётся заполнить crontab
конфигурацию. Это производится также от имени root
:
@reboot rm /root/startup_docker_logs
0 */5 * * * /root/scripts/monitor_startup_docker_container telegraf >> /root/startup_docker_logs
10 */5 * * * /root/scripts/monitor_startup_docker_container influxdb >> /root/startup_docker_logs
20 */5 * * * /root/scripts/monitor_startup_docker_container grafana >> /root/startup_docker_logs
30 */5 * * * /root/scripts/monitor_startup_docker_container jenkins >> /root/startup_docker_logs
40 */5 * * * /root/scripts/monitor_startup_docker_container gogs >> /root/startup_docker_logs
50 */5 * * * /root/scripts/monitor_startup_docker_container radicale >> /root/startup_docker_logs
10 */5 * * * /root/scripts/monitor_startup_docker_container syncthing >> /root/startup_docker_logs
Заключение
В итоге, у нас получилась относительно легко управляемая система, которая если ничего не обновлять в среднем почти не требует обслуживания. Что не было затронуто в рамках этой статьи:
- Настройка доступа по
SSH
к малинке: есть большое число туториалов на эту тему, вот пример с DigitalOcean - Настройка самих сервисов
- Покупка и настройка DNS для доменов
Буду рад комментариям и полезным замечаниям.
KentSilver
Уже не модно пихать в докер ради того, чтобы был докер. Все описанное не требует его, в чем мякотка статьи то?
Mokrassar
В целом — описание процесса установки, проблем по ходу
Относительно докера — избегание установки всего под ряд в систему и сложностей выноса данных на внешний носитель. Например, с тем же syncthing под линуксом я частенько встречался с проблемами настройки, начиная от обновлений, заканчивая расположением папок
В остальном — когда я задумывался о хостинге на малинке, ничего такого не было, разве что разрозненные статьи и отзывы коллег о том, что докер на малинке вообще не вариант (как выяснилось — вполне вариант)