Назад к публикациям

Хабр

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

Пошаговое руководство по развёртыванию Spring Boot приложения с Nginx, SSL от Let's Encrypt и Docker Compose на Ubuntu сервере

7 мин чтения 31K читателей
JavaSpring BootNginxDockerDevOps
Читать на Хабре ↗ Репозиторий на GitHub ↗

Введение

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