Создание общедоступного URL в сети интернет к вашему локальному проекту
Что такое Ngrok, наверное знает каждый разработчик web приложений, и многие им пользуются.
Немного предыстории...
Присоединившись к новому большому проекту, над которым работают десятки разработчиков и QA специалистов, я столкнулся с тем, что разработка ведется удаленно на специально выделенных серверах.
А т.к. я уже несколько лет разработку приложений веду исключительно в докере, я никак не мог адаптироваться к текущему подходу.
Думаю все, кто более менее освоил работу в докере, уже не мыслят как можно разрабатывать без него. Причин на это много. Конечно, как и везде у докера есть своя цена и это тоже уже много раз обсуждалось.
Итак, новый проект не похож на предыдущие. Он имеет много зависимостей с другими сервисами, как внутренними так и внешними.
Большое количество внешних интерграций порождало проблему связи локального приложения с внешним миром. И если объединить внутренние сервисы используя docker netwokr не вызывало каких либо проблем, то необходимость связать внешний сервис уже требовал дополнительных инструментов.
Интеграции платежных систем всегда подразумевает, что будут callback (notification).
При такой необходимости, часто выбирают Ngrok. Хорошее решение, но в удобном варианте - платное. Особенно это ощущается, когда разработчиков много.
Т.к. Ngrok не подходил, первое что пришло в голову, создать виртуалку, на нее завести домен, и создавать ssh туннель с ним.
docker-compose.yml
version: '3.7'
services:
callback-tunnel:
build:
context: ./.docker/ssh-tunnel-callback
restart: unless-stopped
volumes:
- ~/.ssh:/root/ssh:ro
environment:
TUNNEL_HOST: ${CALLBACK_TUNNEL_HOST}
LOCAL_PORT: ${CALLBACK_TUNNEL_LOCAL_PORT}
LOCAL_HOST: ${CALLBACK_TUNNEL_LOCAL_HOST}
REMOTE_PORT: ${CALLBACK_TUNNEL_REMOTE_PORT}
networks:
- local
app:
image: php
restart: unless-stopped
tty: true
volumes:
- ./:/var/www/html
где Dockerfile callback-tunnel выглядел вот так
Dockerfile
FROM alpine
RUN apk add --update openssh-client && rm -rf /var/cache/apk/*
CMD rm -rf /root/.ssh && mkdir /root/.ssh && cp -R /root/ssh/* /root/.ssh/ && chmod -R 600 /root/.ssh/* && \
ssh \
-vv \
-o StrictHostKeyChecking=no \
-N $TUNNEL_HOST \
-R *:$REMOTE_PORT:$LOCAL_HOST:$LOCAL_PORT \
&& while true; do sleep 30; done;
Данный подход решал проброс callback запросов от внешних провайдеров, но он был не надежный. Иногда при жестком разрыве соединения, порт на виртуалке оставался занятым. При совместной разработки необходимо было договариваться, кто какой порт будет использовать. Короче, решение такое себе, костыль.
Дальше - больше, при локальной разработке, бывает необходимость проверить приложение на другом устройстве, например на телефоне или планшете, или в определенной версии браузера поведение фронта не адекватное. И снова мысли об Ngrok.
Пришлось искать.
В какой то момент мне попался список бесплатных инструментов решающих данную проблему. Сам список уже не найду, но один инструмент меня сильно заинтересовал, и я решил попробовать.
Fast Reverse Proxy
https://github.com/fatedier/frp
Сервер написан на Go
JS, Vue для Dashboard
Схема
Репозиторий с примерами https://github.com/anydasa/frp-example
Как поднимал
Подготовил на github репозиторий
Купил домен
Создал дроплет на Digital Ocean (5$ docker)
Установил туда nginx
Установил letsencript и создал Wildcard SSL Certificate по этой доке. Wildcard нужен для того чтоб public URL были https
Настроил nginx, он выступает как первый proxy server. Можно обойтись без него, но мне так было проще
Склонил https://github.com/anydasa/frp-example и запустил server-ную часть
-
Локально,
Создал .env из .env-example и прописал необходимые переменные
запустил client-ский docker-compose
В зависимости от того какой указал REVERSE_PROXY_PERSONAL_ALIAS, будет мой URL.
В моем примере есть 3 хоста (обычно нужно для проекта), и в зависимости какой PERSONAL_ALIAS указан, будут доступны по URLs. К примеру PERSONAL_ALIAS=project, тогда
frontend - https://project.frp.example.com
backend - https://admin-project.frp.example.com
api - https://api-project.frp.example.com
Т.к. Wildcard SSL настраивается на *.frp.example.com, то все поддомены нужно указывать без точек.
Server Nginx
server {
listen 443 ssl;
server_name dashboard.frp.example.com;
ssl_certificate /etc/letsencrypt/live/frp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/frp.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7500/;
proxy_set_header host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-forward-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_redirect off;
}
}
server {
listen 443 ssl;
server_name ~^.+\.frp\.example\.com$;
ssl_certificate /etc/letsencrypt/live/frp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/frp.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:7000/;
proxy_set_header host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-forward-for $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_redirect off;
}
}
server {
listen 80;
server_name ~^.+\.frp\.example\.com$;
return 301 https://$host$request_uri;
}
Server FRP Dockerfile
FROM alpine:3
MAINTAINER Sykchin Artem
ENV FRP_VERSION=0.37.1
ENV FRP_URL=https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_amd64.tar.gz
WORKDIR /opt/frp
ADD ${FRP_URL} /tmp/frp.tar.gz
RUN tar --strip 1 -xvzf /tmp/frp.tar.gz -C /opt/frp && rm /tmp/frp.tar.gz
ADD frps.ini /opt/frp
ADD 404.html /opt/frp
ADD entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
Client docker-compose.yml
version: '3.7'
services:
proxy:
build: docker/proxy
depends_on:
- webserver
environment:
PERSONAL_ALIAS: ${REVERSE_PROXY_PERSONAL_ALIAS}
SERVER_HOST: ${REVERSE_PROXY_SERVER_HOST}
SERVER_TOKEN: ${REVERSE_PROXY_SERVER_TOKEN}
SERVER_PORT: ${REVERSE_PROXY_SERVER_PORT}
webserver:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./docker/nginx/templates:/etc/nginx/templates
depends_on:
- app
app:
image: php:8-fpm-alpine
restart: unless-stopped
volumes:
- ./src:/var/www/html
настройки для client proxy (proxy это контейнер)
FRP Client.ini
[common]
server_addr = {SERVER_HOST}
server_port = {SERVER_PORT}
token = {SERVER_TOKEN}
login_fail_exit = true
[frontend-{PERSONAL_ALIAS}]
type = http
local_ip = webserver
local_port = 8081
subdomain = {PERSONAL_ALIAS}
[admin-{PERSONAL_ALIAS}]
type = http
local_ip = webserver
local_port = 8082
subdomain = admin-{PERSONAL_ALIAS}
[api-{PERSONAL_ALIAS}]
type = http
local_ip = webserver
local_port = 8083
subdomain = api-{PERSONAL_ALIAS}
Выполняя docker-compose up, вы поднимаете проект + proxy client и связываете все вместе. Соответственно, выполняя docker-compose down вы "тушите" проект вместе с проксированием
Комментарии (28)
Imbecile
16.10.2021 22:34Не совсем понятны дополнительные телодвижения. Почему просто не настроить nginx, как reverse proxy, без frp?
anydasa Автор
16.10.2021 23:27+1Как вы собрались проксировать запрос с nginx в локальную сеть?
Imbecile
16.10.2021 23:33Если сервер с nginx не в локальной сети, то всегда можно поднять VPN.
anydasa Автор
16.10.2021 23:39+2Ну опишите, что для этого нужно сделать. По шагам.
beatleboy
17.10.2021 02:40+2Делается это так, поднимаем VPN на удаленном хосте, выдаем себе статический локальный адрес например 192.168.42.55, (прописываем его на своем компе в настройках VPN подключения).
Далее настраиваем на VPN сервере NGINX, прикручиваем сертификат от LE (через certbot например), и добавляем server на нужный домен с proxy_pass на свой статический локальный адрес http://192.168.42.55.
Схема достаточно простая, работает шустро и безотказно.
Для работы reverse-proxy достаточно подключиться к VPNanydasa Автор
17.10.2021 08:08-1Это уже как минимум не проще.
А добавьте сюда вопросы менеджмента роутинга. Например появился новый дев, и теперь ему нужно тоже проксировать, добавлять правила в nginx?
И дальше, как локально роутить трафик по контейнерам? У нас докер, одновременно может работать несколько nginx
Да, технически это возможно. Но это совсем не проще. ИМХО.
Но если быть откровенным, такая мысль не приходила, возможно ее так же можно до ума довести. Спасибо
Smashrock
17.10.2021 16:43+1Тоже не совсем понимаю, почему это сложно. На VPS ставите nginx в режиме reverse proxy и на том же сервере ставите wireguard. А потом просто создаёте wireguard приватную сеть со всей плеядой серверов, а nginx reverse proxy на VPS пусть выбирает, куда именно и что проксировать. Ну там запрос к example.com пустит на сервер 10.0.0.1, а к api.example.com пустит на сервер 10.0.0.2.
Конечно же не забываем про плохих людей и защищаем VPS сервер и nginx на нём от атак, ботов и прочей нечести.anydasa Автор
17.10.2021 18:08Возможно я не раскрыл мысль.
Допустим у вас есть сервер, на нем Nginx + VPN. У вас два разработчика в команде. Вы решили, что
*.dev1.example.com будет проксировать запрос на 10.0.0.1
*.dev2.example.com будет проксировать запрос на 10.0.0.2
Когда придет новый дев, нужно будет в конфиге Nginx правки вносить. Не проблема, но зачем? если с FRP управление какой поддомен куда вести будет уходит на плечи клиента.
Хорошо, будем руками править на reverse nginx, но дальше, все запросы для dev1 идут на 10.0.0.1, у меня развернуто 2 проекта, допустим:
site, admin-site, static-site, api-site - это 1 docker-compose
site2, admin-site2, static-site2, api-site2 - это 2 docker-compose
теперь стоит задача, создать маршруты локально,
site.dev1.example.com -> 10.0.0.1 -> docker container site
site-admin.dev1.example.com -> 10.0.0.1 -> docker container site-admin
....
когда разработка идет в докере, обычно это делаю так
version: '3.7' services: webserver: image: nginx:alpine depends_on: - app app: image: php:8-fpm-alpine
Вы будете на клиенте в не докера еще 1 nginx ставить который будет роутить запросы? или на сервере будете указывать порты? которые откроете в докере?
И снова.... технически решаемо, но это ад становится с поддержкой.
С FRP всего этого геморроя нет.
Ngrok, кстати, на такой услуге зарабатывает, FRP 40т звезд на гитхабе, это как минимум намек на то что инструмент востребованный.
Могу предположить что вы ближе к администрированию, тогда вам возможно сложно понять понять взгляд программиста.
Smashrock
17.10.2021 21:16Смотрите, я не пытаюсь на вас напасть, поэтому давайте не будем утверждать, что кому-то сложно будет кого-то понять - обмен знаниями и опытом есть основа здорового обучения.
Я заинтересовался в этом способе и хочу для себя понять, он лучше используемого мной или нет, где слабые места и преимущества, безопасен он или нет.
Из-за чего столько вопросов - схема в статье легко заменяется на связку nginx + VPN + nginx, при условии, что мы откроем порты в контейнерах на серверах (тогда как на схеме nginx + frp + frp + возможно последний nginx в контейнере у вас сидит). Даже вот статья есть о том, что связка nginx+VPN отлично заменяет пресловутый ngrok. Может админская панель FRP помогает быстро всё настраивать - хз, на гитхабе нормально не описано это, или я не могу увидеть никак.
Будет круто, если сможете начертить сложную схему, которая будет возможна с минимальными затратами времени с помощью FRP и трудозатратна для связки nginx + VPN + nginx.anydasa Автор
17.10.2021 21:49+1Смотрите...
proxy frp находится в связке контейнеров. Поднимая проект (docker-compose up) поднимаешь все сразу: тунель, nginx, php, sql. после того как поработал, сделал docker-compose down и убил и прокси и nginx и тд.
ненужно править nginx серверный, в той статье что вы поделились, проксирование на 10.99.0.2:8443; это совсем не гибко, пока ты один и у тебя всегда проект будет на 8443 то пойдет, но так не бывает, в моей реальности :)
открывать порты nginx (docker) ненужно, все проксируется внутри сети докер-проекта. Т.е. вообще нет открытых портов
я не писал, но у FRP можно создать любой тунель по tcp, ssh, + куча плагинов есть
самое главное, настройка не на сервере происходит, а на клиенте. На клиенте указываешь какой хост юзаешь, в какой контейнер проксируешь, на какой порт, какой тип проксирования.
В статье не писал, все это в репе есть, настройка на клиенте (внутри докер контейнера) proxy
client.ini
[common] server_addr = {SERVER_HOST} server_port = {SERVER_PORT} token = {SERVER_TOKEN} login_fail_exit = true [frontend-{PERSONAL_ALIAS}] type = http local_ip = webserver local_port = 8081 subdomain = {PERSONAL_ALIAS} [admin-{PERSONAL_ALIAS}] type = http local_ip = webserver local_port = 8082 subdomain = admin-{PERSONAL_ALIAS} [api-{PERSONAL_ALIAS}] type = http local_ip = webserver local_port = 8083 subdomain = api-{PERSONAL_ALIAS}
и dockre-compose воображаемого проекта, в реальный проект перенести нужно только блок proxy и подсунуть ему нужные конфиги, см. выше
Клиентский docker-compose.yml
version: '3.7' services: proxy: build: docker/proxy depends_on: - webserver environment: PERSONAL_ALIAS: ${REVERSE_PROXY_PERSONAL_ALIAS} SERVER_HOST: ${REVERSE_PROXY_SERVER_HOST} SERVER_TOKEN: ${REVERSE_PROXY_SERVER_TOKEN} SERVER_PORT: ${REVERSE_PROXY_SERVER_PORT} webserver: image: nginx:alpine restart: unless-stopped volumes: - ./docker/nginx/templates:/etc/nginx/templates depends_on: - app app: image: php:8-fpm-alpine restart: unless-stopped volumes: - ./src:/var/www/html
anydasa Автор
17.10.2021 22:20да, писатель из меня еще тот)
да и не заметил что в 1 спойлер засунул два других, только сейчас понял, поправил. + дописал этот момент
inkvizitor68sl
18.10.2021 13:24*.dev1.example.com будет проксировать запрос на 10.0.0.1
Nginx может проксировать *.devX.example.com в 10.0.0.X регулярками.
VPN может выдавать статические адреса, можно взять pritunl, чтобы ничего не настраивать руками.
Тот же VPN пушем маршрутов решает проблему с доступом к другим сервисам-зависимостям и партнёрам.
anydasa Автор
18.10.2021 14:47да, вариант, мне правда не нравится, т.к. он не так красиво ложится на связку с докером, + "числовые" домены тоже не очень понятны.
я так понял, вы предлагаете такую схему
{PORT}.dev{X}.example.com -> 10.0.0.X:PORT
какой проект, какой дев, не ясно.
+ тут с wildcard сертификатами сложнее становитсямне лучше понимать когда такой тип домена
cms-vasiliy.example.com
api-cms-vasily.example.com
btyshkevich
18.10.2021 14:31Запустить tailscale на сервере и у себя на компе. Авторизоваться своим гугловым эккаунтом. Всё.
tabtre
16.10.2021 23:31Данный подход решал проброс callback запросов от внешних провайдеров
А что за «callback запросы» вы имели ввиду?anydasa Автор
16.10.2021 23:34Когда запрос, к примеру от банка, прилетает к вам. Которым он уведомляет, что транзакция которую вы создали ранее, завершена успешно (или нет)
ModestONE
17.10.2021 19:44Имхо, за докером и/или подобными решениями будущее. Юзаю его уже давненько и не понимаю как люди без него обходятся. И не важно кто ты, программист, админ или просто гик.
DimoNj
18.10.2021 13:14Спасибо за статью! Мне пока, правда, ngrok хватает, но кто знает, что будет дальше)
softkot
18.10.2021 13:59+1В контексте данной задачи ещё можно рассмотреть варианты на базе vpn zerotier. его бесплатного варианта хватит на многое, а дальше не грех и денежку заплатить.
lizergil
Просветите, зачем нужно идти по публичному домену на локальное приложение?
anydasa Автор
получить callback от внешнего сервиса
посмотреть на приложение со смартфона
поделится ссылкой с коллегой для обсуждения
https не самоподписной
много причин можно найти
но не всем это нужно, мне долгое время так же ненужно было
Yser
Тестировать вебхуки, например.