Хабр
Развёртывание Spring Boot приложения с помощью Nginx, Let's Encrypt и Docker Compose
Пошаговое руководство по развёртыванию Spring Boot приложения с Nginx, SSL от Let's Encrypt и Docker Compose на Ubuntu сервере
Введение
Привет, Хабр! В своей первой статье я бы хотел поделиться опытом в развертывании Spring Boot приложения. Но для начала небольшое отступление, которое должно ответить на вопросы зачем и почему.
Недавно я столкнулся с задачей разработать Telegram бота. Казалось бы, что тут сложного? Но вот беда, ранее я не сталкивался с задачей развертывания проекта, тем более было много вопросов касаемо получения SSL сертификата так как Telegram API работает только с HTTPS протоколом. Увы после долгих поисков я так и не нашел статьи, которая ответила бы на все вопросы, поэтому процесс деплоя затянулся из-за того, что пришлось собирать весь материал по кусочкам.
Репозиторий с финальным проектом вы можете найти на GitHub. Для удобства сделал 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 nginx/conf.d/app.conf
И напишем серверный блок:
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/init.sql
CREATE USER myuser WITH PASSWORD 'pass';
CREATE DATABASE app;
GRANT ALL PRIVILEGES ON DATABASE app TO myuser;
Docker Compose
Открываем файл 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 на ваше, а также заменить почту.
Создаём файл .env:
vim .env
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
На старт, внимание, марш!
docker-compose up -d --build
Если все прошло по плану, то перейдя по ссылке http://domen.ru/person, вы увидите ответ нашего приложения.
Полезные команды для отладки:
docker-compose logs service_name— почитать логиdocker system df— посмотреть используемую памятьdocker container ls -a/docker rm <container_id>— посмотреть/почистить контейнерыdocker image ls -a/docker rmi <image_id>— посмотреть/почистить образыdocker system prune— снести всё, что было в докере
Получаем SSL сертификат
Проверяем правильность монтирования учётных данных:
docker-compose exec nginx ls -la /etc/letsencrypt/live
Если все прошло успешно, то вы увидите файлы: README и domen.ru.
Меняем в docker-compose.yml флаг --staging на --force-renewal:
certbot:
# ...
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
Перезапускаем certbot:
docker-compose up --force-recreate --no-deps certbot
Финальная конфигурация
Останавливаем nginx:
docker-compose stop nginx
Генерируем ключ Diffie-Hellman:
mkdir dhparam
sudo openssl dhparam -out ./dhparam/dhparam-2048.pem 2048
Обновляем конфигурацию nginx (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;
}
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
}
Добавляем порт 443 и том dhparam в docker-compose.yml:
nginx:
# ...
ports:
- 80:80
- 443:443
volumes:
# ...
- dhparam:/etc/ssl/certs
volumes:
# ...
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
Что стоит знать
SSL сертификаты от Certbot надо обновлять каждые 90 дней, а лучше каждые 60 дней. Certbot не любит, когда его беспокоят больше 5 раз в неделю.
Процесс обновления можно автоматизировать с помощью cron:
#!/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 на будущее. Буду рад советам в комментариях.