Для владельца нескольких сайтов важно не только знать, что они работают, но и иметь красивую публичную страницу статуса. Uptime Kuma — отличный инструмент, но иногда хочется чего-то более легковесного и минималистичного. В этой статье мы разберем установку EndPoll на VPS с Ubuntu 24.04, настройку уведомлений на Яндекс.Почту (актуально для российских серверов) и создание кастомной статусной страницы на PHP, которая берет данные из API EndPoll.
👉 Живой пример: посмотреть, как выглядит итоговая страница статуса, можно по адресу status.biggratz.ru.
Почему EndPoll?
EndPoll — это легковесный инструмент мониторинга и статусных страниц. Его ключевые особенности:
- Минимализм: чистый дашборд без лишнего визуального шума.
- Конфигурация через YAML: никаких баз данных и сложных веб-интерфейсов для настройки. Вы просто правите текстовый файл.
- JSON API: встроенный эндпоинт
/api/hosts, который отдает статусы в машиночитаемом формате. Это позволяет легко создавать собственные фронтенды. - Легкость: Docker-образ весит около 17 МБ.
Шаг 1. Установка EndPoll через Docker
Предполагается, что Docker уже установлен на вашем VPS. Если нет — установите его стандартным способом для Ubuntu 24.04.
Создайте директорию для проекта и перейдите в нее:
mkdir -p ~/endpoll && cd ~/endpoll
Запустите контейнер одной командой:
sudo docker run -d \
--name endpoll \
--restart unless-stopped \
-p 8822:8822 \
-v $(pwd)/config.yaml:/app/config.yaml \
-v $(pwd)/data:/app/data \
exelban/endpoll:latest
Что делают эти флаги:
-p 8822:8822— открывает веб-интерфейс на порту 8822.-v .../config.yaml:/app/config.yaml— монтирует ваш конфиг внутрь контейнера. Изменения в файле подхватываются автоматически (hot reload).-v .../data:/app/data— сохраняет историю проверок на хосте, чтобы она не терялась при перезапуске.--restart unless-stopped— гарантирует автозапуск после ребута VPS.
После запуска дашборд доступен по адресу http://IP_ВАШЕГО_VPS:8822. Не забудьте открыть порт 8822 в файрволе.

Шаг 2. Настройка конфигурации и уведомлений
Создайте файл config.yaml в папке ~/endpoll. Вот полный рабочий пример с четырьмя сайтами и уведомлениями на Яндекс.Почту:
# Глобальные настройки
interval: 30s
timeout: 60s
failureThreshold: 2
# Настройки интерфейса
ui:
title: "Мониторинг BigGratz"
hideURL: false
# Уведомления
notifications:
initializationMessage: true # Отправит письмо при старте (удобно для проверки SMTP)
shutdownMessage: false
smtp:
host: "smtp.yandex.ru"
port: 587 # ⚠️ Важно: используйте 587, порт 465 часто заблокирован на VPS
username: "your_email@yandex.ru"
password: "ВАШ_ПАРОЛЬ_ПРИЛОЖЕНИЯ" # Создайте пароль приложения в Яндекс ID!
from: "your_email@yandex.ru"
to:
- "your_email@yandex.ru"
# Сайты для мониторинга
hosts:
- name: "biggratz.ru"
url: "https://biggratz.ru"
- name: "eda4you.ru"
url: "https://eda4you.ru"
- name: "vault33.ru"
url: "https://vault33.ru"
- name: "fallout.vault33.ru"
url: "https://fallout.vault33.ru"

⚠️ Важные нюансы для российских VPS
- Telegram не работает. API Telegram заблокирован на уровне DPI в российских дата-центрах. Уведомления просто не дойдут. Используйте Email (SMTP) — это надежно.
- Порт 465 часто закрыт. Многие хостеры блокируют исходящий SSL-SMTP. Порт 587 (STARTTLS) обычно открыт. Если письма не уходят — проверьте доступность порта командой
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/smtp.yandex.ru/587'. - Пароль приложения. Обычный пароль от Яндекс.Почты не подойдет для SMTP. Зайдите в Яндекс ID → Безопасность → Пароли приложений и создайте отдельный пароль для EndPoll.
Совет: После успешной проверки уведомлений замените
initializationMessage: trueнаfalse, чтобы не получать письмо при каждом перезапуске контейнера.
Шаг 3. Собственная страница статуса на PHP

Стандартный дашборд EndPoll минималистичен и не поддерживает кастомизацию дизайна. Но благодаря JSON API (/api/hosts) мы можем создать свою красивую страницу, которая отображает статусы в любом стиле.
Подготовка
Установите PHP и Nginx (если еще не установлены):
sudo apt install -y php-fpm php-curl nginx
Создайте директорию для страницы:
sudo mkdir -p /var/www/status/img
Положите вашу фоновую картинку в /var/www/status/img/hero.png.
Код страницы
Создайте файл /var/www/status/index.php:
<?php
// URL API EndPoll (используйте localhost, если PHP и EndPoll на одном сервере)
$apiUrl = 'http://localhost:8822/api/hosts';
// Кастомные имена и ссылки (ключ = id из EndPoll API)
$overrides = [
'rgeE-T' => ['BigGratz', 'https://biggratz.ru'],
'HTC8yP' => ['Eda4You', 'https://eda4you.ru'],
'H9cGDY' => ['Vault33', 'https://vault33.ru'],
'GzHN-y' => ['Fallout Vault33', 'https://fallout.vault33.ru'],
];
// Запрос к API
$ch = curl_init($apiUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = null;
if ($httpCode === 200 && $response) {
$data = json_decode($response, true);
}
$overallStatus = $data['status'] ?? 'unknown';
$hosts = $data['hosts'] ?? [];
function statusLabel(string $s): string {
return match ($s) {
'up' => 'Работает',
'down' => 'Недоступен',
'degraded' => 'Сбои',
default => 'Неизвестно',
};
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Статус сервисов</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
/* 🖼️ Фон: градиент + ваша картинка.
Прозрачность 0.45/0.55 — баланс между видимостью картинки и читаемостью текста */
background:
linear-gradient(135deg, rgba(15, 20, 40, 0.45), rgba(30, 15, 50, 0.55)),
url('/img/hero.png') center/cover no-repeat fixed;
color: #fff;
}
.card {
background: rgba(20, 22, 35, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
padding: 40px;
max-width: 680px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
}
h1 { font-size: 1.8em; font-weight: 600; letter-spacing: -0.02em; }
.overall {
display: flex; align-items: center; gap: 8px;
font-size: 0.9em; font-weight: 500;
padding: 6px 14px; border-radius: 20px;
background: rgba(255, 255, 255, 0.05);
}
.overall.up { color: #4ade80; }
.overall.down { color: #f87171; }
.overall.degraded { color: #fbbf24; }
.overall.unknown { color: #94a3b8; }
.overall .pulse {
width: 8px; height: 8px; border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.site {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 20px; margin-bottom: 10px;
background: rgba(255, 255, 255, 0.03);
border-radius: 14px; text-decoration: none; color: inherit;
transition: all 0.2s ease; border: 1px solid transparent;
}
.site:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.site-info { display: flex; flex-direction: column; gap: 4px; }
.site-name { font-weight: 500; font-size: 1.05em; }
.site-meta { font-size: 0.78em; color: #8892b0; }
.status {
display: flex; align-items: center; gap: 8px;
font-size: 0.85em; font-weight: 600;
padding: 5px 12px; border-radius: 12px; flex-shrink: 0;
}
.status.up { color: #4ade80; background: rgba(74, 222, 128, 0.1); }
.status.down { color: #f87171; background: rgba(248, 113, 113, 0.1); }
.status.degraded { color: #fbbf24; background: rgba(251, 191, 36, 0.1); }
.status.unknown { color: #94a3b8; background: rgba(148, 163, 184, 0.1); }
.dot {
width: 10px; height: 10px; border-radius: 50%;
background: currentColor; box-shadow: 0 0 10px currentColor;
}
.footer {
margin-top: 28px; padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
text-align: center; color: #64748b; font-size: 0.8em;
}
.footer a {
color: #64748b; text-decoration: none;
border-bottom: 1px dashed rgba(100, 116, 139, 0.4);
}
.footer a:hover { color: #94a3b8; }
.error {
padding: 20px; background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.2);
border-radius: 12px; color: #f87171; font-size: 0.9em;
}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>🛰️ Статус сервисов</h1>
<div class="overall <?= htmlspecialchars($overallStatus) ?>">
<span class="pulse"></span>
<?= $overallStatus === 'up' ? 'Всё работает' : statusLabel($overallStatus) ?>
</div>
</div>
<?php if ($data === null): ?>
<div class="error">Не удалось получить данные от EndPoll.</div>
<?php else: ?>
<?php foreach ($hosts as $host): ?>
<?php
$id = $host['id'] ?? '';
$name = $overrides[$id][0] ?? ($host['name'] ?? 'Без имени');
$link = $overrides[$id][1] ?? ($host['url'] ?? '#');
$status = $host['status'] ?? 'unknown';
$code = $host['last']['code'] ?? null;
$rt = $host['last']['responseTimeMs'] ?? null;
$meta = [];
if ($code !== null) $meta[] = "HTTP $code";
if ($rt !== null) $meta[] = "{$rt} ms";
?>
<a class="site" href="<?= htmlspecialchars($link) ?>" target="_blank" rel="noopener">
<span class="site-info">
<span class="site-name"><?= htmlspecialchars($name) ?></span>
<?php if ($meta): ?>
<span class="site-meta"><?= htmlspecialchars(implode(' · ', $meta)) ?></span>
<?php endif; ?>
</span>
<span class="status <?= htmlspecialchars($status) ?>">
<span class="dot"></span>
<?= statusLabel($status) ?>
</span>
</a>
<?php endforeach; ?>
<?php endif; ?>
<div class="footer">
Данные: <a href="http://localhost:8822/" target="_blank">EndPoll</a>
· Обновлено <?= date('H:i:s') ?>
</div>
</div>
<script>setTimeout(() => location.reload(), 30000);</script>
</body>
</html>
Настройка Nginx
Создайте конфиг /etc/nginx/sites-available/status:
server {
listen 80;
server_name _; # Замените на ваш домен, когда он появится
root /var/www/status;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
}
Активируйте и перезапустите:
sudo ln -s /etc/nginx/sites-available/status /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default # Уберите дефолтный конфиг!
sudo nginx -t && sudo systemctl reload nginx
⚠️ Частая ошибка: Если после настройки видите «Welcome to nginx!» вместо своей страницы — значит, дефолтный конфиг не удален и перехватывает запросы. Убедитесь, что в
/etc/nginx/sites-enabled/остался только ваш конфигstatus.
Как узнать ID сайтов для массива $overrides
Откройте API EndPoll в браузере: http://IP_VPS:8822/api/hosts. В JSON-ответе у каждого хоста есть поле id (например, "id": "rgeE-T"). Именно эти значения нужно использовать как ключи в массиве $overrides.
Фоновая картинка
Картинка должна лежать в /var/www/status/img/hero.png. Права доступа:
sudo chown -R www-data:www-data /var/www/status
sudo chmod 644 /var/www/status/img/hero.png
Если картинка не видна — скорее всего, градиент слишком плотный. В CSS уменьшите значения прозрачности (например, с 0.45/0.55 до 0.25/0.35). Если картинка открывается напрямую по http://IP/img/hero.png, но не видна на странице — проблема точно в градиенте, а не в Nginx.
Итог
Мы получили двухуровневую систему мониторинга:
| Компонент | Адрес | Назначение |
|---|---|---|
| EndPoll | http://IP:8822 | Бэкенд: проверка доступности, история, SMTP-уведомления |
| PHP-страница | http://IP (порт 80) | Фасад: красивый дизайн, кастомные имена, фон |
| JSON API | http://IP:8822/api/hosts | Связующее звено между ними |
EndPoll выполняет всю тяжелую работу по мониторингу и оповещениям, а PHP-страница дает полную свободу в дизайне. При необходимости можно добавить кэширование API-запросов, автообновление без перезагрузки через fetch(), или защиту паролем через auth_basic в Nginx.



