16.06, ООО «ВК»
16.06, ООО «ВК»
16.06, ООО «ВК»

Как я развернул сайт на Java Spring Boot + Angular SSR с Docker и Nginx: личный опыт

Аватар Александр Тюрин
Отредактировано

Реальный опыт развёртывания сайта на Spring Boot и Angular SSR с использованием Docker, Nginx и HTTPS. Ошибки, тонкости настройки и защита от ботов — всё по шагам.

445 открытий3К показов
Как я развернул сайт на Java Spring Boot + Angular SSR с Docker и Nginx: личный опыт

Проект представляет собой веб-приложение на Spring Boot и Angular. Первоначально я выбрал простую монолитную архитектуру, но по мере роста требований и целей пришлось перейти на микросервисный подход. Здесь я расскажу, как это происходило: с конфигурациями, ошибками и решениями.

Монолитная архитектура

Исходная схема:

  • Backend: Spring Boot
  • Frontend: Angular

Так как у меня уже был арендованный VDS, я зарегистрировал домен у одного известного провайдера, а хостил сайт уже на VDS сервере. Для простоты интегрировал Angular-сборку в Spring Boot как статику через frontend-maven-plugin. Всё собиралось в один JAR. Почему так? Ранее уже был такой опыт, и я решил пойти этим путем.

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

  • Отсутствие SSR в Angular негативно влияло на SEO;
  • Ошибка в Angular ломала Maven-сборку;
  • Сложно поддерживать и масштабировать.

Переход на микросервисы

Было принято решение разделить фронтенд и бэкенд на отдельные сервисы и использовать микросервисный подход с SSR для Angular:

Новая структура:

  • Backend: Spring Boot
  • Frontend: Angular Universal (SSR)
  • Инфраструктура: Docker Compose, Nginx, HTTPS

Docker, Nginx и запуск

Создана базовая структура docker-compose.yml:

			services:
  backend-service:
    image: amazoncorretto:17-alpine-jdk
    command: java -jar app.jar

  frontend-service:
    image: node:18-alpine
    command: node server/main.js

  nginx:
    image: nginx:alpine
    # Конфигурация проксирования
		

И соответствующий Nginx:

			server {
    server_name my-site.ru;

    location / {
        proxy_pass http://frontend-service:9002;
    }

    location /api {
        proxy_pass http://backend-service:9001;
    }
}
		

После запуска проект открывался по IP, но появилось множество проблем:

  • Дублирование API-префиксов: /api/api/endpoint → Решение: корректировка proxy_pass и URL в Angular
  • Редиректы на POST: Spring добавлял лишние слеши → Решение: правильные аннотации и настройка Nginx
  • 405 Method Not Allowed → Решение: использовать @PostMapping
  • Docker-сети → Решение: задать общую сеть в docker-compose.yml

Безопасность: HTTPS, SSL, CORS

Чтобы сайт открывался по имени с панели провайдера для моего домена, значения DNS-сервера оставил без изменений (провайдер предоставляет их бесплатно), а для ресурсной записи @ и www указал ip адрес своего хостинга. Так как провайдер домена бесплатно предоставил SSL-сертификаты (Let's Encrypt), то как я их интегрировал:

Скачал с панели провайдера и перенес на хостинг такие файлы:

domain.crt — сертификат

domain.key — приватный ключ

ca_bundle.crt — цепочка доверия

Добавил в Nginx такую запись для HTTPS:

			listen 443 ssl;
 ssl_certificate /etc/nginx/ssl/domain.crt;
 ssl_certificate_key /etc/nginx/ssl/domain.key;
 ssl_trusted_certificate /etc/nginx/ssl/ca_bundle.crt;
		

Но при обращении к сайту по https конечно же возникли ошибки. А именно: в логах Nginx были ошибки на SSL_CTX_use_PrivateKey_file. Оказалось формат был PKCS#7, а требовался PEM. Конвертировал через OpenSSL:

			openssl pkcs7 -print_certs -in domain.p7b -out domain.crt
		

Так как сертификаты имеют срок действия, не стоит забывать об их продлении и обновлении.

К тому же всегда следует проверять логи Nginx (error.log), там могут находится ответы на вопросы, почему сайт не работает.

На стороне Spring Boot были CORS-проблемы, решено так:

			@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/api/**")
                    .allowedOrigins("https://example.com");
        }
    };
}
		

Атаки ботов и защита

После запуска сайта в логах Nginx стали появляться тысячи запросов от ботов, ищущих уязвимости WordPress, PHPMyAdmin и других популярных утилит, хотя мой сайт работает на Java/Angular. Пример лога:

45.155.205.213 - - [01/Jan/2023:04:12:11 +0000] "GET /wp-login.php HTTP/1.1" 404 153

Добавлен фильтр в nginx.conf:

			location ~* ^/(wp-admin|wp-login|phpmyadmin|.env|config.php) {
    return 403;
}

if ($request_method ~ ^(TRACE|TRACK|DEBUG)) {
    return 405;
}
		

Финальная конфигурация

После всех настроек мой сайт работает и полностью функционирует. Конфигурация сильно изменена по сравнению с первоначальной. Вот как они выглядят сейчас.

docker-compose.yml

			version: '3.8'

networks:
  app-network:

services:
  backend-service:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: backend-service
    ports:
      - "9001:9001"
    volumes:
      - ./backend/logs:/app/logs
    env_file:
      - ./.env
    restart: always
    networks:
      - app-network

  frontend-service:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: frontend-service
    ports:
      - "9002:9002"
    environment:
      - NODE_ENV=production
    restart: always
    networks:
      - app-network

  proxy-service:
    image: nginx:alpine  	# Используем alpine для уменьшения размера
    container_name: proxy-service
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d:ro
      - ./ssl-certs:/etc/nginx/ssl:ro
      - ./nginx/logs:/var/log/nginx
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - backend-service
      - frontend-service
    restart: always
    networks:
      - app-network
		

nginx.conf

			worker_processes auto;

events {
    worker_connections 1024;
}

http {
    upstream frontend {
        server frontend-service:9002; # имя контейнера как в docker-compose
    }

    upstream backend {
        server backend-service:9001;
    }

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

    server {
        listen 443 ssl;
        server_name example.com www.example.com;

        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
	
	# Security headers
        add_header Strict-Transport-Security "max-age=31536000" always;

        location / {
            proxy_pass http://frontend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        location /api/ {
            proxy_pass http://backend/api/;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }

        location ~* ^/(wp-admin|wp-login|phpmyadmin|.env|config.php) {
            return 403;
        }

        if ($request_method ~ ^(TRACE|TRACK|DEBUG)) {
            return 405;
        }
    }
}
		

Dockerfile backend

			FROM amazoncorretto:17-alpine-jdk
WORKDIR /app
COPY my-spring-1.0.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
		

Dockerfile frontend

			FROM node:18-alpine AS runtime
WORKDIR /app
COPY browser /app/browser
COPY server /app/server
COPY node_modules /app/node_modules
EXPOSE 4000
CMD ["node", "/app/server/server.mjs"]
		

Разделение сервисов улучшило поддержку и масштабируемость, к тому же, я получил ценные знания. Надеюсь, статья будет полезна тем, кто разворачивает подобные конфигурации на Java и Angular.

Следите за новыми постами
Следите за новыми постами по любимым темам
445 открытий3К показов