Post

CI/CD-пайплайн: автодеплой Spring Boot приложения на VPS через GitHub Actions

CI/CD-пайплайн: автодеплой Spring Boot приложения на VPS через GitHub Actions

В этой статье разберем, как развернуть Spring Boot приложение на VPS и настроить автодеплой из GitHub. В качестве примера используем небольшой сервис - Weather Map Demo - и опубликуем его на поддомене weather.abykov.dev.

Арендуем сервер и настроим поддомен, systemd-сервис, HTTPS через Nginx/Let’s Encrypt и CI/CD-пайплайн на GitHub Actions.

Что используем

  • VPS на Ubuntu 24.04;
  • Домен abykov.dev с поддоменом weather.abykov.dev;
  • Java 21, Spring Boot 3, Maven;
  • Nginx + Let’s Encrypt (HTTPS);
  • systemd для запуска jar как сервиса;
  • GitHub Actions для сборки и деплоя.

Что сделаем пошагово

  • Арендуем VPS и привяжем поддомен weather.abykov.dev к IP сервера (DNS A-запись);
  • Подготовим окружение на сервере: установим Java, создадим рабочие директории;
  • Запустим приложение вручную и настроим его как systemd-сервис (переживает перезагрузку);
  • Включим Nginx как reverse-proxy и выпустим бесплатный SSL-сертификат Let’s Encrypt;
  • Настроим GitHub Actions: Maven-сборка, копирование JAR на VPS по SSH и рестарт сервиса - автоматически при пуше в ветку main;

На выходе получим приложение, доступное по HTTPS на собственном домене, обновляемое одним коммитом в репозиторий.

Аренда VPS и домена

Для демонстрационного проекта - Spring Boot-приложения Weather Map Demo - арендуем VPS у хостинг-провайдера и приобретем домен abykov.dev. Для обеспечения удобного доступа к приложению в панели управления DNS создадим A-запись поддомена weather.abykov.dev, указывающую на публичный IP-адрес VPS.

Заходим в DNS-панель, добавляем A-запись для поддомена, указывая на IP VPS:

Пример настройки A-записи для поддомена

  • Имя: weather.abykov.dev
  • Тип: A
  • Значение: 89.111.171.25 (IP адрес VPS)
  • TTL: 3600

Теперь при обращении к www.weather.abykov.dev трафик будет направляться на наш VPS.

Настройка VPS

После приобретения VPS первым этапом является подготовка окружения - обновление системы и установка необходимых пакетов.

1
2
3
4
5
6
7
8
9
10
# обновляем систему
sudo apt update && sudo apt upgrade -y 

# устанавливаем Java (например, OpenJDK 21)
sudo apt install openjdk-21-jdk -y 

# создаем рабочую директорию под приложение
sudo mkdir -p /opt/weather-demo
sudo chown $USER:$USER /opt/weather-demo

В эту директорию (/opt/weather-demo/) будет загружаться JAR-файл приложения.

Запуск Spring Boot вручную

После локальной сборки артефакт необходимо передать на сервер с помощью scp:

1
2
3
scp \
  api/target/api-0.0.1-SNAPSHOT.jar \
  root@<IP_сервера>:/opt/weather-demo/weather-demo.jar

Затем выполнить запуск:

1
2
cd /opt/weather-demo
java -jar weather-demo.jar

⚠️ Для успешного запуска требуется fat jar (он же runnable jar или uber-jar). Такой JAR включает в себя все зависимости и формируется при использовании spring-boot-maven-plugin. Также это обязательное требование для GitHub Pages при работе с кастомными доменами.

В данном виде приложение работает, однако при перезапуске сервера процесс завершится. Чтобы обеспечить автоматический запуск и перезапуск при сбое, используется настройка systemd-сервиса.

Автозапуск через systemd

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

Создадим unit-файл /etc/systemd/system/weather-demo.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Weather Demo Spring Boot App
After=network.target

[Service]
User=root
WorkingDirectory=/opt/weather-demo
ExecStart=/usr/bin/java -jar /opt/weather-demo/weather-demo.jar
SuccessExitStatus=143
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Далее активируем и запускаем сервис:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable weather-demo
sudo systemctl start weather-demo

Проверить состояние можно командой:

1
systemctl status weather-demo

Для просмотра логов приложения используется journalctl:

1
journalctl -u weather-demo -f

Теперь приложение работает как полноценный сервис: оно стартует вместе с системой, перезапускается при сбое и не зависит от активной сессии в терминале.

Настройка доступа по домену и HTTPS

По умолчанию приложение на Spring Boot запускается на порту 8080 и доступно только по IP-адресу сервера. Чтобы открыть его по доменному имени без указания порта, применяется один из двух подходов, рассматриваемых ниже.

Безотносительно подходов, следует выполнить настройку server.address.

Настройка server.address

Важный момент: по умолчанию Spring Boot может слушать только localhost (127.0.0.1), из-за чего приложение доступно лишь внутри самого сервера.

При запуске Spring Boot поднимает встроенный веб-сервер (обычно Tomcat). Параметр server.address определяет, на каком IP-адресе он будет слушать входящие соединения.

Варианты значений:

  • 127.0.0.1 (или localhost). Приложение доступно только внутри сервера (curl http://127.0.0.1:8080). Извне (по IP или домену) открыть не получится.
  • 0.0.0.0. Специальное значение: слушать на всех интерфейсах. Тогда сервис доступен изнутри сервера (localhost:8080), по публичному IP (89.111.171.25:8080), через домен (weather.abykov.dev).
  • Конкретный IP (например 192.168.1.100). Сервер слушает только на этом интерфейсе, иногда полезно для ограниченного доступа.

Так как по умолчанию сервер слушает localhost, запросы снаружи блокируются. После установки server.address=0.0.0.0 приложение становится доступным и по IP, и по домену.

Вариант 1. Spring Boot на 80 порту

В application.properties можно задать:

1
2
server.address=0.0.0.0
server.port=80

После перезапуска сервис станет доступен по адресу: http://weather.abykov.dev

Однако у такого решения есть недостаток: при размещении нескольких приложений придется вручную распределять порты.

Вариант 2. Nginx как reverse-proxy (рекомендуется)

Более универсальный подход - использовать Nginx в качестве обратного прокси: Spring Boot продолжает работать на 8080, а Nginx принимает запросы на 80 и перенаправляет их внутрь.

Что такое обратное проксирование?

Стоит пояснить термин. Существует два вида прокси-серверов:

  • Forward proxy (прямой прокси) - работает на стороне клиента (стоит перед пользователем). Пользователь отправляет запрос не напрямую к сайту, а через прокси. Пример: корпоративный прокси-сервер, который фильтрует трафик сотрудников или скрывает их IP.
  • Reverse proxy (обратный прокси) - работает на стороне сервера (стоит перед сервером). Пользователь обращается к единому адресу (например, weather.abykov.dev), а прокси перенаправляет запрос во внутренние сервисы (например, на Spring Boot приложение на порту 8080).

Именно поэтому его называют “обратным” - он скрывает внутренние сервисы за единым фасадом.

Визуально схема выглядит так:

1
2
Клиент -> Forward Proxy -> Интернет (сайты)
Клиент -> Reverse Proxy -> Внутренние сервисы (API, приложения)

В нашем случае Nginx выполняет роль reverse proxy: он принимает HTTP-запросы на 80-м порту и проксирует их к приложению Spring Boot на порту 8080.

Установка и настройка Nginx:

1
sudo apt install nginx -y

Создание конфигурации /etc/nginx/sites-available/weather.abykov.dev:

1
2
3
4
5
6
7
8
9
10
11
12
server {
    listen 80;
    server_name weather.abykov.dev;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Активация сайта и проверка конфигурации::

1
2
3
sudo ln -s /etc/nginx/sites-available/weather.abykov.dev /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Теперь приложение открывается по адресу http://weather.abykov.dev.

HTTPS через Let’s Encrypt

Для защиты трафика необходимо настроить HTTPS. Самый удобный способ - использовать Let’s Encrypt, который позволяет бесплатно выпустить TLS-сертификат.

Устанавливаем certbot и плагин для Nginx:

1
sudo apt install certbot python3-certbot-nginx -y

Запрашиваем сертификат для поддомена:

1
sudo certbot --nginx -d weather.abykov.dev

После выполнения команда certbot автоматически:

  • Обновит конфигурацию Nginx;
  • Добавит директиву listen 443 ssl;
  • Пропишет пути к сертификату и ключу;
  • Настроит редирект с HTTP → HTTPS.

Теперь сайт доступен по защищенному протоколу: https://weather.abykov.dev.

Сертификат Let’s Encrypt действует 90 дней, но certbot настраивает автоматическое продление. Проверить можно так:

1
sudo systemctl status certbot.timer

⚠️ Даже если у основного домена (abykov.dev) уже был установлен SSL-сертификат (например, AlphaSSL), он не распространяется автоматически на поддомены. Для weather.abykov.dev требуется отдельный сертификат или wildcard-сертификат (*.abykov.dev). В нашем случае был использован Let’s Encrypt, так как это бесплатное и удобное решение.

Автоматический деплой через GitHub Actions

Развернутое вручную приложение решает задачу, но требует постоянного копирования JAR-файла и перезапуска сервиса вручную при каждом обновлении кода. Это неудобно и не подходит для продуктивной разработки.

Лучшее решение - настроить CI/CD-пайплайн на GitHub Actions, который будет:

  • Собирать проект при каждом пуше в ветку main;
  • Передавать собранный JAR на VPS по SSH;
  • Перезапускать systemd-сервис.

Конфигурация GitHub Actions

Для этого в проекте создаем файл .github/workflows/deploy.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
name: Deploy to VPS

on:
  push:
    branches:
      - main   # Автодеплой будет запускаться только при пуше в main

jobs:
  deploy:
    runs-on: ubuntu-latest   # GitHub Actions будет использовать виртуалку с Ubuntu

    steps:
      # 1. Клонируем репозиторий
      - name: Checkout code
        uses: actions/checkout@v4

      # 2. Устанавливаем JDK 21 (нужен для сборки Spring Boot проекта)
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      # 3. Собираем проект через Maven (без тестов для ускорения)
      - name: Build with Maven
        run: mvn -B clean package -DskipTests

      # 4. Проверяем, что index.html попал в JAR (отладочный шаг)
      - name: Verify index.html inside JAR
        run: |
          echo "Check if index.html is inside JAR..."
          jar tf api/target/api-0.0.1-SNAPSHOT.jar \
            | grep index.html || echo "index.html not found!"
          echo "----- index.html content -----"
          unzip -p api/target/api-0.0.1-SNAPSHOT.jar \
            BOOT-INF/classes/templates/index.html \
            || echo "No index.html inside JAR"
          echo "------------------------------"

      # 5. Копируем JAR на VPS по SSH
      - name: Copy JAR to VPS
        uses: appleboy/scp-action@v0.1.7
        with:
          host: $        # IP адрес VPS
          username: $    # Пользователь
          key: $      # SSH ключ из GitHub Secrets
          source: "api/target/api-0.0.1-SNAPSHOT.jar"
          target: "/opt/weather-demo/"
          overwrite: true

      # 6. Перемещаем JAR в правильное место на сервере
      - name: Move JAR into place
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: $
          username: $
          key: $
          script: |
            mv /opt/weather-demo/api/target/api-0.0.1-SNAPSHOT.jar \
               /opt/weather-demo/weather-demo.jar

      # 7. Перезапускаем systemd-сервис
      - name: Restart service
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: $
          username: $
          key: $
          script: |
            sudo systemctl stop weather-demo || true
            sudo systemctl start weather-demo
            sudo systemctl status weather-demo --no-pager

Подготовка SSH-ключей для GitHub Actions

Для безопасного копирования артефактов на сервер через GitHub Actions используется аутентификация по SSH-ключам. Нам понадобится сгенерировать пару ключей: приватный (для GitHub) и публичный (для VPS).

Генерация ключей

На локальной машине выполним:

1
ssh-keygen -t ed25519 -C "github-actions" -f github_actions

Здесь:

  • -t ed25519 - современный и безопасный алгоритм;
  • -C "github-actions" - комментарий, чтобы было понятно, для чего ключ;
  • -f github_actions - имя файлов (получатся github_actions и github_actions.pub).

В результате будут созданы два файла:

  • github_actions - приватный ключ (его кладем в GitHub Secrets);
  • github_actions.pub - публичный ключ (его добавляем на VPS).

Установка ключа на VPS

Подключаемся к серверу:

1
ssh root@<IP_сервера>

И добавляем публичный ключ в файл ~/.ssh/authorized_keys:

1
2
cat github_actions.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Теперь GitHub Actions сможет подключаться по SSH без пароля.

Secrets в GitHub

Чтобы pipeline имел доступ к VPS, необходимо добавить следующие секреты в настройках репозитория GitHub (SettingsSecrets and variablesActions):

  • VPS_HOST - IP-адрес сервера;
  • VPS_USER - пользователь для подключения;
  • VPS_SSH_KEY - приватный SSH-ключ для доступа.

После этого пайплайн сможет без проблем подключаться к серверу и деплоить обновления.

Результат

После всех настроек, при каждом коммите в ветку main GitHub Actions автоматически соберет JAR, загрузит его на VPS в /opt/weather-demo/, перезапустит сервис weather-demo.

Таким образом, весь деплой сводится к одному действию - git push.

Дополнительно: уведомления о деплое

После настройки CI/CD полезно получать уведомления о том, успешно ли прошел деплой. GitHub Actions поддерживает интеграции с популярными мессенджерами (Slack, Telegram, Matrix и др).

Пример: уведомления в Matrix

1
2
3
4
5
6
7
8
9
      - name: Notify Matrix room
        uses: matrix-org/matrix-message-action@v1
        with:
          homeserver: "https://matrix.org"
          access_token: $
          room_id: "!yourRoomId:matrix.org"
          message: |
            ✅ Deploy completed successfully on weather.abykov.dev

Здесь:

  • MATRIX_ACCESS_TOKEN хранится в GitHub Secrets и генерируется в Matrix (для бота или пользователя).
  • room_id - уникальный идентификатор комнаты, его можно найти в настройках Matrix.

Другие варианты

Таким образом, можно получать уведомления о каждом деплое прямо в рабочий чат.

This post is licensed under CC BY 4.0 by the author.