Введение

Привет, Хабр! В своей первой статье я бы хотел поделиться опытом в развертывании 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 на будущее.

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

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

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


  1. meljohin
    08.09.2022 18:41
    +3

    Советую попробовать Caddy. Простое проксирование HTTPS -> HTTP можно запустить одной командой:

    $ docker run -d -p 80:80 -p 443:443 -p 443:443/udp \
        -v caddy_data:/data \
        -v caddy_config:/config \
        caddy caddy reverse-proxy --from domen.ru --to app:8080

    Это заменит certbot + nginx (пример можно легко адаптировать под docker-compose или посмотреть тут). Новый сертификат будет запрошен после первого обращения и будет обновляться автоматически.


    1. Mark1708 Автор
      08.09.2022 18:46

      Большое спасибо! Обязательно освою такую полезность.

      Жаль, что тогда не разобрался в плюсах использования этого веб сервера. Это могло сэкономить много времени(


      1. nkgrig
        09.09.2022 14:12

        Могу посоветовать также для этих целей Trarfik


        1. Mark1708 Автор
          09.09.2022 14:14

          Спасибо, изучу)


    1. l0ser140
      09.09.2022 03:21
      +2

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


      1. Mark1708 Автор
        09.09.2022 10:14

        Спасибо! Это очень пригодится мне в ближайшее)


  1. valera5505
    09.09.2022 01:28

    много вопросов касаемо получения SSL сертификата так как Telegram API работает только с HTTPS протоколом

    А какая связь между API Telegram для ботов и сертификатом для домена? С телеграмом можно общаться и без домена вовсе.


    1. Mark1708 Автор
      09.09.2022 10:08

      Такие требования при использовании Webhook ????‍♂️

      Сам был не в восторге от прочитанного


      1. valera5505
        09.09.2022 10:10

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

        A certificate can either be verified or self-signed.


        1. Mark1708 Автор
          09.09.2022 10:19

          Ой, это я упустил ????

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