Traefik + Portainer + Keycloak

В данной статье мы рассмотрим, как настроить три мощных инструмента для управления вашим сервером: Traefik, Portainer и Keycloak. В итоге вы получите готовый файл docker-compose.yaml, который позволит вам развернуть все эти сервисы на вашем VPS сервере.

Предварительные условия

  1. VPS сервер: Убедитесь, что у вас есть доступ к VPS серверу с установленной операционной системой Linux. Если у вас ещё нет VPS, её можно арендовать у провайдера, например таких как aeza, beget, timeweb.
  2. Docker и Docker Compose: Убедитесь, что Docker и Docker Compose установлены на вашем сервере. Если они не установлены, следуйте инструкциям на официальном сайте Docker для установки.

Сервисы и их предназначение

  • Traefik — это современный HTTP reverse proxy и load balancer, разработанный для автоматизации работы с контейнерами. Он автоматически обнаруживает контейнеры и их настройки, облегчая маршрутизацию и управление трафиком. Он будет доступен по адресу traefik.example.com.
  • Portainer — это удобный веб-интерфейс для управления Docker. С его помощью можно легко создавать, обновлять и удалять контейнеры, а также управлять Docker-образами и сетями. Он будет доступен по адресу portainer.example.com.
  • Keycloak — это сервер управления идентификацией и доступом с открытым исходным кодом. Он предоставляет функции единого входа (SSO), федерации идентификационных данных и управления пользователями. Он будет доступен по адресу auth.example.com.

Файлы docker-compose.yaml и .env

Для настройки данных сервисов на вашем сервере, используйте следующий файл docker-compose.yaml:

docker-compose.yaml
version: "3.8"

volumes:
  letsencrypt:
  portainer_data:
  keycloakdata:
networks:
  intranet:
    name: intranet

services:
  traefik:
    container_name: traefik
    image: "traefik:latest"
    command:
      - "--api.insecure=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entryPoints.ssh.address=:${SHELL_SSH_PORT}"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=intranet"
      - "--log.level=DEBUG"
      - "--certificatesresolvers.leresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.leresolver.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.leresolver.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--metrics.prometheus=true"
      - "--api.dashboard=true"
    ports:
      - "${SHELL_SSH_PORT}:${SHELL_SSH_PORT}"
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "${LETSENCRYPT_DIR}:/letsencrypt"
    networks:
      - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN}`)"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls.certresolver=leresolver"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
      - "traefik.http.routers.traefik.middlewares=forwardauth"

  traefik-forward-auth:
    image: mesosphere/traefik-forward-auth
    container_name: traefik-forward-auth
    restart: on-failure
    depends_on:
      - traefik
      - keycloak
    environment:
      - TZ=${TZ}
      - SECRET=${FORWARD_AUTH_SECRET}
      - PROVIDER_URI=${FORWARD_AUTH_PROVIDER_URI}
      - CLIENT_ID=${FORWARD_AUTH_CLIENT_ID}
      - CLIENT_SECRET=${FORWARD_AUTH_CLIENT_SECRET}
      - ENCRYPTION_KEY=${FORWARD_AUTH_ENCRYPTION_KEY}
      # - COOKIE_DOMAIN=https://emxample.com
      # - DISABLE_SSL_VERIFICATION=true # might be unnecessary
      # - INSECURE_COOKIE=1
      - SCOPE=profile email openid
    networks:
        - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=web"
      - "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
      - "traefik.http.routers.traefik-forward-auth.entrypoints=websecure"
      - "traefik.http.routers.traefik-forward-auth.middlewares=forwardauth"
      - "traefik.http.middlewares.forwardauth.forwardauth.address=http://traefik-forward-auth:4181"
      - "traefik.http.middlewares.forwardauth.forwardauth.authResponseHeaders=X-Forwarded-User"
      - "traefik.http.middlewares.forwardauth.forwardauth.trustForwardHeader=true"

  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    command: -H unix:///var/run/docker.sock
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=Host(`${PORTAINER_DOMAIN}`)"
      - "traefik.http.routers.portainer.entrypoints=websecure"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"
      - "traefik.http.routers.portainer.service=portainer"
      - "traefik.http.routers.portainer.tls.certresolver=leresolver"

  keycloakdb:
    image: postgres:16.2-alpine
    container_name: keycloakdb
    environment:
        - POSTGRES_DB=${KEYCLOAK_DB_NAME}
        - POSTGRES_USER=${KEYCLOAK_DB_USER}
        - POSTGRES_PASSWORD=${KEYCLOAK_DB_PASSWORD}
        - POSTGRES_ROOT_PASSWORD=${KEYCLOAK_DB_ROOT_PASSWORD}
    networks:
      - intranet
    volumes:
      - keycloakdata:/var/lib/postgresql/data
    labels:
      - "traefik.enable=false"

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keycloak
    hostname: keycloak
    environment:
      - KC_HOSTNAME_STRICT=false
      - KC_DB=postgres
      - KC_DB_URL=jdbc:postgresql://keycloakdb/keycloak
      - KC_DB_URL_PORT=5432
      - KC_DB_USERNAME=${KEYCLOAK_DB_USER}
      - KC_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
      - KC_DB_SCHEMA=public
      - KC_LOG_LEVEL=info
      - KC_FEATURES=docker
      - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN}
      - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
      - KC_PROXY=edge
    networks:
      - intranet
    depends_on:
      - traefik
      - keycloakdb
    volumes:
      - ${KEYCLOAK_DIR}/themes:/opt/keycloak/themes
      - ${KEYCLOAK_DIR}z/standalone/deployments:/opt/keycloak/standalone/deployments
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN}`)"
        - "traefik.http.routers.keycloak.entrypoints=websecure"
        - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
    entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"]

volumes:
  letsencrypt:
  portainer_data:
  keycloakdata:
networks:
  intranet:
    name: intranet
version: "3.9"

services:
  traefik:
    container_name: traefik
    image: "traefik:latest"
    command:
      - "--api.insecure=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entryPoints.ssh.address=:${SHELL_SSH_PORT}"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=intranet"
      - "--log.level=DEBUG"
      - "--certificatesresolvers.leresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.leresolver.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.leresolver.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--metrics.prometheus=true"
      - "--api.dashboard=true"
    ports:
      - "${SHELL_SSH_PORT}:${SHELL_SSH_PORT}"
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "${LETSENCRYPT_DIR}:/letsencrypt"
    networks:
      - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.rule=Host(`${TRAEFIK_DOMAIN}`)"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.tls.certresolver=leresolver"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
      - "traefik.http.routers.traefik.middlewares=forwardauth"

  traefik-forward-auth:
    image: mesosphere/traefik-forward-auth
    container_name: traefik-forward-auth
    restart: on-failure
    depends_on:
      - traefik
      - keycloak
    environment:
      - TZ=${TZ}
      - SECRET=${FORWARD_AUTH_SECRET}
      - PROVIDER_URI=${FORWARD_AUTH_PROVIDER_URI}
      - CLIENT_ID=${FORWARD_AUTH_CLIENT_ID}
      - CLIENT_SECRET=${FORWARD_AUTH_CLIENT_SECRET}
      - ENCRYPTION_KEY=${FORWARD_AUTH_ENCRYPTION_KEY}
      # - COOKIE_DOMAIN=https://emxample.com
      # - DISABLE_SSL_VERIFICATION=true # might be unnecessary
      # - INSECURE_COOKIE=1
      - SCOPE=profile email openid
    networks:
        - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=web"
      - "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181"
      - "traefik.http.routers.traefik-forward-auth.entrypoints=websecure"
      - "traefik.http.routers.traefik-forward-auth.middlewares=forwardauth"
      - "traefik.http.middlewares.forwardauth.forwardauth.address=http://traefik-forward-auth:4181"
      - "traefik.http.middlewares.forwardauth.forwardauth.authResponseHeaders=X-Forwarded-User"
      - "traefik.http.middlewares.forwardauth.forwardauth.trustForwardHeader=true"

  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    command: -H unix:///var/run/docker.sock
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    networks:
      - intranet
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer.rule=Host(`${PORTAINER_DOMAIN}`)"
      - "traefik.http.routers.portainer.entrypoints=websecure"
      - "traefik.http.services.portainer.loadbalancer.server.port=9000"
      - "traefik.http.routers.portainer.service=portainer"
      - "traefik.http.routers.portainer.tls.certresolver=leresolver"

  keycloakdb:
    image: postgres:16.2-alpine
    container_name: keycloakdb
    environment:
        - POSTGRES_DB=${KEYCLOAK_DB_NAME}
        - POSTGRES_USER=${KEYCLOAK_DB_USER}
        - POSTGRES_PASSWORD=${KEYCLOAK_DB_PASSWORD}
        - POSTGRES_ROOT_PASSWORD=${KEYCLOAK_DB_ROOT_PASSWORD}
    networks:
      - intranet
    volumes:
      - keycloakdata:/var/lib/postgresql/data
    labels:
      - "traefik.enable=false"

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: keycloak
    hostname: keycloak
    environment:
      - KC_HOSTNAME_STRICT=false
      - KC_DB=postgres
      - KC_DB_URL=jdbc:postgresql://keycloakdb/keycloak
      - KC_DB_URL_PORT=5432
      - KC_DB_USERNAME=${KEYCLOAK_DB_USER}
      - KC_DB_PASSWORD=${KEYCLOAK_DB_PASSWORD}
      - KC_DB_SCHEMA=public
      - KC_LOG_LEVEL=info
      - KC_FEATURES=docker
      - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN}
      - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}
      - KC_PROXY=edge
    networks:
      - intranet
    depends_on:
      - traefik
      - keycloakdb
    volumes:
      - ${KEYCLOAK_DIR}/themes:/opt/keycloak/themes
      - ${KEYCLOAK_DIR}z/standalone/deployments:/opt/keycloak/standalone/deployments
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.keycloak.rule=Host(`${KEYCLOAK_DOMAIN}`)"
        - "traefik.http.routers.keycloak.entrypoints=websecure"
        - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt"
    entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"]

Переменные окружения находятся в файле .env, в нем необходимо изменить значения на свои доменные имена, данные для входа, пароли и т.д.:

.env
TZ="Europe/Moscow"
ACME_EMAIL=mail@example.com
LETSENCRYPT_DIR="./data/letsencrypt"
SHELL_SSH_PORT=2222

TRAEFIK_DOMAIN=traefik.example.com
TRAEFIK_DIR="./data/traefik"

FORWARD_AUTH_SECRET=123456
FORWARD_AUTH_PROVIDER_URI=https://auth.example.com/realms/traefik
FORWARD_AUTH_CLIENT_ID=traefik
FORWARD_AUTH_CLIENT_SECRET=123456
FORWARD_AUTH_ENCRYPTION_KEY=123456

PORTAINER_DOMAIN=portainer.example.com

KEYCLOAK_DOMAIN=auth.example.com
KEYCLOAK_DB_NAME=keycloak
KEYCLOAK_DB_USER=keycloak
KEYCLOAK_DB_PASSWORD=password
KEYCLOAK_DB_ROOT_PASSWORD=password
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=admin
KEYCLOAK_DIR="./data/keycloak"

Пошаговая настройка

  1. Создание сети Docker:

Для того чтобы Traefik мог взаимодействовать с другими контейнерами, создайте внешнюю сеть:

docker network create intranet
  1. Создание docker-compose.yaml и .env файлов

Копируем приведенные выше файлы в директорию проекта и изменяем данные на валидные. Данные с префиксом FORWARD_AUTH пока не трогаем, их нужно добавить после настройки авторизации.

  1. Настройка доменных имен:

    Убедитесь, что у вас настроены DNS записи для доменов traefik.example.com, portainer.example.com и auth.example.com, указывающие на ваш VPS сервер. Рекомендую подождать минут 15 после смены A записи доменов, чтобы настройки dns записей успели примениться.

  2. Запуск Docker Compose:

Перейдите в директорию проекта, где находится файл docker-compose.yaml и выполните команду:

docker-compose up -d

На данном этапе должны уже успешно работать домены portainer.example.com и auth.example.com.

Перейдем к настройке авторизации.

Настройка авторизации через keycloak

Для создания нового пользовательского Realm, пользователя и клиента в Keycloak, следуйте пошаговой инструкции, основанной на официальной документации Keycloak.

Запуск Keycloak с использованием Docker Compose

Убедитесь, что ваш Keycloak контейнер запущен с использованием Docker Compose, как было описано ранее. Введите команду для запуска контейнера, если он еще не запущен:

docker-compose up -d

Доступ к Keycloak Admin Console

  1. Откройте браузер и перейдите на URL вашего Keycloak, например: http://auth.example.com.
  2. Войдите в административную консоль, используя учетные данные администратора, которые вы задали в файле .env.

Создание нового пользовательского Realm

  1. После входа в административную консоль Keycloak, в верхнем левом углу выберите текущий Realm (обычно это "Master").
  2. Нажмите на "Add Realm" (Добавить Realm) в выпадающем меню.
  3. Заполните форму:
    • Name: Введите имя для нового Realm, например, myrealm.
  4. Нажмите "Create" (Создать).

Создание нового пользователя в Realm

  1. Перейдите в только что созданный Realm, выбрав его в верхнем левом углу.
  2. В левом меню выберите "Users" (Пользователи), затем нажмите "Add user" (Добавить пользователя).
  3. Заполните форму:
    • Username: Введите имя пользователя, например, newuser.
    • Остальные поля можно оставить пустыми или заполнить по необходимости.
  4. Нажмите "Save" (Сохранить).
  5. После создания пользователя вы будете перенаправлены на страницу профиля пользователя. Перейдите на вкладку "Credentials" (Учетные данные).
  6. Установите пароль для пользователя:
    • Password: Введите пароль.
    • Password confirmation: Подтвердите пароль.
    • Снимите флажок "Temporary" (Временный), если вы хотите, чтобы пароль не требовал смены при первом входе.
  7. Нажмите "Set Password" (Установить пароль).

Добавление нового клиента

  1. В левом меню выберите "Clients" (Клиенты), затем нажмите "Create" (Создать).
  2. Заполните форму:
    • Client ID: Введите идентификатор клиента, например, myclient.
    • Client Protocol: Выберите протокол (например, openid-connect).
  3. Нажмите "Save" (Сохранить).
  4. На странице настроек клиента заполните следующие поля:
    • Valid Redirect URIs: Введите URL вашего приложения, сейчас авторизацию использует только traefik.example.com так что добавляем его, https://traefik.example.com/*.
    • Web origins: Вводим домены для которых разрешаются CORS запросы. https://traefik.example.com
    • Остальные поля можно настроить по вашему усмотрению.
  5. Нажмите "Save" (Сохранить).

После этого заполняем переменные в файле .env с префиксом FORWARD_AUTH.

Перезапускаем контейнеры:

docker-compose down
docker-compose up -d

Заходим на сайт traefik.example.com и проверяем что он доступен только после ввода логин пароля.

Проверка работы сервисов

После успешного запуска контейнеров, откройте браузер и перейдите по следующим адресам для проверки:

  • Traefik: http://traefik.example.com
  • Portainer: http://portainer.example.com
  • Keycloak: http://auth.example.com

Теперь рассмотрим пример, как добавить docker-compose конфигурацию через Portainer, сайт которой будет открываться только после авторизации через keycloak.

Добавлению Stack в Portainer

Для примера развернем сервис dufs - файловый менеджер доступный через веб-сервер, облегчающий доступ и управлению файлами через HTTP.

Для этого будем использовать следующую конфигурацию:

version: "3.8"

networks:
  intranet:
    external: true

services:
  dufs:
    image: sigoden/dufs
    container_name: dufs
    volumes:
      - ${DATA_PATH}:/data
    restart: unless-stopped
    command: /data -A
    env_file: stack.env
    extra_hosts:
      host.docker.internal: host-gateway
    networks:
      - intranet
    environment:
      USER_UID: 1000
      USER_GID: 1002
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.files.rule=Host(`files.example.com`)" # Домен по которому будет доступен сайт
      - "traefik.http.services.files.loadbalancer.server.port=5000" # Порт на котором отдаётся веб сайт
      - "traefik.http.routers.files.entrypoints=websecure"
      - "traefik.http.routers.files.tls.certresolver=leresolver"
      - "traefik.http.routers.files.middlewares=forwardauth" # Удалить если не нужно защитить доступ к сайту через авторизацию

Откройте браузер и перейдите на URL вашего Portainer, например: http://portainer.yourdomain.com.

Войдите в систему с использованием ваших учетных данных администратора.

Добавление нового Stack

  1. На главной странице Portainer выберите вашу Docker окружение (например, локальный Docker).
  2. В левом меню выберите "Stacks".
  3. Нажмите на кнопку "Add stack" (Добавить стек).

Конфигурация Stack

  • В поле "Name" введите имя для вашего Stack, например, dufs.
  • В поле "Web editor" вставьте Docker Compose конфигурацию, приведенную выше.
  • В нижней части страницы вы увидите опцию "Environmental variables" (Переменные окружения). Нажмите на "Add environment variable" и добавьте переменную DATA_PATH со значением пути к директории в которой будут лежать файлы доступные через данный сервис.

Развертывание Stack

  1. Нажмите "Deploy the stack" (Развернуть стек).
  2. Portainer начнет развертывание Stack на вашем Docker окружении. Вы сможете отслеживать статус развертывания на странице Stack.

Проверка развертывания

После успешного развертывания Stack, откройте браузер и перейдите по адресу http://files.example.com, чтобы убедиться, что сервис dufs успешно запущен и доступен через Traefik, а также, что он доступен только после авторизации.