Развертывание Spring Boot приложения с помощью Nginx, Let’s Encrypt и Docker Compose

от автора

Введение

Привет, Хабр! В своей первой статье я бы хотел поделиться опытом в развертывании Spring Boot приложения. Но для начала небольшое отступление, которое должно ответить на вопросы зачем и почему.

Недавно я столкнулся с задачей разработать Telegram бота. Казалось бы, что тут сложного? Ну раз надо, то разрабатывай, где тут могут быть сложности? Но вот беда, ранее я не сталкивался с задачей развертывания проекта, тем более было много вопросов касаемо получения SSL сертификата так как Telegram API работает только с HTTPS протоколом. Увы после долгих поисков я так и не нашел статьи, которая ответила бы на все вопросы, поэтому процесс деплоя затянулся из-за того, что пришлось собирать весь материал по кусочкам. Теперь, когда у меня получилось разобраться с этой проблемой, я бы хотел вам рассказать как это сделать, чтобы сэкономить вам время и бонусом 2000 рублей за SSL сертификат)

Репозиторий с финальным проектом вы можете найти здесь — тык. Для удобства сделал 3 ветки, о смысле которых вы поймете после прочтения)

И так, начнем!

Подготовим сервер

Для своих тестов я использовал самый простой облачный сервер на Ubuntu от Timeweb.

Первое, что нам потребуется сделать — это подготовить сервер, а именно:

  1. Создать нового пользователя с привилегией администратора

  2. Установить Docker и Docker Compose

  3. Установить git и авторизоваться

Если будет интересно могу позже написать отдельную статью как подготовить сервер

Клонируем приложение

Для тестов я сделал простое Spring Boot приложение и чтобы было интересней использовал не H2, а PostgreSQL + Flyway.

mkdir spring-boot-deploy-with-nginx-example cd spring-boot-deploy-with-nginx-example/ git clone git@github.com:Mark1708/simple-spring-boot-app.git test-deploy

В этом проекте вы можете найти заготовленный Dockerfile. Совершенно простой без multistage, но нам этого и не надо для простого тестового проекта.

FROM maven:3.6.3-jdk-11 AS builder COPY ./ ./ RUN mvn clean package -DskipTests FROM openjdk:11.0.7-jdk-slim COPY --from=builder /target/simple-spring-boot-app-0.0.1-SNAPSHOT.jar /app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar"]

Настроим веб сервер

Использовать будем Nginx, поэтому настроим минимальную конфигурацию и двинем дальше.

Для начала создадим директорию: mkdir -p nginx/conf.d

Затем откроем файл через vim: vim nginx/conf.d/app.conf

Никогда не думал, что vim может понравиться, однако пока занимался деплоем проекта моё мнение поменялось

Полезные команды которые пригодились:

:set paste для копирования без авто отступов

:set number для нумерации строк

И напишем серверный блок:

server {  listen 80;  listen [::]:80;   charset utf-8;  access_log off;   root /var/www/html;  server_name domen.ru www.domen.ru;   location / {      proxy_pass http://simple-spring-boot-app:8080;      proxy_set_header Host $host:$server_port;      proxy_set_header X-Forwarded-Host $server_name;      proxy_set_header X-Real-IP $remote_addr;      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;  }   location /static {      access_log   off;      expires      30d;       alias /simple-spring-boot-app/static;  }   location ~ /.well-known/acme-challenge {      allow all;      root /var/www/html;  } }

И да, не забываем заменить domen.ru на ваш настоящий домен, либо можете его приобрести и не париться?

Инициализация базы данных

База данных кто такой??

Пока никто и это можно исправить создав файл init.sql в папке init.

mkdir init && vim init.sql

Остается лишь записать в него волшебное слово Пожалуйста на языке запросов SQL.

CREATE USER myuser WITH PASSWORD 'pass'; CREATE DATABASE app; GRANT ALL PRIVILEGES ON DATABASE app TO myuser;

Куда нам без Docker Compose?

Конечно, никуда, поэтому им мы и займемся! И да, тут начинается веселье, так что пристегнитесь?

Откроем файл docker-compose.yml: vim docker-compose.yml

И напишем в нем небольшое сочинение на тему «Как автор статьи не отдохнул летом»

version: '3'   services:   nginx:     container_name: nginx     image: nginx:1.13     restart: always     ports:       - 80:80     volumes:       - ./nginx/conf.d:/etc/nginx/conf.d       - web-root:/var/www/html       - certbot-etc:/etc/letsencrypt       - certbot-var:/var/lib/letsencrypt     networks:       - app-network        certbot:     image: certbot/certbot     depends_on:       - nginx     container_name: certbot     volumes:       - certbot-etc:/etc/letsencrypt       - certbot-var:/var/lib/letsencrypt       - web-root:/var/www/html     command: certonly --webroot --webroot-path=/var/www/html --email pochta@gmail.com --agree-tos --no-eff-email --staging -d domen.ru -d www.domen.ru    postgresql:     container_name: postgresql     image: postgres:12.2-alpine     environment:       POSTGRES_USER: ${POSTGRES_USER}       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}     ports:       - "5432:5432"     restart: always     volumes:       - ./init:/docker-entrypoint-initdb.d/     networks:       - app-network        app:     container_name: simple-spring-boot-app     build:       context: ./simple-spring-boot-app       dockerfile: Dockerfile     environment:       - "DB_HOST=postgresql"       - "POSTGRES_USER=${POSTGRES_USER}"       - "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}"       - "SERVER_PORT=8080"     expose:       - "8080"     depends_on:       - nginx       - postgresql     restart: always     networks:       - app-network  volumes:   certbot-etc:   certbot-var:   web-root:          networks:   app-network:     driver: bridge

Не забываем поменять доменное имя domen.ru на ваше, а также было бы неплохо заменить почту. PS: строка 27

Если у вас получилось что-то такое, то считайте, что за сочинение у вас твердая пятерка!

Мы создали 4 контейнера, названия которых достаточно ясно описывают их назначение, за исключением одного. Именно certbot вам и будет экономить 2000 ₽ в год, за что низкий поклон Let’s Encrypt.

И да, чуть не забыл. Мы же хотим, чтобы у нас было всё безопасно?

Поэтому создаём чудесный файл .env.

vim .env

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

# db POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres

На старт, внимание, марш!

С этого этапа обратного пути нет!

Так что настройтесь на веселье

Интерпретируем для docker заголовок этого этапа на его языке:

docker-compose up -d --build

Если все прошло по плану, то перейдя по ссылке — «http://domen.ru/person», вы увидите ответ нашего приложения. А введя команду docker-compose ps -a вы увидите статус UP у всех контейнеров кроме certbot, у которого должен быть статус Exit 0.

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

Тогда вы можете почитать логи с помощью команды — docker-compose logs service_name

Также может случиться так, что память на сервере закончится и тогда придется подчистить закрома Docker. Вот команды которые мне пригодились:

1. docker system df - total info — чтобы посмотреть используемую память

2. docker container ls -a / docker rm <container_id> — чтобы посмотреть все контейнеры / почистить

3. docker image ls -a / docker rmi <image_id> — чтобы посмотреть все имеджи / почистить

4. docker system prune — снести под нолик все, что было в докере

Для внимательных ребят можно двинуться дальше и проверить правильно ли были смонтированы ваши учетные данные для получения сертификата:

docker-compose exec nginx ls -la /etc/letsencrypt/live

Если все прошло успешно, то вы увидите файлы: README и domen.ru

Вперед, только вперед

В предыдущем этапе мы сделали тестовый запрос на получения SSL сертификата и теперь мы можем сделать настоящий, поменяв пару букв в нашем сочинении: vim docker-compose.yml

...   certbot:     image: certbot/certbot     depends_on:       - nginx     container_name: certbot     volumes:       - certbot-etc:/etc/letsencrypt       - certbot-var:/var/lib/letsencrypt       - web-root:/var/www/html     command: certonly --webroot --webroot-path=/var/www/html --email pochta@gmail.com --agree-tos --no-eff-email --force-renewal -d domen.ru -d www.domen.ru ...

Обратите внимание, мы заменим --staging на --force-renewal

После перезапустим certbot с помощью команды — docker-compose up --force-recreate --no-deps certbot

Вы должны увидеть поздравления с получением сертификата и остается дело за малым.

Сделаем финальную конфигурацию

Остается сделать последний шаг, чтобы увидеть эти заветные 5 букв HTTPS!

  1. Останавливаем nginx: docker-compose stop nginx

  2. Создаём директорию для ключа Diffie-Hellman: mkdir dhparam

  3. Генерируем ключ:

    sudo openssl dhparam -out /home/myuser/spring-boot-deploy-with-nginx-example/dhparam/dhparam-2048.pem 2048

  4. Меняем файл конфигурации nginx: vim nginx/conf.d/app.conf

    server {         listen 80;         listen [::]:80;         server_name domen.ru www.domen.ru;          location ~ /.well-known/acme-challenge {           allow all;           root /var/www/html;         }          location / {                 rewrite ^ https://$host$request_uri? permanent;         } }  server {         listen 443 ssl http2;         listen [::]:443 ssl http2;         server_name domen.ru www.domen.ru;          server_tokens off;          ssl_certificate /etc/letsencrypt/live/domen.ru/fullchain.pem;         ssl_certificate_key /etc/letsencrypt/live/domen.ru/privkey.pem;          ssl_buffer_size 8k;          ssl_dhparam /etc/ssl/certs/dhparam-2048.pem;          ssl_protocols TLSv1.2 TLSv1.1 TLSv1;         ssl_prefer_server_ciphers on;          ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;          ssl_ecdh_curve secp384r1;         ssl_session_tickets off;          ssl_stapling on;         ssl_stapling_verify on;         resolver 8.8.8.8;          location / {                 try_files $uri @simple-spring-boot-app;         }          location @simple-spring-boot-app {                 proxy_pass http://simple-spring-boot-app:8080;                 add_header X-Frame-Options "SAMEORIGIN" always;                 add_header X-XSS-Protection "1; mode=block" always;                 add_header X-Content-Type-Options "nosniff" always;                 add_header Referrer-Policy "no-referrer-when-downgrade" always;                 add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;                 # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;                 # enable strict transport security only if you understand the implications         }          root /var/www/html;         index index.html index.htm index.nginx-debian.html; }

    Вы уже знающие, но всё же напомню, что надо менять domen.ru на свой домен)

  5. Вносим пару изменений в docker-compose.yml: vim docker-compose.yml

    Добавляем порт 443 и том dhparam

    ... nginx:     container_name: nginx     image: nginx:1.13     restart: always     ports:       - 80:80       - 443:443  # <======     volumes:       - ./nginx/conf.d:/etc/nginx/conf.d       - web-root:/var/www/html       - certbot-etc:/etc/letsencrypt       - certbot-var:/var/lib/letsencrypt       - dhparam:/etc/ssl/certs   # <======     networks:       - app-network ...   volumes:   certbot-etc:   certbot-var:   web-root:   dhparam:   # <======     driver: local     driver_opts:       type: none       device: /home/myuser/spring-boot-deploy-with-nginx-example/dhparam/       o: bind

  6. И вот она последняя команда, после которой вы выдохните и нальёте себе чего-нибудь покрепче (я про чай конечно), чтобы отметить

    docker-compose up -d --force-recreate --no-deps nginx

Поздравляю!!! Теперь с чувством победителя отправляемся по ссылочке —«https://domen.ru/person»

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

  1. SSL сертификаты от Certbot штука не вечная. Их надо обновлять каждые 90 дней, а лучше для подстраховки каждые 60 дней

  2. Certbot паренек не сильно общительный так как не любит чтобы его беспокоили больше чем 5 раз в неделю (пришел к этому опытным путем пока разбирался в чем ошибка, а про 5 дней вычитал на просторах интернета). Поэтому аккуратней с обновлением сертификата)

  3. Процесс обновления сертификата можно автоматизировать с помощью cron или systemd и такого срипта:

    #!/bin/bash  /usr/local/bin/docker-compose -f /home/myuser/spring-boot-deploy-with-nginx-example/docker-compose.yml run certbot renew --dry-run \ && /usr/local/bin/docker-compose -f /home/myuser/spring-boot-deploy-with-nginx-example/docker-compose.yml kill -s SIGHUP nginx
  4. Да да, теперь вы гуру, но это не предел совершенства

Заключение

Вот и всё, надеюсь эта статья сэкономила вам время, а может даже подняла настроение!)

Для меня это был интересный опыт, все-таки первая статья, а не сухие заметки в README на будущее.

Буду рад советам в комментариях, так как для меня эта тема в новинку и уверен есть много моментов для улучшения.

Большое спасибо за то, что прочитали! Надеюсь, что скоро руки доберутся до написания новых публикаций.


ссылка на оригинал статьи https://habr.com/ru/post/687210/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *