Установка и развёртка за nginx

От чистого Ubuntu до работающего HTTPS-сайта: сборка, PostgreSQL, внешняя конфигурация, systemd, nginx и обязательные правки настроек по умолчанию.

Инструкция рассчитана на Ubuntu 22.04/24.04 или Debian 12 и одну машину, на которой живут и приложение, и база, и nginx. Для другого дистрибутива меняются только имена пакетов.

Внимание

Настройки в application.properties в репозитории — девелоперские. Там путь к файлам с чужой рабочей машины, пароль администратора в открытом виде и порт 80, который конфликтует с nginx. Раздел «Что обязательно поменять» — не рекомендация, а условие работоспособности.

Итоговая схема#

сервер интернет :80 → :443 nginx TLS-сертификат, gzip, лимит размера загрузки, два location proxy_pass / alias /media/ 127.0.0.1:8080 systemd: otryadwebsite.service отдача файлов напрямую Java не участвует PostgreSQL 127.0.0.1:5432 контент properties.json в рабочем каталоге настройки и SEO /var/lib/otryadwebsite/media/ запись — приложением, чтение — nginx фото и видео
Приложение слушает только localhost. Наружу смотрит один nginx.

1. Зависимости#

sudo apt update && sudo apt install -y openjdk-21-jdk-headless postgresql nginx git

Spring Boot 4 требует Java 17 как минимум; в pom.xml указано <java.version>17</java.version>, но собирать и запускать можно на любой более новой LTS — Java 21 предпочтительнее из-за производительности сборщика мусора. Проверьте, что версия действительно подхватилась:

java -version

2. База данных#

sudo -u postgres psql -c "CREATE USER otryad WITH PASSWORD 'ЗАМЕНИТЕ_МЕНЯ';"
sudo -u postgres psql -c "CREATE DATABASE otryadwebsite OWNER otryad;"

Схему создавать руками не нужно: в проекте стоит spring.jpa.hibernate.ddl-auto=update, и Hibernate сам построит таблицы при первом запуске.

Осторожно

ddl-auto=update умеет добавлять столбцы и таблицы, но никогда не удаляет и не сужает существующие. После обновлений, которые переименовывают поля, в базе останутся старые столбцы — это безопасно, но за схемой нужно следить руками. Перед каждым обновлением делайте pg_dump.

3. Сборка#

Обратите внимание: в репозитории Maven-проект лежит во вложенной папке OtryadWebsite/, а не в корне. Собирать нужно оттуда.

git clone https://github.com/Electronprod/OtryadWebsite.git /tmp/otryad-src
cd /tmp/otryad-src/OtryadWebsite && ./mvnw -B clean package -DskipTests

Результат — target/OtryadWebsite-1.0.jar, исполняемый fat jar. Lombok и DevTools в него не попадают: первый исключён в настройках spring-boot-maven-plugin, второй помечен optional и Spring Boot не упаковывает его в собранный архив.

Раскладываем по местам:

sudo useradd --system --home /var/lib/otryadwebsite --shell /usr/sbin/nologin otryad
sudo mkdir -p /opt/otryadwebsite /var/lib/otryadwebsite/media /etc/otryadwebsite
sudo cp target/OtryadWebsite-1.0.jar /opt/otryadwebsite/app.jar
sudo chown -R otryad:otryad /var/lib/otryadwebsite

4. Что обязательно поменять#

Файл application.properties внутри jar трогать не нужно — Spring Boot подхватывает внешний файл и он перекрывает встроенный. Создайте /etc/otryadwebsite/application.properties:

# --- порт: 80 занят nginx ---
server.port=8080
server.address=127.0.0.1

# --- за обратным прокси: иначе host в sitemap и JSON-LD будет неправильным ---
server.forward-headers-strategy=native

# --- файлы ---
system.storage.path=/var/lib/otryadwebsite/media/
web.storage.url-prefix=/media/
web.storage.local-serving=false

# --- размеры загрузки: согласуйте с client_max_body_size в nginx ---
spring.servlet.multipart.max-file-size=2GB
spring.servlet.multipart.max-request-size=2GB

# --- база ---
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/otryadwebsite
spring.datasource.username=otryad
spring.datasource.password=ЗАМЕНИТЕ_МЕНЯ

# --- первый администратор: используется только если админов в базе ещё нет ---
security.admin.login=ЗАМЕНИТЕ_МЕНЯ
security.admin.password=ЗАМЕНИТЕ_МЕНЯ8

# --- выключить девелоперский переключатель ---
dev=false

Что здесь важно и почему:

ПараметрВ репозиторииПочему нужно менять
server.port${SERVER_PORT:80}80 занят nginx — приложение не поднимется
system.storage.pathC:/Users/Electron/Downloads/путь с рабочей машины разработчика
web.storage.local-servingtrueпри true файлы отдаёт Spring, а не nginx; вся выгода от прокси теряется
server.forward-headers-strategyзакомментированбез него приложение считает себя доступным по http://127.0.0.1:8080, и этот адрес попадает в sitemap.xml, og:url и JSON-LD
security.admin.passwordnadezda123пароль администратора по умолчанию лежит в публичном репозитории
spring.servlet.multipart.max-file-size10GBвместе с лимитом nginx определяет реальный максимум; 10 ГБ в одном запросе почти всегда ошибка
devtrueфлаг девелоперского режима — в продакшене ему нечего делать включённым

Важно

Пароль администратора должен пройти валидацию сущности Person: минимум 8 символов, хотя бы одна латинская буква и хотя бы одна цифра. Иначе приложение стартует, но создать первого админа не сможет и упадёт с ошибкой валидации. Пара security.admin.* читается только когда в таблице users нет ни одного пользователя с ролью ROLE_ADMIN — после первого запуска менять её бессмысленно, пароль меняется через панель.

5. systemd#

Критичная деталь: настройки сайта хранятся в файле properties.json, и путь к нему в коде относительный — файл создаётся в текущем рабочем каталоге процесса. Поэтому WorkingDirectory обязателен: без него файл окажется в / или в каталоге, откуда служба была запущена, и после переезда все настройки, SEO и короткие ссылки «исчезнут».

/etc/systemd/system/otryadwebsite.service:

[Unit]
Description=OtryadWebsite
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
User=otryad
Group=otryad
WorkingDirectory=/var/lib/otryadwebsite
ExecStart=/usr/bin/java -Xmx512m -jar /opt/otryadwebsite/app.jar \
  --spring.config.additional-location=file:/etc/otryadwebsite/
Restart=on-failure
RestartSec=5
SuccessExitStatus=143

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=/var/lib/otryadwebsite

[Install]
WantedBy=multi-user.target

Права на файл с паролями и запуск:

sudo chmod 640 /etc/otryadwebsite/application.properties && sudo chown root:otryad /etc/otryadwebsite/application.properties
sudo systemctl daemon-reload && sudo systemctl enable --now otryadwebsite

Проверка, что поднялось:

curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/

Логи — journalctl -u otryadwebsite -f. В них при первом запуске должно быть Administrator profile not found, creating a new one....

6. nginx#

/etc/nginx/sites-available/otryadwebsite:

server {
    listen 80;
    server_name example.ru www.example.ru;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.ru www.example.ru;

    ssl_certificate     /etc/letsencrypt/live/example.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.ru/privkey.pem;

    # должно быть не меньше spring.servlet.multipart.max-file-size
    client_max_body_size 2G;

    # приложение сжимает только html/xml/json — css, js и svg остаются на nginx
    gzip on;
    gzip_types text/css application/javascript image/svg+xml application/json;
    gzip_min_length 1024;

    # медиафайлы: отдаём с диска, Java не трогаем
    location /media/ {
        alias /var/lib/otryadwebsite/media/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
        try_files $uri =404;
    }

    # телеметрия наружу не нужна, хотя она и закрыта авторизацией
    location /actuator/ { deny all; }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        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;
        proxy_set_header X-Forwarded-Host  $host;

        # загрузка больших видео из панели
        proxy_request_buffering off;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

Включаем и проверяем конфигурацию:

sudo ln -s /etc/nginx/sites-available/otryadwebsite /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx

Сертификат:

sudo apt install -y certbot python3-certbot-nginx && sudo certbot --nginx -d example.ru -d www.example.ru

Почему именно так#

alias, а не root. Публичные адреса файлов формируются как web.storage.url-prefix + batchId + имя файла, то есть /media/<uuid>/01.jpg. На диске это <system.storage.path>/<uuid>/01.jpg. alias отбрасывает префикс /media/, root бы его сохранил и получилось бы /var/lib/otryadwebsite/media/media/<uuid>/01.jpg. Слэши на конце у location и alias обязательны — и оба, и одновременно.

Загрузка всё равно идёт через Java. nginx отдаёт медиа только на чтение. POST /dashboard/media/upload обрабатывает Spring, поэтому лимит размера нужен и в nginx (client_max_body_size), и в приложении (spring.servlet.multipart.*). Меньший из двух и будет реальным ограничением; при превышении лимита nginx вернёт 413.

X-Forwarded-* без forward-headers-strategy бесполезны. Приложение собирает абсолютные адреса через ServletUriComponentsBuilder — они попадают в sitemap.xml, в Open Graph и в JSON-LD. Без native там окажется внутренний адрес и схема http, и поисковики проиндексируют ссылки, которые никуда не ведут.

7. После установки#

  1. Зайдите на https://example.ru/auth и войдите парой из security.admin.*. Сразу же смените пароль в /dashboard/users и заведите отдельные учётные записи модераторам.
  2. Включите режим защиты в /dashboard, пока наполняете сайт: публичная часть закроется для всех, кроме сотрудников и владельцев ссылки с токеном.
  3. Замените домен в robots.txt — там прописан Sitemap: https://otryad-nadezhda.ru/sitemap.xml. Файл лежит в src/main/resources/static/, то есть правится в исходниках и требует пересборки. Альтернатива без пересборки — перехватить /robots.txt отдельным location в nginx.
  4. В /dashboard/seo пройдитесь по заголовкам и описаниям: значения по умолчанию написаны про отряд «Надежда».
  5. Проверьте https://example.ru/sitemap.xml — адреса внутри должны начинаться с вашего домена и https. Если нет, не применился server.forward-headers-strategy.

8. Резервное копирование#

Состояние сайта — это три независимых вещи, и потеря любой из них теряет контент:

#!/bin/sh
# /usr/local/bin/otryad-backup.sh
set -eu
DST=/var/backups/otryad/$(date +%F)
mkdir -p "$DST"
sudo -u postgres pg_dump otryadwebsite | gzip > "$DST/db.sql.gz"
cp /var/lib/otryadwebsite/properties.json "$DST/"
tar czf "$DST/media.tar.gz" -C /var/lib/otryadwebsite media

properties.json маленький, но восстановить его нельзя ничем: там SEO для всех страниц, короткие ссылки, контакты, соцсети и токен доступа.

9. Обновление версии#

cd /tmp/otryad-src && git pull && cd OtryadWebsite && ./mvnw -B clean package -DskipTests
sudo -u postgres pg_dump otryadwebsite | gzip > /var/backups/otryad-pre-upgrade.sql.gz
sudo systemctl stop otryadwebsite
sudo cp target/OtryadWebsite-1.0.jar /opt/otryadwebsite/app.jar
sudo systemctl start otryadwebsite

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

Частые проблемы#

СимптомПричина
Служба падает при старте, в логах Address already in useне переопределён server.port, приложение пытается занять 80
Все настройки сайта сбросились после перезапускане задан WorkingDirectoryproperties.json создался в другом каталоге
Картинки в альбоме не открываются, 404несогласованы system.storage.path и alias, либо потерян слэш на конце одного из них
Альбом открывается, но пустойв каталоге batchId нет файлов, кроме preview.jpg — из галереи он исключается всегда
В sitemap.xml адреса вида http://127.0.0.1:8080/...не применился server.forward-headers-strategy=native
Загрузка большого видео обрывается на 413client_max_body_size меньше размера файла
Загрузка обрывается на 500 при большом файлелимит nginx подняли, а spring.servlet.multipart.max-file-size нет
Вход в панель отдаёт 429 с JSONсработал лимит попыток: 5 в минуту с адреса, 5 в минуту на логин, 30 в сутки с адреса
/dashboard отдаёт 403 после входау пользователя роль не ROLE_ADMIN и не ROLE_MODERATOR