Развертывание Spring Boot приложения с помощью Nginx, Let's Encrypt и Docker Compose
Введение
Привет, Хабр! В своей первой статье я бы хотел поделиться опытом в развертывании Spring Boot приложения. Но для начала небольшое отступление, которое должно ответить на вопросы зачем и почему.
Недавно я столкнулся с задачей разработать Telegram бота. Казалось бы, что тут сложного? Ну раз надо, то разрабатывай, где тут могут быть сложности? Но вот беда, ранее я не сталкивался с задачей развертывания проекта, тем более было много вопросов касаемо получения SSL сертификата так как Telegram API работает только с HTTPS протоколом. Увы после долгих поисков я так и не нашел статьи, которая ответила бы на все вопросы, поэтому процесс деплоя затянулся из-за того, что пришлось собирать весь материал по кусочкам. Теперь, когда у меня получилось разобраться с этой проблемой, я бы хотел вам рассказать как это сделать, чтобы сэкономить вам время и бонусом 2000 рублей за SSL сертификат)
Репозиторий с финальным проектом вы можете найти здесь — тык. Для удобства сделал 3 ветки, о смысле которых вы поймете после прочтения)
И так, начнем!
Подготовим сервер
Для своих тестов я использовал самый простой облачный сервер на Ubuntu от Timeweb.
Первое, что нам потребуется сделать — это подготовить сервер, а именно:
Создать нового пользователя с привилегией администратора
Установить Docker и Docker Compose
Установить 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!
Останавливаем nginx:
docker-compose stop nginx
Создаём директорию для ключа Diffie-Hellman:
mkdir dhparam
Генерируем ключ:
sudo openssl dhparam -out /home/myuser/spring-boot-deploy-with-nginx-example/dhparam/dhparam-2048.pem 2048
Меняем файл конфигурации 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
на свой домен)Вносим пару изменений в 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
И вот она последняя команда, после которой вы выдохните и нальёте себе чего-нибудь покрепче (я про чай конечно), чтобы отметить
docker-compose up -d --force-recreate --no-deps nginx
Поздравляю!!! Теперь с чувством победителя отправляемся по ссылочке -"https://domen.ru/person"
То, что стоит знать, прежде чем считать, что вы стали гуру по деплою
SSL сертификаты от
Certbot
штука не вечная. Их надо обновлять каждые 90 дней, а лучше для подстраховки каждые 60 днейCertbot
паренек не сильно общительный так как не любит чтобы его беспокоили больше чем 5 раз в неделю (пришел к этому опытным путем пока разбирался в чем ошибка, а про 5 дней вычитал на просторах интернета). Поэтому аккуратней с обновлением сертификата)Процесс обновления сертификата можно автоматизировать с помощью
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
Да да, теперь вы гуру, но это не предел совершенства
Заключение
Вот и всё, надеюсь эта статья сэкономила вам время, а может даже подняла настроение!)
Для меня это был интересный опыт, все-таки первая статья, а не сухие заметки в README на будущее.
Буду рад советам в комментариях, так как для меня эта тема в новинку и уверен есть много моментов для улучшения.
Большое спасибо за то, что прочитали! Надеюсь, что скоро руки доберутся до написания новых публикаций.