Всем привет! В этой статье я поделюсь своим взглядом на инфраструктуру, которая помогает эффективно решать следующие задачи:

  • Доставлять запросы до бэкенда и фронтенда при переходе по купленному домену

  • Работать с сервером через терминал

  • Входить в контейнер через множество действий

  • Отлаживать через терминал

  • Мониторить состояние железа сервера

  • Заново изучать, как настроить мониторинг Docker-приложений

  • Использовать готовые решения вместо изобретения «велосипедов»

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

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

В конце статьи вы найдёте ссылку на репозиторий с шаблоном.

Основные принципы моего подхода:

  • Минимум ручных действий на сервере

  • Переиспользование готовых решений

  • Задел под «фабрику» бизнес-логики

  • Система, близкая к Production

  • Изоляция разработчиков от сервера

Стек

Инструментарий
Инструментарий

Конечный вариант с бизнес-логикой и базой данных будет выглядеть так:

Структура решения
Структура решения

В качестве прокси я также пробовал Nginx и Nginx Proxy Manager.

Альтернативы Caddy

  • Nginx Proxy Manger

  • Nginx + Certbot

Nginx Proxy Manager — удобное решение, но мне показалось, что оно достаточно ограничено в функционале.

Использование Nginx в паре с Certbot требует дополнительной настройки SSL. Кроме того, добавление и управление новыми поддоменами требует действий.

Caddy оказался универсальным решением — золотой серединой между гибкостью Nginx и простотой управления SSL, то есть он покрывает обе задачи. Именно поэтому я остановился на Caddy.

Общий конвейер

Основные шаги:

  1. Настройка VPS сервера:

    1. Установка Docker и Docker Сompose

    2. Ручное создание Docker Network

    3. Установка Gitlab Runner:

      • Создание в Gitlab группы репозиториев

      • Подключение Group Runner

  2. Настройка прокси и SSL для доменов:

    1. Развёртывание Caddy через Runner

  3. Настройка мониторинга Docker:

    1. Развёртывание Portainer через Runner

  4. База мониторинга для приложений:

    1. Развёртывание через Runner Monitoring (Grafana, Loki, Prometheus)

  5. Написание бизнес-логики, не возвращаясь к пунктам 1—4 и не заходя на сервер.

Структура репозитория

  • Caddy

  • Portainer

  • Monitoring

  • App-1

  • App-n

0. Покупаем домен на хостинге

1. Настраиваем Docker и Gitlab Runner

2. Ставим Caddy через Gitlab Runner

Схема на шаге настройки Caddy
Схема на шаге настройки Caddy

Организация структуры репозитория для Caddy:

Caddy Gitlab репозиторий
Caddy Gitlab репозиторий

Один поддомен = один <sub_domain>.caddy.

Все наши поддомены храним здесь: ./caddy/conf.d .

В Caddyfile указываем host:2019/metrics для дальнейшего мониторинга и подключаем конфиги из conf.d.

{
    admin 0.0.0.0:2019

    # Включаем встроенный сбор метрик для Prometheus
    servers {
        metrics
    }
}

# Подгружаем настройки ваших доменов из подпапки
import /etc/caddy/conf.d/*.caddy

Настраиваем домены для мониторинга

grafana.my-site.ru {
    reverse_proxy vps_grafana:3000
}
prometheus.my-site.ru {
    reverse_proxy vps_prometheus:9090
}

Для восприятия просто, конфиг пишется легко.

docker-compose.yml для проекта будет такой:

version: '3.8'

services:
  caddy:
    image: caddy:2.8
    container_name: vps_caddy_proxy
    restart: unless-stopped
    ports:
      - "80:80"   # Для выпуска SSL и редиректа с HTTP на HTTPS
      - "443:443" # Для защищенного HTTPS трафика ваших сайтов
    volumes:
      # Монтируем всю папку конфигурации (Caddyfile и папку conf.d внутри)
      - ./caddy:/etc/caddy
      - ./caddy/html:/usr/share/caddy/html
      # Тома для хранения SSL-сертификатов Let's Encrypt на хосте
      - caddy_data:/data
      - caddy_config:/config
    logging:
      driver: json-file
    # Автоматическое обнаружение для Prometheus и Promtail работает прямо на Caddy!
    labels:
      prometheus.scrape: "true"
      prometheus.port: "2019"         # Порт Caddy, где лежат встроенные метрики
      prometheus.interval: "10s"
      environment: "production"
      logging.scrape: "true"          # Включает сбор логов запросов Caddy в Grafana Loki
    networks:
      - vps_common_network

volumes:
  caddy_data:
    name: vps_caddy_data
  caddy_config:
    name: vps_caddy_config

networks:
  vps_common_network:
    external: true

Ничего сверхъестественного — обычный docker-compose.yml. Тему Labels пока опустим, и вернёмся к ней позже.

3. Ставим Portainer

Что получаем:

Интерфейс-инструмент для управления Docker, а также магазин приложений для автозапуска
Интерфейс-инструмент для управления Docker, а также магазин приложений для автозапуска

Например, если вашему B2B-AI-SaaS-стартапу будет нужен блог, вы cделаете это в два клика:

Шаблон для блога
Шаблон для блога

docker-compose.yml

version: "3.8"
services:
  twportainer:
    image: portainer/portainer-ce:latest
    container_name: twportainer
    environment:
      - TZ=Europe/Moscow
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - vps_common_network
    restart: always


volumes:
  portainer_data:
    name: vps_portainer_data
    
networks:
  vps_common_network:
    external: true

Этого вполне достаточно. Делаем один раз и не дорабатываем.

4. Ставим мониторинг

Теперь подробнее рассмотрим docker-compose.yml :

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:v3.13.1
    container_name: vps_prometheus
    user: root
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - prometheus_data:/prometheus
    command:
      # Указывает путь к главному файлу конфигурации внутри контейнера.
      - '--config.file=/etc/prometheus/prometheus.yml'
      # Задает директорию, куда Prometheus будет сохранять собранные метрики (базу данных TSDB).
      - '--storage.tsdb.path=/prometheus'
      # Путь к библиотекам консольных шаблонов (встроенная фича Prometheus для кастомных HTML-панелей).
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      # Время хранения данных. Метрики старше 200 часов (примерно 8.3 дня) будут автоматически удаляться.
      - '--storage.tsdb.retention.time=200h'
      # Это позволяет обновлять конфиг prometheus.yml без перезапуска контейнера (через curl-запрос).
      - '--web.enable-lifecycle'  # curl -X POST http://localhost:9090/-/reload
    logging:
      driver: json-file
    networks:
      - vps_common_network
    depends_on:
      - loki

  grafana:
    image: grafana/grafana:13.1.1
    container_name: vps_grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin # В продакшене лучше вынести в .env файл
      - GF_USERS_ALLOW_SIGN_UP=false
    logging:
      driver: json-file
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    networks:
      - vps_common_network
    depends_on:
      - prometheus
      - loki

  loki:
    image: grafana/loki:3.7.4
    container_name: vps_loki
    command: -config.file=/etc/loki/local-config.yaml
    logging:
      driver: json-file
    volumes:
      - loki_data:/loki
    networks:
      - vps_common_network

  promtail:
    image: grafana/promtail:3.6.11
    container_name: vps_promtail
    user: root
    volumes:
      - /var/log:/var/log:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./promtail/promtail-config.yml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    logging:
      driver: json-file
    networks:
      - vps_common_network
    depends_on:
      - loki


volumes:
  prometheus_data:
  grafana_data:
  loki_data:

networks:
  vps_common_network:
    external: true

В этом конфиге можно ничего не менять.

Итак, datasources.yml описывает, откуда Grafana может брать данные:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100

А prometheus.yml отвечает за сбор метрик с Docker-контейнеров:

global:
  scrape_interval: 5s         # Интервал по умолчанию для всех
  evaluation_interval: 5s

scrape_configs:
  # 1. Сам Prometheus мониторит себя локально
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # 2. Автоматическое обнаружение всех остальных контейнеров
  - job_name: 'docker-containers'
    docker_sd_configs:
      - host: unix:///var/run/docker.sock

    relabel_configs:
      # Фильтр: собираем метрики только с контейнеров, где указано prometheus.scrape: "true"
      - source_labels: [__meta_docker_container_label_prometheus_scrape]
        regex: 'true'
        action: keep

      # Формируем адрес контейнера (имя_контейнера:порт_из_метки)
      - source_labels: [__meta_docker_container_host, __meta_docker_container_label_prometheus_port]
        regex: '([^;]+);([^;]+)'
        replacement: '${1}:${2}'
        target_label: __address__

      # Создаем красивый лейбл "app" из имени контейнера (удаляем начальный слэш)
      - source_labels: [__meta_docker_container_name]
        regex: '/(.*)'
        target_label: app

      # Автоматически прокидываем лейбл "environment" из Docker-метки прямо в метрики
      - source_labels: [__meta_docker_container_label_environment]
        target_label: environment

      # Динамический scrape_interval: если у контейнера есть метка prometheus.interval, применяем её
      - source_labels: [__meta_docker_container_label_prometheus_interval]
        regex: '(.+)'
        target_label: __scrape_interval__

      # Динамический путь: если у контейнера есть метка prometheus.path, подменяем стандартный /metrics
      - source_labels: [__meta_docker_container_label_prometheus_path]
        regex: '(.+)'
        target_label: __metrics_path__

Этот инструмент нацелен на использование Labels в Docker-контейнерах. Хотя эта концепция не является новинкой, она хорошо подходит для общего понимания.

Labels:

  • Prometheus.scrape

  • Prometheus.port

  • Environment

  • Interval

  • Path

Их использование позволяет настроить мониторинг один раз и не изменять конфигурацию впоследствии. При добавлении нового бизнес-домена или сервиса подключение к системе мониторинга сводится к простому добавлению необходимых меток в контейнер.

promtail-config.yml

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/*log

  - job_name: docker-containers
    docker_sd_configs:
      - host: unix:///var/run/docker.sock # Подключаемся к сокету Docker

    relabel_configs:
      # Фильтр: собираем логи только если у контейнера есть лабель logging.scrape=true
      - source_labels: [__meta_docker_container_label_logging_scrape]
        regex: 'true'
        action: keep

      # Автоматически создаем лейбл container_name (Promtail сам уберет начальный слэш)
      - source_labels: [__meta_docker_container_name]
        target_label: container_name

      # Автоматически добавляем имя Docker-образа
      - source_labels: [__meta_docker_container_image]
        target_label: image_name

      # Пробрасываем лейбл окружения из Docker-меток (как делали в Prometheus)
      - source_labels: [__meta_docker_container_label_environment]
        target_label: environment

      # Задаем фиксированный лейбл для Grafana Loki, чтобы отделять логи контейнеров от системных
      - replacement: containerlogs
        target_label: job

Labels:

  • Logging_scrape

Это требуется для подключения сервиса к журналам.

Журналы для django_app
Журналы для django_app
Дашборд Caddy
Дашборд Caddy

Ссылка на дашборд: https://grafana.com/grafana/dashboards/22870-caddy/

Метки

Каждому Docker-контейнеру можно назначить метки, которые будут считываться в мониторинге. Это позволяет управлять видимостью на уровне приложения, а не мониторинга.

  services:
  my-django-backend:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: django_app
    logging:
      driver: json-file
    labels:
      prometheus.scrape: "true"         # Сбор метрик (Prometheus)
      prometheus.port: "8000"           # Внутренний порт приложения внутри контейнера
      prometheus.path: "/metrics"       # Необязательно, так как /metrics используется по умолчанию
      logging.scrape: "true"            # Сбор логов (Promtail)
      environment: "production"         # Общий маркер для фильтрации в Grafana
    networks:
      - vps_common_network

Резюме

Общая картина
Общая картина

В результате мы получили систему, в которой:

  • Разработка бизнес-логики ведётся независимо

  • Управление кодом происходит без постоянного подключения к серверу

  • Добавление новой функциональности и SSL-сертификатов для поддоменов не требует больших усилий

Публичная группа с необходимыми репозиториями доступна по ссылке: https://gitlab.com/simple-infrastructure-template

Это шаблон можно переиспользовать и адаптировать под свои задачи.

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

Комментарии (1)


  1. Apalin
    25.08.2026 12:17

    Спасибо, забрал идею с host:2019/metrics и conf.d, у нас Caddyfile до сих пор лежит одним куском.

    Про restart расскажу, как это вышло у нас, потому что в ваших примерах caddy идёт с unless-stopped, а portainer с always.

    На прошлой неделе перезапустили демон docker руками, обычный systemctl restart docker. Сайты отвалились почти сразу. Не все: часть вернулась сама, часть нет, и общего у упавших сперва не находилось. Нашлось в политиках перезапуска. Все упавшие стояли на unless-stopped, все выжившие на always.