Metabase на VPS: дашборди з PostgreSQL за вечір
TL;DR
Metabase можна розгорнути на VPS за один вечір за допомогою Docker Compose, підключити до PostgreSQL і відкрити дашборди через HTTPS. У цьому посібнику ми встановимо Metabase 0.57.x, PostgreSQL 17 і Caddy, налаштуємо окрему базу застосунку, користувача лише для читання, резервне копіювання та базовий захист сервера.
- Для невеликої команди достатньо VPS із 2 vCPU, 4 ГБ RAM і SSD від 40 ГБ.
- Metabase запускатиметься в Docker-контейнері, а PostgreSQL — зберігатиме налаштування, користувачів і запитання.
- Джерело аналітичних даних буде доступне Metabase через внутрішню Docker-мережу.
- Caddy автоматично отримає TLS-сертифікат Let's Encrypt для домену.
- Доступ до робочих даних здійснюватиметься через окремого користувача PostgreSQL із правами лише на читання.
- Щоденний бекап PostgreSQL і конфігурації надсилатиметься до зовнішнього S3-сховища.
1. TL;DR
У цьому розділі вже наведено короткий план: ми розміщуємо Metabase на VPS, запускаємо його через Docker Compose, підключаємо PostgreSQL і публікуємо інтерфейс через HTTPS. Докладні команди та конфігураційні файли наведено нижче.
2. Зміст
Стаття розрахована на власника VPS, який хоче самостійно отримати робочу систему бізнес-аналітики без обов’язкової залежності від хмарного сервісу. Приклад орієнтований на Ubuntu Server 24.04 LTS, домен bi.example.com і чистий сервер із публічною IPv4-адресою.
Команди передбачають підключення до сервера під користувачем із правами sudo. Якщо у вас уже є робоча база PostgreSQL, частину зі створення демонстраційної бази можна пропустити, але мережеву конфігурацію та конфігурацію користувачів усе одно варто перевірити.
3. Що ми налаштовуємо і навіщо
Що таке Metabase
Metabase — вебзастосунок для аналізу даних і створення дашбордів. Він підключається до PostgreSQL, MySQL, ClickHouse та інших джерел, після чого дає змогу створювати таблиці, графіки, KPI-картки та фільтри без написання SQL для кожного запиту.
Для команди Metabase зазвичай стає внутрішнім порталом аналітики: керівник переглядає виручку, маркетолог — воронку, служба підтримки — кількість звернень, а розробник — технічні метрики. На відміну від BI-систем, побудованих лише навколо SQL, Metabase пропонує візуальний конструктор запитань, але водночас зберігає повноцінний SQL-редактор.
Що працюватиме після налаштування
Підсумкова схема складатиметься з кількох контейнерів. Metabase відповідатиме за вебінтерфейс і дашборди, PostgreSQL — за внутрішню базу Metabase, а окремий контейнер PostgreSQL використовуватиметься як демонстраційне джерело даних. Caddy прийматиме HTTPS-запити й передаватиме їх Metabase.
| Компонент | Призначення | Порт |
|---|---|---|
| Metabase 0.57.x | Вебінтерфейс, запити й дашборди | 3000 у мережі |
| PostgreSQL 17 | Метадані Metabase | 5432 у мережі |
| PostgreSQL 17 | Демонстраційні аналітичні дані | 5432 у мережі |
| Caddy 2 | Reverse proxy і HTTPS | 80 і 443 |
Self-hosted або хмарний Metabase
Хмарний варіант не потребує обслуговування ОС, Docker, TLS і резервних копій. Він підходить, якщо важливіше швидко почати аналізувати дані, ніж контролювати інфраструктуру. Однак вартість зростає разом із кількістю користувачів, обсягом функцій і вимогами до корпоративного доступу.
Self-hosted Metabase на VPS дає контроль над розміщенням даних, мережею, версіями та витратами. Такий варіант корисний, якщо в компанії є спеціаліст, здатний оновлювати контейнери, перевіряти бекапи та реагувати на інциденти. Важливо розуміти: VPS не перетворює систему на повністю керований сервіс. Відповідальність за безпеку й відновлення залишається на власнику.
Як Metabase працює з PostgreSQL
У Metabase є власна application database. У ній зберігаються користувачі, права доступу, налаштування підключень, збережені запитання та структура колекцій. Для production не слід використовувати вбудовану H2-базу: вона призначена переважно для простого тестування та міграції.
Робоча база даних підключається окремо. Metabase не копіює автоматично всі таблиці на VPS, а виконує запити до джерела під час відкриття запитання або дашборда. Тому пропускна здатність і затримка мережі між Metabase і PostgreSQL впливають на швидкість візуалізацій.
4. Яка конфігурація VPS потрібна для цього завдання
Ресурси залежать не стільки від самого інтерфейсу Metabase, скільки від кількості одночасних запитів і швидкості PostgreSQL. Metabase може працювати на невеликому сервері, якщо дашборди відкривають кілька людей, а запити використовують індекси. Великі таблиці, десятки користувачів і часте оновлення графіків потребуватимуть більше CPU та пам’яті.
| Сценарій | CPU | RAM | Диск | Мережа |
|---|---|---|---|---|
| Тестування та особистий проєкт | 1–2 vCPU | 2–4 ГБ | 30–40 ГБ SSD | 100 Мбіт/с |
| Невелика команда до 15 користувачів | 2–4 vCPU | 4–8 ГБ | 60–100 ГБ SSD | 100–500 Мбіт/с |
| Кілька десятків користувачів | 4–8 vCPU | 8–16 ГБ | 100–250 ГБ NVMe | 500 Мбіт/с і вище |
Практична конфігурація
Для описаного у статті сценарію розумна відправна точка — 2 vCPU, 4 ГБ RAM, 60 ГБ SSD або NVMe, одна публічна IPv4 і канал від 100 Мбіт/с. На диску зберігатимуться Docker-образи, PostgreSQL, логи й тимчасові файли. Якщо джерело даних розміщене на іншому сервері, основний обсяг сховища потрібен не для таблиць, а для операційної системи, бекапів і кешу.
Можна взяти відповідний VPS із такими характеристиками або вибрати аналогічний сервер в іншого провайдера. Під час вибору перевірте, що дозволені вхідні з’єднання на 80 і 443, доступне встановлення Docker і є можливість робити знімки диска або підключати додаткове сховище.
Коли потрібен dedicated
Виділений сервер виправданий, якщо PostgreSQL уже обробляє важкі аналітичні запити, дані займають сотні гігабайт, потрібні гарантовані IOPS або на одному сервері планується запуск кількох сервісів. Dedicated також зручний для ETL, локального сховища резервних копій і великих materialized views.
Для звичайного Metabase перехід на dedicated сам собою не вирішує проблему повільних графіків. Спочатку потрібно перевірити плани виконання SQL, індекси, агрегації та кешування. Якщо запит виконується дві хвилини через повне сканування таблиці, додаткові ядра не завжди дадуть помітний результат.
Вибір локації
Локація впливає на затримку до PostgreSQL і користувачів. Якщо база даних і Metabase розташовані в різних регіонах, кожен інтерактивний запит отримує додаткову мережеву затримку. Для невеликих таблиць це майже непомітно, а для великої кількості послідовних запитів — уже суттєво.
Розміщуйте Metabase у тому самому регіоні, де розташована основна база або застосунок, який постачає дані. Для користувачів із кількох країн обирайте регіон із прийнятною затримкою або використовуйте CDN лише для статичного вмісту, не намагаючись кешувати персоналізовані відповіді Metabase.
5. Підготовка сервера
Створення користувача та оновлення Ubuntu
Нижче передбачається Ubuntu Server 24.04 LTS. Підключіться через SSH під користувачем, створеним провайдером, і замініть deploy на потрібне ім’я. Не закривайте поточну SSH-сесію до перевірки входу під новим користувачем.
# Обновляем индекс пакетов и устанавливаем исправления безопасности
sudo apt update && sudo apt full-upgrade -y
# Создаём отдельного администратора для повседневной работы
sudo adduser deploy
# Разрешаем пользователю выполнять административные команды
sudo usermod -aG sudo deploy
З локального комп’ютера скопіюйте публічний SSH-ключ. Команда не переносить приватний ключ на сервер, а додає лише відкритий ключ до файлу авторизації.
# Выполняем на локальном компьютере
ssh-copy-id deploy@SERVER_IP
# Проверяем вход новым пользователем
ssh deploy@SERVER_IP
Обмеження SSH
Після успішної перевірки входу за ключем забороніть автентифікацію за паролем і вхід root. Перед зміною файлу переконайтеся, що ключ справді працює в окремому вікні термінала.
# Открываем конфигурацию SSH
sudo nano /etc/ssh/sshd_config.d/ hardening.conf
У наведеній вище команді пробіл в імені каталогу неприпустимий. Використовуйте правильний варіант:
# Создаём отдельный файл настроек SSH
sudo nano /etc/ssh/sshd_config.d/hardening.conf
Вставте такі параметри:
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
X11Forwarding no
# Проверяем синтаксис конфигурации SSH
sudo sshd -t
# Применяем настройки без завершения текущих подключений
sudo systemctl reload ssh
Firewall і fail2ban
Відкрийте SSH, HTTP і HTTPS. Якщо SSH працює на нестандартному порту, вкажіть його замість 22. UFW не має блокувати вже встановлену SSH-сесію, але порядок дій усе одно краще дотримуватися: спочатку дозволити SSH, потім увімкнути firewall.
# Устанавливаем базовые инструменты и защиту от перебора паролей
sudo apt install -y ca-certificates curl gnupg git jq unzip ufw fail2ban
# Разрешаем SSH и веб-трафик
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Включаем firewall
sudo ufw --force enable
# Проверяем активные правила
sudo ufw status verbose
Створіть локальну конфігурацію fail2ban. Вона блокуватиме адреси, які багаторазово помиляються під час входу в SSH.
# Создаём настройки jail для SSH
sudo tee /etc/fail2ban/jail.d/sshd.local > /dev/null <<'EOF'
[sshd]
enabled = true
backend = systemd
port = 22
maxretry = 5
findtime = 10m
bantime = 1h
EOF
# Запускаем fail2ban и добавляем его в автозагрузку
sudo systemctl enable --now fail2ban
# Проверяем состояние SSH-защиты
sudo fail2ban-client status sshd
Синхронізація часу та базові параметри
Правильний час важливий для TLS, логів і cron-завдань. В Ubuntu зазвичай уже використовується systemd-timesyncd, тому достатньо перевірити його стан.
# Проверяем синхронизацию времени
timedatectl status
# Проверяем свободное место, память и загрузку
df -h
free -h
uptime
6. Встановлення ПЗ — покроково
Встановлення Docker Engine і Compose
Для production використовуйте офіційний репозиторій Docker, а не застарілий пакет зі стандартного репозиторію Ubuntu. На момент підготовки посібника цільовий стек — Docker Engine 27.x або новіший сумісний реліз і Docker Compose v2.
# Создаём каталог для ключей APT
sudo install -m 0755 -d /etc/apt/keyrings
# Загружаем официальный ключ Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Разрешаем чтение ключа пакетным менеджером
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Добавляем официальный репозиторий Docker для Ubuntu 24.04
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Устанавливаем Docker Engine, CLI и Compose plugin
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Разрешаем пользователю deploy обращаться к Docker без sudo
sudo usermod -aG docker "$USER"
Вийдіть із SSH і підключіться знову, щоб оновилася група користувача.
# Проверяем версии Docker и Compose
docker version
docker compose version
# Проверяем запуск тестового контейнера
docker run --rm hello-world
Створення структури проєкту
Усі файли проєкту розмістимо в /opt/metabase. Секрети зберігатимуться у файлі .env, який не слід додавати до Git і публікувати у вебкаталозі.
# Создаём каталоги проекта и хранения Caddy
sudo mkdir -p /opt/metabase/{postgres-init,analytics-init,caddy}
sudo chown -R "$USER":"$USER" /opt/metabase
cd /opt/metabase
# Создаём файл переменных окружения с закрытыми правами
touch .env
chmod 600 .env
Підготовка змінних середовища
Згенеруйте випадкові паролі. Пароль застосунку Metabase не повинен збігатися з паролем користувача аналітичної бази. Для production зберігайте копію секретів у менеджері паролів, інакше відновлення після втрати VPS буде ускладнене.
# Генерируем три независимых случайных значения
openssl rand -base64 32
openssl rand -base64 32
openssl rand -base64 32
Створіть /opt/metabase/.env із таким вмістом. Замініть усі значення після знака рівності.
POSTGRES_DB=metabase
POSTGRES_USER=metabase
POSTGRES_PASSWORD=CHANGE_ME_APP_DB_PASSWORD
ANALYTICS_DB=analytics
ANALYTICS_USER=analytics_reader
ANALYTICS_PASSWORD=CHANGE_ME_ANALYTICS_PASSWORD
MB_ENCRYPTION_SECRET_KEY=CHANGE_ME_LONG_RANDOM_SECRET
MB_SITE_URL=https://bi.example.com
TZ=UTC
Змінна MB_ENCRYPTION_SECRET_KEY використовується для шифрування чутливих значень у конфігурації Metabase. Не змінюйте її після першого запуску без розуміння наслідків: збережені налаштування підключень можуть стати недоступними.
Ініціалізація демонстраційної бази
Якщо у вас уже є PostgreSQL, цей крок можна адаптувати під наявну схему. Для самостійної перевірки створіть невеликий набір таблиць із замовленнями, клієнтами та товарами.
# Создаём SQL-файл начальной схемы аналитической базы
cat > analytics-init/001-schema.sql <<'EOF'
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
country TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id),
amount NUMERIC(12,2) NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO customers (name, country)
SELECT
'Customer ' || n,
CASE n % 4
WHEN 0 THEN 'RU'
WHEN 1 THEN 'KZ'
WHEN 2 THEN 'DE'
ELSE 'PL'
END
FROM generate_series(1, 100) AS n;
INSERT INTO orders (customer_id, amount, status, created_at)
SELECT
(random() 99 + 1)::bigint,
round((random() 490 + 10)::numeric, 2),
CASE
WHEN n % 10 = 0 THEN 'cancelled'
ELSE 'paid'
END,
now() - ((random() 180)::int || ' days')::interval
FROM generate_series(1, 3000) AS n;
CREATE INDEX orders_created_at_idx ON orders (created_at);
CREATE INDEX orders_customer_id_idx ON orders (customer_id);
CREATE INDEX orders_status_idx ON orders (status);
CREATE USER analytics_reader WITH PASSWORD 'CHANGE_ME_ANALYTICS_PASSWORD';
GRANT CONNECT ON DATABASE analytics TO analytics_reader;
GRANT USAGE ON SCHEMA public TO analytics_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO analytics_reader;
EOF
У демонстраційному SQL пароль має збігатися з ANALYTICS_PASSWORD у файлі .env. Для реального проєкту краще створювати користувача окремою командою або шаблонізувати SQL, щоб секрет не дублювався в кількох файлах.
Docker Compose
Створіть файл docker-compose.yml. Версії образів зафіксовані тегами, щоб раптове оновлення не змінило поведінку системи. Перед оновленням спочатку ознайомтеся з release notes Metabase і зробіть бекап.
services:
metabase-db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: ${TZ}
volumes:
- metabase_db_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
analytics-db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: ${ANALYTICS_DB}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: ${TZ}
volumes:
- analytics_db_data:/var/lib/postgresql/data
- ./analytics-init:/docker-entrypoint-initdb.d:ro
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d ${ANALYTICS_DB}"]
interval: 10s
timeout: 5s
retries: 5
metabase:
image: metabase/metabase:v0.57.0
restart: unless-stopped
depends_on:
metabase-db:
condition: service_healthy
analytics-db:
condition: service_healthy
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: ${POSTGRES_DB}
MB_DB_PORT: 5432
MB_DB_USER: ${POSTGRES_USER}
MB_DB_PASS: ${POSTGRES_PASSWORD}
MB_DB_HOST: metabase-db
MB_ENCRYPTION_SECRET_KEY: ${MB_ENCRYPTION_SECRET_KEY}
MB_SITE_URL: ${MB_SITE_URL}
JAVA_TIMEZONE: ${TZ}
expose:
- "3000"
networks:
- internal
caddy:
image: caddy:2.9
restart: unless-stopped
depends_on:
- metabase
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- internal
volumes:
metabase_db_data:
analytics_db_data:
caddy_data:
caddy_config:
networks:
internal:
driver: bridge
У Metabase 0.57.x актуальний образ може мати новіший patch-тег. Перед запуском перевірте доступність вибраного тега в офіційному реєстрі. Для production важливо фіксувати конкретну версію, а не використовувати latest.
7. Конфігурація, HTTPS і перевірка
Налаштування Caddy
До запуску Caddy створіть DNS-запис A для bi.example.com, що вказує на публічну IP-адресу VPS. Якщо використовується IPv6, переконайтеся, що запис AAAA також веде на цей сервер і firewall дозволяє IPv6-трафік.
# Создаём конфигурацию reverse proxy
cat > caddy/Caddyfile <<'EOF'
bi.example.com {
encode gzip zstd
reverse_proxy metabase:3000
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
log {
output file /data/access.log
format json
}
}
EOF
# Проверяем, что домен резолвится в IP сервера
dig +short bi.example.com
Якщо утиліту dig не встановлено, додайте пакет dnsutils. Результат має містити адресу саме вашого VPS.
# Устанавливаем утилиту проверки DNS при необходимости
sudo apt install -y dnsutils
# Проверяем DNS-запись ещё раз
dig +short bi.example.com
Перший запуск
Перед стартом перевірте файл Compose і змінні середовища. Docker Compose підставить значення з .env. Помилки в назвах змінних часто призводять до того, що контейнер запускається з порожнім паролем або не може підключитися до PostgreSQL.
# Проверяем итоговую конфигурацию Compose
docker compose config
# Запускаем контейнеры в фоновом режиме
docker compose up -d
# Смотрим состояние всех сервисов
docker compose ps
# Смотрим последние сообщения Metabase
docker compose logs --tail=100 metabase
Перший запуск Metabase може тривати кілька хвилин: контейнер застосовує міграції application database. Відкрийте у браузері https://bi.example.com. Caddy автоматично запитає сертифікат Let's Encrypt, якщо DNS уже оновився, а порти 80/443 доступні ззовні.
Початкове налаштування Metabase
У майстрі першого запуску створіть адміністративний обліковий запис з унікальним довгим паролем. Не використовуйте пароль від VPS або PostgreSQL. Вкажіть робочий часовий пояс, валюту та назву організації, після чого додайте підключення до аналітичної бази.
| Поле Metabase | Значення |
|---|---|
| Database type | PostgreSQL |
| Host | analytics-db |
| Port | 5432 |
| Database name | analytics |
| Username | analytics_reader |
| Password | Значення ANALYTICS_PASSWORD |
Ім’я analytics-db — це DNS-ім’я сервісу всередині Docker-мережі. Не вказуйте localhost: усередині контейнера Metabase воно означає сам контейнер Metabase, а не PostgreSQL.
Перевірка з’єднання з PostgreSQL
Перевірити мережу та права можна з контейнера Metabase або тимчасового клієнта PostgreSQL. Спочатку переконайтеся, що база відповідає.
# Проверяем состояние контейнеров
docker compose ps
# Проверяем доступность PostgreSQL из отдельного временного контейнера
docker run --rm --network metabase_internal \
-e PGPASSWORD="$ANALYTICS_PASSWORD" postgres:17 \
psql -h analytics-db -U analytics_reader -d analytics -c \
"SELECT count() AS orders_count FROM orders;"
Назва мережі може відрізнятися, якщо каталог проєкту має іншу назву. Дізнайтеся фактичну назву командою docker network ls. Для перевірки застосунку використовуйте HTTP-запит із самого сервера.
# Проверяем локальный HTTPS-ответ Caddy
curl -I https://bi.example.com
# Проверяем срок и параметры сертификата
curl -vI https://bi.example.com 2>&1 | grep -E "SSL connection|subject:|expire date:"
# Проверяем логи Caddy при проблеме с TLS
docker compose logs --tail=100 caddy
Створення першого дашборда
У розділі New створіть запит на основі таблиці orders. Для простої картки виручки виберіть суму поля amount із фільтром status = paid. Для графіка продажів згрупуйте суму за днем поля created_at. Потім збережіть запити в колекцію та об’єднайте їх на новому дашборді.
Для SQL-запиту можна використати наведений нижче запит. Параметр дати краще додавати через інтерфейс Metabase, щоб користувачі могли змінювати період без редагування SQL.
SELECT
date_trunc('day', created_at)::date AS day,
SUM(amount) AS revenue,
COUNT() AS paid_orders
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;
Права доступу
Адміністратор має створити групи користувачів і надати їм доступ лише до потрібних колекцій. Не робіть усіх співробітників адміністраторами: права адміністратора дають змогу змінювати підключення, користувачів і налаштування всієї системи.
Якщо використовуються чутливі дані, створіть окремі представлення PostgreSQL замість надання доступу до вихідних таблиць. Наприклад, представлення може приховувати email, телефон і внутрішні ідентифікатори, залишаючи лише агреговані показники. Також вимкніть або обмежте користувацькі SQL-запити, якщо аудиторія має працювати лише з підготовленими моделями.
8. Резервні копії та обслуговування
Що необхідно зберігати
Головна цінність Metabase міститься не в контейнері, а в його application database. У ній зберігаються користувачі, колекції, запитання, моделі, налаштування підключень і права. Тому потрібно регулярно зберігати базу metabase, а не лише Docker-образ.
- Дамп PostgreSQL application database Metabase.
- Дані робочої аналітичної бази, якщо вона розташована на цьому ж VPS.
- Файл
.envу зашифрованому або захищеному сховищі. docker-compose.yml, Caddyfile та SQL-скрипти ініціалізації.- Каталоги Caddy
/dataі/config, якщо потрібно зберегти стан сертифікатів. - Документація щодо DNS, версій образів і процедури відновлення.
Встановлення restic
Резервна копія на той самий диск захищає лише від помилки користувача або пошкодження бази. У разі втрати VPS вона зникне разом із вихідними даними. Для production використовуйте зовнішній S3-сумісний бакет, окремий сервер або об'єктне сховище в іншому регіоні.
# Встановлюємо restic і PostgreSQL-клієнт
sudo apt install -y restic postgresql-client
# Створюємо закритий файл із налаштуваннями віддаленого сховища
sudo nano /root/.restic-env
sudo chmod 600 /root/.restic-env
Приклад файлу:
export AWS_ACCESS_KEY_ID="CHANGE_ME"
export AWS_SECRET_ACCESS_KEY="CHANGE_ME"
export RESTIC_REPOSITORY="s3:https://s3.example.net/metabase-backups"
export RESTIC_PASSWORD="CHANGE_ME_RESTIC_PASSWORD"
Скрипт резервного копіювання
Скрипт спочатку створює дампи обох баз, потім додає конфігурацію та надсилає все до restic. Дампи тимчасово зберігаються в каталозі з правами root і видаляються після завершення. Зовнішній S3-бакет має підтримувати версіонування та обмежений доступ за ключем.
# Створюємо каталог для скрипта
sudo mkdir -p /usr/local/sbin /var/backups/metabase
sudo nano /usr/local/sbin/metabase-backup.sh
#!/usr/bin/env bash
set -Eeuo pipefail
PROJECT="/opt/metabase"
BACKUP_DIR="/var/backups/metabase"
STAMP="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
source /root/.restic-env
set -a
source "${PROJECT}/.env"
set +a
mkdir -p "${BACKUP_DIR}/${STAMP}"
docker compose -f "${PROJECT}/docker-compose.yml" exec -T metabase-db \
pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
| gzip > "${BACKUP_DIR}/${STAMP}/metabase.sql.gz"
docker compose -f "${PROJECT}/docker-compose.yml" exec -T analytics-db \
pg_dump -U postgres -d "${ANALYTICS_DB}" \
| gzip > "${BACKUP_DIR}/${STAMP}/analytics.sql.gz"
cp "${PROJECT}/.env" "${BACKUP_DIR}/${STAMP}/.env"
cp "${PROJECT}/docker-compose.yml" "${BACKUP_DIR}/${STAMP}/docker-compose.yml"
cp "${PROJECT}/caddy/Caddyfile" "${BACKUP_DIR}/${STAMP}/Caddyfile"
restic backup "${BACKUP_DIR}/${STAMP}" --tag metabase
restic forget --tag metabase --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
rm -rf "${BACKUP_DIR:?}/${STAMP}"
# Робимо скрипт виконуваним
sudo chmod 700 /usr/local/sbin/metabase-backup.sh
# Ініціалізуємо restic-репозиторій один раз
sudo bash -c 'source /root/.restic-env && restic init'
# Запускаємо тестову резервну копію вручну
sudo /usr/local/sbin/metabase-backup.sh
# Перевіряємо список збережених знімків
sudo bash -c 'source /root/.restic-env && restic snapshots'
Зверніть увагу, що передавання паролів через змінні середовища зручне для прикладу, але доступ до них може отримати процес із достатніми правами. Обмежте доступ до root-файлів і використовуйте IAM-ключ із правами лише на конкретний бакет.
Планувальник cron
Запускайте резервне копіювання вночі або в період низького навантаження. Для невеликих баз щоденної копії достатньо, але критичні дані зазвичай потребують коротшого інтервалу та окремого WAL-архівування PostgreSQL.
# Відкриваємо системний crontab root
sudo crontab -e
Додайте рядок:
30 2 /usr/local/sbin/metabase-backup.sh >> /var/log/metabase-backup.log 2>&1
Перевірка відновлення
Резервну копію, яку жодного разу не відновлювали, не можна вважати перевіреною. Щонайменше раз на місяць запускайте тимчасовий PostgreSQL, розпаковуйте дамп і перевіряйте наявність користувачів, колекцій і запитань. Для робочих даних окремо перевірте кілька ключових таблиць і контрольні суми або кількість рядків.
# Завантажуємо останній знімок у тимчасовий каталог
sudo mkdir -p /tmp/metabase-restore
sudo bash -c 'source /root/.restic-env && restic restore latest --tag metabase --target /tmp/metabase-restore'
# Шукаємо відновлені дампи
find /tmp/metabase-restore -type f -name '*.sql.gz' -ls
Оновлення Metabase
Не оновлюйте production автоматично щоразу, коли з'являється новий Docker-тег. Спочатку створіть резервну копію, прочитайте список змін і перевірте нову версію на копії application database. Для невеликої команди достатньо maintenance window тривалістю 10–20 хвилин.
# Зберігаємо поточні версії образів і стан контейнерів
cd /opt/metabase
docker compose images
docker compose ps
# Створюємо резервну копію перед оновленням
sudo /usr/local/sbin/metabase-backup.sh
# Завантажуємо нові образи вибраних версій
docker compose pull
# Перезапускаємо сервіси з оновленими образами
docker compose up -d
# Стежимо за міграціями Metabase
docker compose logs -f --tail=200 metabase
Якщо нова версія потребує міграції бази, відкат контейнера без відновлення бази може бути небезпечним. Для важливих інсталяцій спочатку створіть snapshot VPS або відновіть дамп в окремий тестовий екземпляр.
Спостереження за ресурсами
Стежте за вільним місцем, пам'яттю та часом виконання запитів. PostgreSQL і Docker-логи можуть поступово заповнити диск. Додайте ротацію логів Docker, особливо якщо Caddy або Metabase працюють із докладним рівнем журналювання.
# Перевіряємо споживання ресурсів контейнерами
docker stats --no-stream
# Перевіряємо розміри Docker-даних
docker system df
# Шукаємо каталоги, що займають місце
sudo du -xh /var/lib/docker /opt/metabase | sort -h | tail -n 20
9. Усунення несправностей і FAQ
Чому відкривається помилка 502 Bad Gateway?
Спочатку перевірте стан контейнера Metabase командою docker compose ps та останні повідомлення docker compose logs --tail=200 metabase. Якщо контейнер перезапускається, імовірна помилка підключення до application database, неправильний пароль або нестача пам'яті. Якщо Metabase працює, перевірте, що Caddy використовує ім'я metabase, а не localhost:3000. Після зміни Caddyfile перезапустіть Caddy.
Чому Caddy не отримує сертифікат Let's Encrypt?
Переконайтеся, що DNS-запис домену вже вказує на публічний IP VPS. Порти 80 і 443 мають бути дозволені в UFW та у зовнішньому firewall провайдера. Якщо існує неправильний запис AAAA, Let's Encrypt може звертатися через IPv6 до іншого сервера. Перевірте docker compose logs caddy, потім виконайте curl -I http://bi.example.com із зовнішнього комп'ютера. Не використовуйте закритий для інтернету домен без DNS-челенджу.
Metabase не підключається до PostgreSQL з помилкою “connection refused”
У Docker Compose у полі Host потрібно вказати ім'я сервісу analytics-db і порт 5432. Не вказуйте IP контейнера: він може змінитися після перезапуску. Перевірте docker compose ps, healthcheck бази та логи docker compose logs analytics-db. Якщо базу було ініціалізовано раніше, зміна змінних POSTGRES_PASSWORD не змінює вже створений пароль автоматично. У такому разі пароль потрібно змінити SQL-командою всередині PostgreSQL.
Чому користувач analytics_reader отримує permission denied?
Перевірте права на схему, наявні таблиці та default privileges. SQL із каталогу analytics-init виконується лише під час першого створення порожнього тому PostgreSQL. Якщо том уже існує, новий SQL-файл автоматично не запускається. Надайте права вручну: GRANT USAGE ON SCHEMA public TO analytics_reader і GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_reader. Для нових таблиць налаштуйте ALTER DEFAULT PRIVILEGES від імені власника таблиць.
Дашборди завантажуються повільно. Що перевірити?
Почніть із SQL-запитів, а не зі збільшення VPS. Виконайте проблемний запит у PostgreSQL з EXPLAIN (ANALYZE, BUFFERS) і перевірте, чи використовуються індекси за датами, зовнішніми ключами та полями фільтрації. Зменште діапазон даних, об'єднайте повторювані запити та створіть агреговані представлення. У Metabase перевірте, чи не запускається одночасно надто багато карток. Після цього аналізуйте CPU, RAM, IOPS і мережеву затримку.
Яка конфігурація VPS мінімально підійде?
Для тестового Metabase достатньо 1–2 vCPU, 2 ГБ RAM і 30 ГБ SSD, але такий сервер майже не залишає запасу для PostgreSQL, Docker і фонових завдань. Практичний мінімум для невеликої команди — 2 vCPU, 4 ГБ RAM і 40–60 ГБ SSD. Якщо на тому ж VPS розташована аналітична база, ETL або регулярні важкі запити, обирайте 4 vCPU та 8 ГБ RAM. Використовуйте SSD, а не повільний HDD.
Що обрати — VPS чи dedicated для цього завдання?
VPS підходить для більшості невеликих і середніх інсталяцій Metabase: він дешевший, швидко масштабується та не потребує керування фізичним сервером. Dedicated потрібен за великих обсягів PostgreSQL, високих вимог до IOPS, постійного ETL-навантаження або потреби гарантувати відсутність сусідів на фізичному хості. Перед переходом виміряйте реальне навантаження. Часто індекси та агреговані таблиці дають більший ефект, ніж додаткові ядра.
Чи можна відкрити Metabase напряму через порт 3000?
Технічно можна, але для production це погана практика. Користувачі підключатимуться без належного TLS, а застосунок буде напряму доступний з інтернету. Використовуйте Caddy або інший reverse proxy на портах 80/443, а порт 3000 залишайте лише всередині Docker-мережі. Не додавайте ports: "3000:3000" у Compose. Якщо такий порт уже опубліковано, видаліть його та перезапустіть контейнери.
Що робити, якщо закінчилася RAM і контейнер Metabase перезапускається?
Перевірте docker stats, free -h і системний журнал на наявність OOM killer. Спочатку зменште паралельність важких запитів, оптимізуйте PostgreSQL і зупиніть непотрібні сервіси. Тимчасовий swap на SSD може запобігти аварійному завершенню, але не замінює фізичну пам'ять. Для постійного аналітичного навантаження збільште VPS щонайменше до 8 ГБ RAM і налаштуйте моніторинг, щоб бачити проблему до відмови.
10. Висновки та наступні кроки
У результаті ми отримали self-hosted Metabase на VPS із PostgreSQL 17, Docker Compose, HTTPS через Caddy та окремим користувачем бази лише для читання. Конфігурація підходить для невеликої команди й дає змогу створювати дашборди за замовленнями, виручкою та іншими даними без ручного встановлення Java і вебсервера.
- Налаштуйте моніторинг CPU, RAM, диска, часу відповіді PostgreSQL і строку дії сертифіката.
- Для зростання навантаження винесіть аналітичну базу на окремий сервер, додайте read replica або агреговані таблиці.
- Регулярно перевіряйте відновлення резервних копій і тестуйте оновлення Metabase на копії application database.