Делаем домашний VPS для тестовых и пет проектов

Привет, Хабр! Меня зовут Васьен, я — .NET backend разработчик. До этого момента я несколько месяцев проходил увлекательней жизненный квест по поиску работы, выполняя одни и те же задания — расскажи рекрутеру кем ты себя видишь через 5 лет, реши тестовое, не получи ответ. И задумавшись о том, как же повысить вероятность попасть на тех собес после решения тестового и выделиться на фоне остальных кандидатов, я пришел к идеи развернуть на компьютере окружение, в котором я смогу его запустить и отправить ссылку для взаимодействия.

 На самом деле все, о чем будет рассказано далее, можно и нужно делать на VPS любого хостера — стоимость недорогая, 24\7 аптайм. Но, во-первых, иногда хочется сэкономить. Во-вторых, некоторые проекты могут потребовать больше ресурсов, чем одноядерный Celeron и 1Gb оперативки, а такая машинка будет уже дороже. Поэтому будучи владельцем компьютера с чуть более выдающимися характеристиками, чем предлагает хостер на начальных тарифах, а также имея дома гигабитный интернет и не выключающийся комп, я пришел к решению развернуть импровизированный хостинг на домашнем компьютере. Для этого нам понадобятся доступ к роутеру и докер на ПК.

1. Удаленный доступ.

Первое, с чего стоит начинать — это предоставить возможность кому-то из внешнего мира предоставить возможность достучаться до вашего роутера. По серому IP обращаться заведомо плохая идея, поэтому нам нужно какое-то доменное имя, по которому можно будет обратиться и попасть в нашу сеть. Тут есть несколько вариантов, у каждого свои есть преимущества и недостатки.

  • Используем DynDNS. Из плюсов — вы получаете возможность по имени достучаться до роутера и решаете проблему с динамическим IP. Из минусов — не факт, что ваш IP не серый за NAT, поэтому такой вариант может и не сработать. Ну и не все роутеры умеют в сервисы DDNS, поэтому привязать к роутеру что-то типа myname.homeip.net может стать проблемой.

  • Завести выделенный IP адрес у провайдера и купить домен. Из плюсов — получите красивую ссылку и гарантию, что все заработает, просто все настроить на любом роутере. Из минусов — придется платить за аренду адреса и имени. Правда стоит это какие-то сущие копейки, многие регистраторы дают на первый год домен второго уровня в ru зоне за 100 рублей (а дальше можете и не продлевать), где-то столько же стоит услуга фиксированный IP (или вообще бесплатно).

Микротик в два клика выдает DNS. Будь как микротик

Микротик в два клика выдает DNS. Будь как микротик

Также хочется отметить, что некоторые роутеры имеют свой DDNS сервис и можно в один клик получить доменное имя, правда оно будет очень длинное. Поэтому я советую не пожалеть 200 рублей и приобрести себе доменное имя и взять IP адрес у провайдера. После этого идем в панель управления регистратора, находим управление доменной зоной и добавлять A запись: символ »@» в качестве субдомена и вписываем выданный адрес. Спустя какое-то время мы по этому домену попадем к нам на роутер.

2. Порты.

Теперь нам надо решить следующий вопрос: как по запросу попадать не в роутер, а на наш компьютер. И выход есть, и он простой — Port forwarding. Договариваемся с роутером, что все приходящие запросы извне на какой-то порт перенаправляются на указанный во внутренней сети адрес. Здесь тоже ничего нет сложного, гуглим модель роутера + port forwarding и получаем готовый гайд.

На моем микротике это делается одной командой. Этой командой мы указали NAT правило: транслировать запросы, приходящие на 80 порт на ether5, на IP 192.168.88.52 и 9000 порт. Логично, что этот IP стоит делать зарезервированным и это локальный адрес нашего компьютера.

/ip firewall nat add action=dst-nat chain=dstnat dst-port=80 in-interface=ether5 log=yes protocol=tcp to-addresses=192.168.88.52 to-ports=9000

Теперь можем проверить, что все заработало. Для этого достаточно создать dockerfile с nginx, куда скопируем HTML файл. В моем случае это файлы из папки Catalog

FROM nginx:latest
COPY ./Catalog/ /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

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

docker build -t catalog .
docker run --name catalog -d -p 9000:80 catalog 

Теперь можем проверить, что все прошло успешно. Мы можем увидеть содержимое страницы, которую мы упаковали в контейнер с Nginx. Она должна быть доступна по адресу http://localhost:9000, а также по имеющемуся доменному имени. В моем случае я упаковал каталог издательского дома «Вильямс» по состоянию на 2005 год. С актуальной версией сайта визуальных отличий не много, но вот цены…

2f464aec60cce9251b91f7da5dcb1fee.png

Собственно, на этом основная часть задачи выполнена — мы привязали наш роутер к нужному доменному имени и пробросили порты на нужный нам порт на определенной машине. Теперь все, что мы запустим на 9000 порту на нашем ПК будет доступно, можем запускать любые тестовые задания и API, к ним есть доступ. Но на этом останавливаться рано, есть что еще улучшить поэтому переходим к следующему этапу.

3. Контейнеризация.

Конечно, если нам надо запустить одно простое приложение, то вроде и нет особого смысла изгаляться с докером, с образами и контейнерами, ведь можно просто запустить через терминал или IDE приложение сразу на указанном порту. В .NET можно отредактировать launchSettings.json

{
    "applicationUrl": "http://localhost:5290",
  }

Но что делать в случае, если в нашем задании нам нужны какие-то внешние системы? Возможно, мы делаем приложение, которое работает с БД, или с брокером сообщений, или у нас вообще два приложения, которые должны работать друг с другом? Самым удачным будет запускать их в изолированной от операционной системы среде, используя docker compose. Создадим пустой webapp проект и назовем его First. Добавим к проекту следующий dockerfile

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app ./
ENTRYPOINT ["dotnet", "First.dll"]

Создадим еще папку deployments, внутрь которой поместим файл docker-compose.yaml. Единственная его задача, просто «смоделировать» ситуацию, что к необходимому приложению надо запустить что-то еще. В нашем случае это Postgres и Grafana.

version: '3.8'
services:
  app:
    build:
      context: ../First
      dockerfile: ./dockerfile
    ports:
      -9000:80   # порт, на который будет роутер перенаправлять запросы
    depends_on:
      - postgres
    volumes:
      - $APPDATA/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
    restart: unless-stopped
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    networks:
      - mynet
    container_name: first
    
  grafana:
    image: grafana/grafana
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./monitoring/grafana-data/data:/var/lib/grafana
    networks:
      - mynet

  postgres:
    image: postgres:latest
    container_name: postgres
    restart: unless-stopped
    ports:
      - '5435:5432'
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=Pass!!w0rd""
      - POSTGRES_DB=ExampleDB
    command:
      - "postgres"
      - "-c"
      - "wal_level=logical"
      - "max_prepared_transactions=10"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - mynet

networks:
  mynet:

volumes:
  postgres-data:
  

После вызова мы соберем контейнер, а наше приложение станет доступно локально на 9000 порту, а значит за роутером это будет тот же самый 80 порт.

 docker compose --project-name=first -f ./deployments/docker-compose.yaml up --build -
d

Миссия выполнена? От части да, мы можем предоставить удобный доступ от нашего приложения во внешний мир и убедиться по логам, что его в итоге никто не посмотрел — что еще нужно для полного счастья?

4. Поддомены.

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

Из вопроса есть логичный ответ — пробросить еще один, в этот раз не 80, а любой другой. Соответственно, ваше решение будет доступно по ссылке domain.ru:777. Хоть мы и не прочь поднять бабла, но все же мы не разрабатываем API для азино, а значит такое решение не сказать, что отличный выход. Ну и руководствуясь мудростью «Не ищите лёгких путей, ищите правильный», мы начинаем городить наши велосипеды дальше.

Идем в личный кабинет регистратора и редактируем ресурсные записи домена. Нам надо добавить парочку A записей и привязать их к нашему адресу, но только теперь вместо субдомена »@» указываем желанный поддомен. Я добавил два — first и second для наглядности и простоты. Теперь надо настроить маршрутизацию, чтобы запросы с разными поддоменами отправлялись на разные порты. Самый простой способ — поднять на 9000 порту Nginx, который и будет разруливать входящие запросы и возращать 404, если запрос не содержит поддомен. Создаем nginx.conf

events {
}

http {

server {
        listen 80;
        server_name domain.ru;

        location / {
            return 404;
        }
    }

server {
    listen 80;

    server_name first.domain.ru;

    location / {
        proxy_pass http://192.168.88.52:11000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;

    server_name second.domain.ru;

    location / {
        proxy_pass http://192.168.88.52:12000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
server {
    listen 80;

    server_name bonus.domain.ru;

    location / {
        proxy_pass http://192.168.88.52:10000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
}

И создаем контейнер

docker build -t nginx-container .
docker run -p 9000:80 --name=nginx -d nginx-container

Теперь все запросы, попадающие на 9000 порт будут из контейнера с Nginx перенаправлять по локальной сети: с поддомена first на 11000 порт, а с поддомена second на 12000. Осталось теперь запустить приложения на указанных портах и проверить работоспособность. Для первого приложения в docker-compose.yaml нужно изменить порт на

ports:
      -11000:80

Можем создать такое же пусте WebApp приложение Second и скопировать туда такой же dockerfile и docker-compose.yaml, изменив только название проекта и порт на 12000. Выходит следующий маршрут: роутер:80 → наш_пк:9000 → nginx → наш_пк:11000 если запрос пришел на first.domain.ru или наш_пк:12000 если запрос пришел на second.domain.ru. Поскольку запускаемые приложения можно вешать на любые (почти) порты, как и поддоменов можно насоздавать много, это должно покрыть базовые потребности по запуску и демонстрации каких-то простых проектов, даже одновременно.

Надеюсь, что данная статья пригодится разработчикам, которые задаются вопросом о запуске нескольких проектов на ПК и организации доступа к нему. Возможно кто-то обратил внимание на третий поддомен bonus. Там размещен полный каталог издательства из прошлого, найденный на старом диске. Можно покликать и поностальгировать. И можно присоединиться к моему небольшому сообществу в телеграмме.

© Habrahabr.ru