Кажется, Microsoft все больше и больше любит Linux! Приложения ASP.NET Core теперь по-настоящему кроссплатформенны и могут запускаться в «никсах», а соответственно, и в Docker. Давай посмотрим, как их можно упаковать, чтобы развертывать на Linux и использовать в связке с Nginx.
Пару слов о Docker
О микросервисной архитектуре слышали практически все. Сам концепт разбития приложения на части не сказать чтобы новый. Но, как известно, новое — это хорошо забытое и переработанное старое.
Если постараться рассказать об архитектуре в нескольких словах, то веб-приложение разбивается на отдельные унитарные части — сервисы. Сервисы не взаимодействуют между собой напрямую и не имеют общих баз данных. Это делается для возможности изменять каждый сервис без последствий для других. Сервисы упаковываются в контейнеры. Среди контейнеров правит бал Docker.
Для того чтобы описать, что такое Docker, очень часто упрощенно используют термин «виртуальная машина». Сходство определенно есть, но говорить так неправильно. Проще всего это различие понять, посмотрев на следующие изображения из официальной документации «Докера».


Контейнеры используют ядро текущей операционной системы и делят его между собой. В то время как виртуальные машины с помощью hypervisor используют аппаратные ресурсы.
Образ/Image «Докера» — это read-only-объект, который, по сути, хранит в себе шаблон для построения контейнера. Контейнер — это среда, в которой выполняется код. Образы хранятся в репозиториях. Например, официальный репозиторий Docker Hub позволяет хранить приватно только один образ. Впрочем, это бесплатно, так что даже за это нужно их поблагодарить.
«Докер» не единственный предоставляет контейнеризацию: существуют и другие технологии. Например, rkt (произносится как «рокет») от CoreOS, LXD (произносится как «лексди») от Ubuntu, Windows Containers ни за что не угадаете от кого.
Разбирать установку «Докера» особого смысла нет, ведь его можно установить на множество операционных систем. Укажу только, что скачать его под свою платформу можно из Docker Store. Если ты устанавливаешь Docker под Windows, то необходимо, чтобы в BIOS и в ОС была включена виртуализация. О том, как включить ее в «десятке», можно прочитать в статье «Установка Hyper-V в Windows 10».
Теперь, когда мы ознакомились с теорией, давай перейдем к практике.
Создание проекта с поддержкой Docker
«Докер» — это, конечно, линуксовый продукт, но при необходимости можно его использовать при разработке под Mac или под Windows. При создании проекта в Visual Studio для добавления поддержки «Докера» достаточно поставить флажок Enable Docker Support.
Поддержку «Докера» можно включить и в существующий проект. Добавляется она таким же образом, как и различные новые компоненты: контекстное меню Add –> Docker Support.
Если на твоей машине установлен и запущен «Докер», будет автоматически открыта консоль и выполнена команда
1 |
$ docker pull microsoft/aspnetcore:2.0 |
которая запускает скачивание образа. Этот образ фактически заготовка, на основе которой будет создан твой образ. ASP.NET Core 2.1 использует уже другой образ — microsoft/dotnet:sdk.
В директории с решением для тебя будут автоматически созданы следующие файлы: .dockerignore (исключение файлов и директорий из образа «Докера»), docker-compose.yml (с помощью этого файла можно сконфигурировать выполнение нескольких сервисов), docker-compose.override.yml (вспомогательная конфигурация docker-compose), docker-compose.dcproj (файл проекта для Visual Studio).
В директории с проектом создастся файл Dockerfile. Собственно, с помощью этого файла мы и создаем свой образ. По умолчанию (в случае если проект называется DockerServiceDemo) он может выглядеть примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
FROM microsoft/aspnetcore:2.0 AS base WORKDIR /app EXPOSE 80 FROM microsoft/aspnetcore-build:2.0 AS build WORKDIR /src COPY DockerServiceDemo/DockerServiceDemo.csproj DockerServiceDemo/ RUN dotnet restore DockerServiceDemo/DockerServiceDemo.csproj COPY . . WORKDIR /src/DockerServiceDemo RUN dotnet build DockerServiceDemo.csproj -c Release -o /app FROM build AS publish RUN dotnet publish DockerServiceDemo.csproj -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "DockerServiceDemo.dll"] |
Начальная конфигурация для .NET Core 2.0 не позволит сразу построить образ с помощью команды docker build. Она настроена на то, что будет запущен файл docker-compose из директории уровнем выше. Чтобы построение пошло успешно, Dockerfile можно привести к подобному виду:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
FROM microsoft/aspnetcore:2.0 AS base WORKDIR /app EXPOSE 80 FROM microsoft/aspnetcore-build:2.0 AS build WORKDIR /src COPY DockerServiceDemo.csproj DockerServiceDemo.csproj RUN dotnet restore DockerServiceDemo.csproj COPY . . WORKDIR /src RUN dotnet build DockerServiceDemo.csproj -c Release -o /app FROM build AS publish RUN dotnet publish DockerServiceDemo.csproj -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "DockerServiceDemo.dll"] |
Все, что я сделал, — убрал лишнюю директорию DockerServiceDemo.
Если ты используешь Visual Studio Code, то файлики придется генерировать вручную, хотя в VS Code и имеется вспомогательная функциональность в виде расширения Docker. Добавлю ссылку на мануал, как работать с «Докером» из VS Code: Working with Docker. Да, статья на английском, но она ведь с картинками.
Три аккорда «Докера»
Для ежедневной работы с «Докером» достаточно помнить всего лишь несколько команд.
Самая главная команда — это, конечно, построение образа. С помощью bash/CMD/PowerShell заходим в директорию, где лежит Dockerfile, и выполняем команду
1 |
$ docker build -t your_image_name . |
Здесь после параметра -t задается имя твоего образа. Внимание: в конце команды после пробела точка. Эта точка означает, что используется текущая директория. Образ можно пометить каким-нибудь тегом (номером или названием). Для этого после имени поставить двоеточие и указать тег. Если этого не сделать, то по умолчанию тег будет задан с наименованием latest. Чтобы отправить образ в репозиторий, в имя образа нужно включить имя репозитория. Примерно так:
1 |
docker build -t docker_account_name/image_name:your_tag . |
Здесь your_docker_account_name — это имя твоего аккаунта в docker hub.
Если ты создал образ только с локальным именем, не включающим в себя репозиторий, то пометить образ другим именем можно и после построения с помощью следующей команды:
1 |
$ docker tag image_name docker_account_name/image_name:your_tag |
Чтобы отправить изменения в хаб, теперь нужно выполнить
1 |
$ docker push docker_account_name/image_name:your_tag |
Перед этим необходимо зайти в твой аккаунт «Докера». На Windows это делается из UI приложения, а вот на *nix — командой
1 |
$ docker login |
На самом деле трех команд недостаточно. Необходимо еще иметь возможность проверить работу контейнера. Команда, с помощью которой можно запустить контейнер, выглядит так:
1 |
$ docker run -it -p 5000:80 image_name |
Параметр -it создаст псевдо-TTY, и твой контейнер будет отвечать на запросы. После запуска команды сервис станет доступным по адресу http://localhost:5000/. Параметр -p 5000:80 связывает 5000-й порт контейнера с 80-м портом хоста.
Кроме того, есть такие команды:
- docker ps -a — покажет список контейнеров. Так как добавлен ключ -a, то будут отображены все контейнеры, а не только те, которые запущены на данный момент;
- docker rm container_name — эта команда удалит контейнер с именем container_name (rm — сокращение от remove);
- docker logs container_name — отобразит логи контейнера;
- docker rmi image_name — удалит образ с именем image_name.
Запуск контейнера через обратный прокси-сервер
Дело в том, что сами приложения .NET Core используют свой веб-сервер Kestrel. Этот сервер не рекомендуется для работы на production. Почему? Есть несколько объяснений.
Если несколько приложений делят между собой IP и порт, то Kestrel не сможет распределять трафик. Кроме того, обратный прокси-сервер предоставляет дополнительный слой безопасности, упрощает балансировку нагрузки и настройку SSL, а также лучше интегрируется в существующую инфраструктуру. Для большинства разработчиков главная причина необходимости реверс-прокси именно в дополнительной безопасности.
Мы сперва восстановим начальную конфигурацию Dockerfile, а после разберемся с файлом docker-compose.yml и попробуем запустить наш сервис в одиночку.
Мой файл docker-compose, созданный по умолчанию, выглядит так:
1 2 3 4 5 6 7 8 |
version: '3.4' services: dockerservicedemo: image: ${DOCKER_REGISTRY}dockerservicedemo build: context: . dockerfile: DockerServiceDemo/Dockerfile |
Файл docker-compose.override.yml добавляет в конфигурацию несколько настроек:
version: ‘3.4’
1 2 3 4 5 6 |
services: dockerservicedemo: environment: - ASPNETCORE_ENVIRONMENT=Development ports: - "80" |
Построить созданное решение мы можем с помощью docker-compose build, а вызвав команду docker-compose up, мы запустим наш контейнер. Все работает? Тогда переходим к следующему шагу. Создаем файл nginx.info. Конфигурация будет примерно следующая:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
worker_processes 4; events { worker_connections 1024; } http { sendfile on; upstream app_servers { server dockerservicedemo:80; } server { listen 80; location / { proxy_pass http://app_servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } } |
Здесь мы указываем, что nginx будет прослушивать 80-й порт (listen 80;). А полученные запросы будет переадресовывать на 80-й порт хоста в контейнер dockerservicedemo. Кроме того, мы указываем nginx, какие заголовки необходимо передавать дальше.
Мы можем использовать HTTP в nginx, а доступ к веб-сайту настроить через HTTPS. Когда HTTPS-запрос проходит через HTTP-прокси, то много информации из HTTPS не передается в HTTP. Кроме того, при использовании прокси теряется внешний IP-адрес. Чтобы эта информация передавалась в заголовках, необходимо изменить код нашего ASP.NET-проекта и добавить в начало метода Configure файла Startup.cs следующий код:
1 2 3 |
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); |
Большинство прокси-серверов используют заголовки X-Forwarded-For и X-Forwarded-Proto. Именно эти заголовки и указаны сейчас в конфигурации nginx.
Теперь включим образ nginx и файл nginx.conf в конфигурацию docker-compose. Осторожно, в YAML пробелы имеют значение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
version: '3.4' services: dockerservicedemo: image: ${DOCKER_REGISTRY}dockerservicedemo build: context: . dockerfile: DockerServiceDemo/Dockerfile ports: - 5000:80 proxy: image: nginx:latest volumes: - ./DockerServiceDemo/nginx.conf:/etc/nginx/nginx.conf ports: - 80:80 |
Здесь мы добавляем к нашей конфигурации прокси в виде образа nginx. К этому образу «цепляем» внешний файл настроек. Его мы как бы монтируем в файловую систему контейнера с помощью механизма под названием volume. Если добавить в конец :ro, то объект будет смонтирован только для чтения.
Прокси слушает внешний 80-й порт машины, на которой запущен контейнер, и передает запрос на внутренний 80-й порт контейнера.
Выполнив команду docker-compose up, мы запулим, то есть извлечем из репозитория, образ nginx и стартанем наш контейнер вместе с контейнером прокси. Теперь по адресу http://localhost:80/ он будет доступен через nginx. На 5000-м порте приложение крутится еще и под Kestrel.
Проверить, что запрос к веб-приложению проходит через реверс-прокси, мы можем так: открыть в браузере Chrome developer tools, зайти на закладку Network, здесь кликнуть на localhost и выбрать закладку Headers.

Запуск контейнера через прокси и HTTPS
Версия ASP.NET Core 2.1 принесла с собой улучшения поддержки HTTPS. Скажем, такой middleware позволяет совершать редирект с незащищенного соединения на защищенное:
1 |
app.UseHttpsRedirection(); |
А следующий позволяет использовать HTTP Strict Transport Security Protocol — HSTS.
1 |
app.UseHsts(); |
HSTS — это фича из протокола HTTP/2, спецификация которого была выпущена в 2015 году. Поддерживается современными браузерами и информирует о том, что веб-сайт использует только HTTPS. Так обеспечивается защита от downgrade-атаки, во время которой атакующий может, перейдя на незащищенный протокол HTTP, например, понизить версию TLS или даже подменить сертификат. Как правило, данный вид атак используется совместно с атаками man in the middle.
Следует помнить, что HSTS не поможет, если пользователь заходит на сайт по протоколу HTTP и потом редиректится на HTTPS. Существует так называемый Chrome preload list, который содержит ссылки на сайты, поддерживающие HTTPS. Другие браузеры (Firefox, Opera, Safari, Edge) также поддерживают списки HTTPS-сайтов, созданные на базе списка Chrome. Но во всех этих списках содержатся далеко не все сайты.
При первом запуске какого-либо Core-приложения на Windows ты получишь сообщение о том, что был создан и установлен сертификат разработчика. Кликнув кнопку и установив сертификат, ты таким образом сделаешь его доверенным. Из командной строки на macOS добавить доверие сертификату можно с помощью
1 |
> dotnet dev-certs https -trust |
Если утилита dev-certs не установлена, то установить ее можно командой
1 |
> dotnet tool install --global dotnet-dev-certs |
Как добавить сертификат в trusted на Linux, зависит от дистрибутива. В тестовых целях используем именно сертификат разработчика. Действия с сертификатом, подписанным CA, аналогичны. При желании можно воспользоваться и бесплатными сертификатами Let’s Encrypt.
Экспортировать сертификат разработчика в файл можно с помощью команды
1 |
> dotnet dev-certs https -ep путь_к_создаваемому_файлу.pfx |
Файл следует скопировать в директорию %APPDATA%/ASP.NET/Https/ под Windows или же в /root/.aspnet/https/ под macOS/Linux. Для того чтобы контейнер подцепил путь к сертификату и его пароль, необходимо создать User secrets со следующим содержимым:
1 2 3 4 5 6 7 8 9 10 |
{ "Kestrel":{ "Certificates":{ "Default":{ "Path": "/root/.aspnet/https/имя_твоего_сертификата.pfx", "Password": "пароль_от_твоего_сертификата" } } } } |
Этот файл хранит незашифрованные данные и потому используется только при разработке. Создается файл в Visual Studio через вызов контекстного меню на иконке проекта или с помощью утилиты user-secrets на Linux.
На Windows файл будет сохранен в директории %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, а на macOS и Linux он сохранится в ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json.
Для сохранения настроек на продакшене некоторые дистрибутивы Linux могут использовать systemd. Настройки сохраняются под атрибутом Service. Например, так:
1 2 3 |
[crayon-67b57edf830f5416727366 inline="true" class="nginx"]<span class="pun">[</span><span class="typ">Service</span><span class="pun">]</span> <span class="typ">Environment</span><span class="pun">=</span><span class="str">"Kestrel _ Certificates _ Default _Path=/root/.aspnet/https/имя_твоего_сертификата.pfx"</span> <span class="typ">Environment</span><span class="pun">=</span><span class="str">"Kestrel _ Certificates _ Default _Password=пароль_от_твоего_сертификата"</span> |
[/crayon]
Далее приведу и разберу сразу рабочий вариант конфигурации «Докера» для прокси и контейнера через HTTPS.
Файл docker-compose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
version: '3.4' services: dockerservicedemo21: image: ${DOCKER_REGISTRY}dockerservicedemo build: context: . dockerfile: DockerServiceDemo/Dockerfile Файл override: version: '3.4' services: dockerservicedemo: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=https://+:44392;http://+:80 - ASPNETCORE_HTTPS_PORT=44392 ports: - "59404:80" - "44392:44392" volumes: - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro proxy: image: nginx:latest volumes: - ./DockerServiceDemo/nginx.conf:/etc/nginx/nginx.conf - ./DockerServiceDemo/cert.crt:/etc/nginx/cert.crt - ./DockerServiceDemo/cert.rsa:/etc/nginx/cert.rsa ports: - "5001:44392" |
Теперь опишу непонятные моменты. ASPNETCORE_URLS позволяет нам не указывать в коде приложения с помощью app.UseUrl прослушиваемый приложением порт. ASPNETCORE_HTTPS_PORT делает редирект, аналогичный тому, какой бы сделал следующий код:
1 |
services.AddHttpsRedirection(options => options.HttpsPort = 44392) |
То есть трафик с HTTP-запросов будет перенаправлен на определенный порт HTTPS-запросов.
С помощью ports указывается, что запрос с внешнего 59404-го порта будет перенаправлен на 80-й контейнера, а с 44392-го внешнего порта на 44392-й. Теоретически, раз у нас будет настроен обратный прокси-сервер, ports с этими перенаправлениями мы можем и убрать. С помощью volumes монтируется директория с pfx-сертификатом и UserSecrets приложения с указанием пароля и ссылки на сертификат. В разделе proxy указывается, что запросы с 5001-го внешнего порта будут перенаправляться на 44392-й порт nginx. Кроме того, монтируется файл с конфигурацией nginx, а также сертификат и ключ к сертификату.
Чтобы из одного сертификата формата pfx (который у нас уже есть) создать файлы crt и rsa, можно воспользоваться OpenSSL. Сначала необходимо извлечь сертификат:
1 |
$ openssl pkcs12 -in ./ваш_сертификат.pfx -clcerts -nokeys -out domain.crt |
А затем приватный ключ:
1 |
$ openssl pkcs12 -in ./ваш_сертификат.pfx -nocerts -nodes -out domain.rsa |
Конфигурация nginx следующая:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
worker_processes 4; events { worker_connections 1024; } http { sendfile on; upstream app_servers { server dockerservicedemo:44392; } server { listen 44392 ssl; ssl_certificate /etc/nginx/cert.crt; ssl_certificate_key /etc/nginx/cert.rsa; location / { proxy_pass https://app_servers; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } } |
Прокси-сервер прослушивает 44392-й порт. На этот порт приходят запросы с 5001-го порта хоста. Далее прокси перенаправляет запросы на 44392-й порт контейнера dockerservicedemo.
РЕКОМЕНДУЕМ:
Безопасность Docker
Разобравшись с данными примерами, ты получишь хороший бэкграунд для работы с «Докером», микросервисами и nginx.