Всем привет! Сегодня я вам расскажу про то, как я настроил безопасное шифрование данных для сайта с помощью протокола HTTPS и SSL/TLS сертификата.

⚡️Подготовка

Представим что у вас есть свой сайт запущенный на VPS на голом HTTP, также вы уже купили домен и связали его со своим IP-адресом. 

Но спустя время вы решили, что не хотите, чтоб весь трафик между клиентами и сервером был виден любому желающему. 

Вспомнили, что есть протокол HTTPS, который по сути обычный HTTP, но на 5 и 6 уровне работает протокол SSL/TLS, который занимается шифрованием и аутентификацией.

Дело осталось за малым, перевести сайт с HTTP на HTTPS.

❓Что такое обратный прокси и зачем он нам?

Обратный прокси — это сетевой шлюз, который полностью перехватывает входящие соединения от клиентов, берет на себя всю работу с протоколами (TCP, TLS, HTTP) и от своего имени отправляет новые, чистые запросы на целевые серверы (бэкенды).

Обратный прокси сервер
Обратный прокси сервер

Для внешнего мира обратный прокси — это и есть сам сайт. Клиент думает, что общается с бэкендом, но на самом деле он общается только с прокси.

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

В VPN мы просим не просто отправить данные, а инкапсулируем и зашифровываем весь наш пакет, который VPN сервер расшифрует, подменит наш IP и отправит уже в интернет, при получении ответного пакета, при помощи сокета понимает какому клиенту нужно отправить пакет.

Я решил использовать обратный прокси caddy, он по размеру меньше, чем nginx (если мы хотим, чтобы он тоже выпускал сертификаты) и умеет автоматически выпускать сертификаты Let’s encrypt.

Для его работы нужно создать Caddyfile:

Ваш домен {
    reverse_proxy app:9000 {
        header_up X-Forwarded-Proto {scheme}
    }
    encode gzip
}

У меня он максимально простой, так как всего один сайт. Тут мы пишем, что при обращении пользователя на такой домен нужно отдать данные контейнеру app, работающему на 9000 порте.

encode gzip помогает ускорить передачу статических файлов.

Обновим или создадим новый docker-compose.yml

Раньше у меня просто в интернет был проброшен порт из контейнера, на котором работает сайт.

Теперь никто не должен видеть, как у нас устроено внутри. Все запросы пользователей вначале будет встречать обратный прокси caddy. И уже он будет передавать их нужному сервису, запущенному у нас.

Это позволяет не только скрыть внутрянку, но и запускать несколько сервисов на одном порте. Точнее все они будут на разных, но для пользователей будет казаться, что все на 443, например.

Я создал такой новый docker-compose-https.yml:

services:
  db:
    image: postgres:17
    container_name: db
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d db"]
      interval: 10s
      timeout: 30s
      retries: 5

  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app
    restart: always
    depends_on:
      - db
    env_file:
      - .env
    expose:
      - "9000"
    volumes:
      - ./app:/code/app
    command: uvicorn main:app --host 0.0.0.0 --port 9000 --reload

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - app

volumes:
  postgres_data:
  caddy_data:
  caddy_config:

Не забудьте поменять пользователя и пароль, а также используйте .env

Теперь сайт работает в контейнере на 9000 порту, прокидывать его наружу не нужно, а контейнеры итак связанны в общую сеть.

expose 9000 не открывает порт наружу, а используется для документации и позволяет указать, какие порты контейнер использует для связи.

Для запуска контейнеров используем теперь эту команду (при первом запуске нужно добавить флаг в конце --build):

sudo docker compose -f docker-compose-https.yml up -d

В чем заключается проблема?

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

Однако, FastAPI общается с Caddy по HTTP и не знает, что клиент теперь использует HTTPS, из-за этого он будет генерировать ссылки со схемой http://

Из-за этого наши перенаправления, ссылки и тд, где мы использовали, например, функцию url_for будут ломаться, создавать бесконечные редиректы, так как браузер ждет ответ только со схемой https://

Тут есть два решения: убрать везде эти функции и писать самому явно схему (но не всегда это можно сделать), либо добавить специальный заголовок в HTTP, при получении пакета обратным прокси сервером.

Разберем второе решение.

⚡️Добавим заголовок во все HTTPS запросы

Ваш Caddyfile должен выглядеть следующим образом:

Ваш домен {
    reverse_proxy app:9000 {
        header_up X-Forwarded-Proto {scheme}
    }
    encode gzip
}

Заголовок header_up X-Forwarded-Proto {scheme} это тот самый нужный нам заголовок.

X-Forwarded Proto это стандарт, который обозначает дословно перенаправленный протокол, а {scheme} является динамической переменной, которая может быть и http, и https.

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

А без неё не получится тестировать работу у себя на компьютере при разработке по http.

Но этого недостаточно, нужно прописать, чтобы бэкэнд читал этот заголовок.

Добавим middleware функцию

Функция middleware — это промежуточная функция, которая перехватывает все http запросы к нашему приложению.

@app.middleware("http")
async def fix_https_urls(request, call_next):
    if request.headers.get("x-forwarded-proto") == "https":
        request.scope["scheme"] = "https"
    response = await call_next(request)
    return response

Тут мы вначале проверяем у перехваченного запроса заголовок и если он есть и его значение https, то мы сами меняем схему http от обратного прокси на https.

FastAPI теперь будет думать, что общается по https и генерировать ссылки с такой схемой.

А асинхронный вызов функции await call_next(request) передаёт управление и request остальным функциям нашего приложения.

После того, как какая-то ручка выполнила свои действия, то мы получаем response и отдаем его клиенту.

Также, если вы используете куки, то поставьте флаг secure=True, но в локальной разработке отключаем.

?На этом всё, теперь мы смогли настроить шифрование канала при помощи HTTPS.


Если вам нужно сверстать сайт, собрать Telegram-бота или что-то иное — пишите мне напрямую в личку: @gesesofficiall, обсудим задачу.

Если вам интересна тема сетей, веб-разработки, криптографии, программирования или ИИ — подписывайтесь на мой канал @pxi_ru, там я публикую свои посты и готовые проекты.

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


  1. ky0
    08.08.2026 12:27

    Захватывающая история.


    1. PXI Автор
      08.08.2026 12:27

      Надеюсь это в хорошем смысле)


  1. maikuss
    08.08.2026 12:27

    Ну, допустим, надо, прямо очень надо как-то подтолкнуть свой тг канал, заодно немножко рекламки в публичное пространство вбросить, ок. Бывает. Почему бы тогда не написать статью-паровоз, но написать её самому и на интересную тему, достойную здешнего сообщества? Жвачка, многажды пережёванная, всё равно не вызовет у людей желания познакомиться поближе с тем, чего там автор ещё нажевал.


    1. PXI Автор
      08.08.2026 12:27

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

      Я это написал с нуля сам, не своровал нигде, показал просто то, что я реально сделал. Разве это плохо и никому не нужно?


      1. blik13
        08.08.2026 12:27

        А почему тут считается

        Потому что так считается.


    1. PXI Автор
      08.08.2026 12:27

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

      Изначально я хотел в раздел постов её отправить, так как понимал, что на статью не особо тянет. Но там ограничение 4000 символов всего


  1. korn3r
    08.08.2026 12:27

    А зачем в X-Forwarded-Proto что-то кроме https? Логично на кадди делать 301 на https и "header_up X-Forwarded-Proto https", разве нет? Или вы http тоже пересылаете?

    Ну и наверно стоит еще X-Forwarded-Port слать хотя бы.


    1. PXI Автор
      08.08.2026 12:27

      В локальной разработке я просто поднимаю два контейнера без обратного прокси, там уже нет https

      И этот заголовок не мешает мне работать


      1. korn3r
        08.08.2026 12:27

        так у вас и заголовка нет. если нет реверс-прокси


        1. PXI Автор
          08.08.2026 12:27

          Извините, не так вас понял, я подумал вы про Питоновскую часть.

          В Caddyfile в моем случае и правда можно поставить просто https, но обычно ставят сразу ставят сразу готовый заголовок из входящего запроса через плейсхолдер: {http.request.scheme} или {scheme}

          Это, например, если бы я не перенаправлял автоматически на 443 с 80, а поддерживал сразу оба


          1. korn3r
            08.08.2026 12:27

            "Обычно" все таки перенаправляют :)

            А вы еще и именно переменную воткнули вместо стокового https, о чем сами же и пишете:

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

            Т.е. на выходе получается лишний код в конфиге чтобы можно было делать неправильно (=пересылать там и http)


            1. PXI Автор
              08.08.2026 12:27

              Да вы правы, но пересылать http требуется и в других специфических задачах, просто я с ними на практике сам не сталкивался


  1. baldr
    08.08.2026 12:27

    Ну и сколько токенов вы зря сожгли на эту "инструкцию"?

    expose 9000 не открывает порт наружу

    Открывает.

    ports: - "127.0.0.1:5432:5432"

    Есть ли хоть одна важная причина такое делать? Учитывая, что пароль к базе прямо в compose-файле и лежит..

    Однако, FastAPI общается с Caddy по HTTP и не знает, что клиент теперь использует HTTPS, из-за этого он будет генерировать ссылки со схемой http://

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

    Казалось бы, в официальной инструкции написано про флаги `--proxy-headers --forwarded-allow-ips='*'` ,но у нас свой путь?

    Зачем-то скомканный абзац про VPN, совершенно не в тему.


    1. PXI Автор
      08.08.2026 12:27

      То есть вы хотите сказать, что если я напишу expose 9000, то смогу обратиться к контейнеру из интернета по публичному айпи?

      Учитывая, что пароль к базе прямо в compose-файле и лежит, только я ниже написал, что не забудьте использовать .env, но хорошо я сам должен был это прописать, тут с вами согласен


      1. baldr
        08.08.2026 12:27

        То есть вы хотите сказать, что если я напишу expose 9000, то смогу обратиться к контейнеру из интернета по публичному айпи?

        Да, именно так. Сможете. "expose 9000" означает - слушать порт 9000 по всем интерфейсам (0.0.0.0). Про файрволлы вы ничего не написали, а многие хостинги такими сложностями не заморачиваются. Для базы вы биндите порт на 127.0.0.1, что имеет хоть какой-то смысл (для локального дебага), но тоже совсем небезопасно и, вообще говоря, не нужно.

        Контейнеры внутри одного compose-энваромента видят друг друга по именам и порт базы вообще не нужно выставлять. Про .env вы написали для приложения, а пароль для базы у вас в compose файле.

        Учитывая, что compose-файл обычно хранится в репозитории - у вас пароль будет приходить дефолтный.

        У меня две статьи тут написаны по поводу конфигурации энваромента и более безопасной инициализации секретов.


        1. korn3r
          08.08.2026 12:27

          бля бд вообще можно network_mode: none и цепляться к ней через unix socket :)


          1. baldr
            08.08.2026 12:27

            Ну тут спорный вопрос что лучше - шарить файл сокета в volume между несколькими контейнерами (явно ставить права и вспоминать что будет как себя вести, если каждый из контейнеров рестартует) или просто довериться файрволлу докера, который он автоматически настроит.


            1. korn3r
              08.08.2026 12:27

              бд вроде бы свои сокеты умеет перезатирать (в отличие от того же нгинкса).

              у меня tmpfs volume сделаны для этого и вроде бы норм, правда контейнеры я по отдельности не часто перезапускаю.

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


        1. lonelyleob
          08.08.2026 12:27

          Разработчики Докера с вами не согласны

          expose позволяет объявлять порты только между контейнерами во внутренних сетях

          Чаще всего expose-порты используются для автодискавери в traefik

          https://docs.docker.com/reference/compose-file/services/#expose


          1. PXI Автор
            08.08.2026 12:27

            Спасибо за ваш комментарий


          1. baldr
            08.08.2026 12:27

            Да, согласен, поторопился и перепутал с ports, а потом уже отредактировать комментарий нельзя было. Выше в коде была секция с ports для базы (уже отредактировано, похоже), я сам expose не использую, поэтому спутал.

            expose порт не открывает, верно, ещё раз извиняюсь.


            1. PXI Автор
              08.08.2026 12:27

              Я очень рад, что мы смогли придти к общему знаменателю


  1. baldr
    08.08.2026 12:27

    У автора этой статьи в профиле ссылка на свой сайт: http://x.x.x.x (да, IP-адрес).

    facepalm.jpg.png

    PS. я не придираюсь из-за того что хочу выглядеть умным. Моя главная претензия - человек пишет мануал по безопасности, сам не до конца разобравшись в теме.


    1. PXI Автор
      08.08.2026 12:27

      Даже если бы там был домен, это не мешало бы узнать айпи


      1. baldr
        08.08.2026 12:27

        Если бы там был домен - вы бы могли выпустить сертификат и использовать TLS. Именно это я имею в виду, особенно в контексте вашей статьи.


        1. pae174
          08.08.2026 12:27

          Может быть он джва года ждал возможности выпустить сертификат TLS для IP адреса :-)


          1. PXI Автор
            08.08.2026 12:27

            Сертификат на айпи адрес мне, к сожалению, пока не получится поставить. Но об этой возможности я знаю


        1. PXI Автор
          08.08.2026 12:27

          И что вы хотите этим сказать? Я сознательно не покупаю домен, так как под мои нужды он мне сейчас не нужен. Зачем тратить деньги на то, что не нужно?


          1. baldr
            08.08.2026 12:27

            Да пожалуйста, конечно, ваша воля. Вот только писать про "информационную безопасность" в XXI веке на сайте с http без tls - немного непоследовательно.

            Ну а уж "разрабатывать мессенджер" и предлагать к нему подключиться (создать пользователя и ввести пароль?) на таком сайте - вообще смахивает на фишинг.


            1. PXI Автор
              08.08.2026 12:27

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

              Во-вторых, про мессенджер, вам любой браузер пишет про небезопасное соединение при вводе данных, а также в самом мессенджере снизу постоянно написано: отсутствует шифрование, отправка сообщений на ваш страх и риск

              Ну и самое главное, человек не должен строить себе во дворе небоскреб, чтобы он мог рассказать как его построить и построить его, как я настроил HTTPS другому человеку, при этом не настроил себе


              1. baldr
                08.08.2026 12:27

                человек не должен строить себе во дворе небоскреб, чтобы он мог рассказать как его построить

                Любопытная точка зрения. Вот я не построил ни одного небоскрёба - я уже могу начинать всех учить? Ну типа кирпич и цемент - чё там сложного, правда?

                В общем-то снимается большинство вопросов к вам, если вы действительно так думаете - всё понятно. Только не обижайтесь на минусы к статье.


                1. PXI Автор
                  08.08.2026 12:27

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


                  1. baldr
                    08.08.2026 12:27

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

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


  1. JBFW
    08.08.2026 12:27

    Минусов, конечно, понаставили, но вспомнилась история с приложением на nodejs, в которое автор вкрячил https, сертификаты и их обновление (прямо в код), вместо того чтобы просто отдать возню с ними nginxу или вот caddy

    А это как раз то, о чем в статье. Подозреваю, что разработчик просто не знал что так можно.


  1. kvazimoda24
    08.08.2026 12:27

    Caddy потребляет меньше ресурсов, чем Nginx? А можно пруфы?

    ACME завезли и в Nginx.


    1. PXI Автор
      08.08.2026 12:27

      Да, неправильно высказался, Caddy имеет меньший размер, по сравнению с nginx, если мы хотим, чтобы он тоже автоматически продлевал сертификат


    1. PXI Автор
      08.08.2026 12:27

      Но спасибо, что обратили внимание


  1. baznikin
    08.08.2026 12:27

    Как я сделал яишенку на завтрак и посолил её


  1. CodeByZen
    08.08.2026 12:27

    Это 100% нейрослоп, автор даже отвечает через нейронку на комментарии.


    1. PXI Автор
      08.08.2026 12:27

      Побольше бы таких Ии, а то таких как ты уже слишком много на этой земле обетованной


  1. austnv
    08.08.2026 12:27

    актуальная тема, свежие идеи, качественная реализация, без рекламы!

    лови минус в карму


    1. PXI Автор
      08.08.2026 12:27

      Ох! нет, в самое сердце, мистер, зачем вы так со мной?