DEV Community

Cover image for How I Deployed a Java Spring Boot + Angular SSR Website Using Docker and Nginx: A Personal Journey
Aleksandr Tiurin
Aleksandr Tiurin

Posted on

How I Deployed a Java Spring Boot + Angular SSR Website Using Docker and Nginx: A Personal Journey

This is a real-world story — with challenges, solutions, and valuable lessons that may benefit other developers or serve as a reference for myself in the future.

Introduction

The project is a web application built with Spring Boot and Angular. Initially, I went with a simple monolithic architecture, but as the requirements and ambitions grew, I had to switch to a microservices-based structure. Here's how it all happened — including configurations, issues, and solutions.

Monolithic Architecture

Initial setup:

  • Backend: Spring Boot
  • Frontend: Angular I already had a rented VDS, so I registered a domain via a well-known provider and decided to host the site on that VDS. To keep things simple, I integrated the Angular build into the Spring Boot resources using frontend-maven-plugin, bundling everything into a single JAR file.

Why? I had done it before and it worked, so I chose the familiar path.

After registering the domain (which takes some time to propagate), I began configuring and testing the project. But during testing, major drawbacks surfaced:

  • No SSR in Angular → SEO was negatively impacted
  • Fragile build: Angular errors broke Maven build
  • Maintenance and scalability became difficult

Moving to Microservices

I decided to split the frontend and backend into separate services and introduce Angular Universal for SSR. Here's the updated architecture:

  • Backend: Spring Boot
  • Frontend: Angular Universal (SSR)
  • Infrastructure: Docker Compose, Nginx, HTTPS

Docker, Nginx and Initial Launch

Base docker-compose.yml setup:

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

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

  nginx:
    image: nginx:alpine
    # Proxy configuration
Enter fullscreen mode Exit fullscreen mode

Initial Nginx config:

server {
    server_name my-site.ru;

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

    location /api {
        proxy_pass http://backend-service:9001;
    }
}
Enter fullscreen mode Exit fullscreen mode

The site started and worked via IP, but there were many issues:

  • Duplicated API prefixes (/api/api/endpoint) → Fixed proxy_pass and Angular API base URL
  • Redirects on POST requests due to Spring adding extra slashes → Fixed via correct controller annotations and Nginx tuning
  • 405 Method Not Allowed → Fixed by explicitly using @PostMapping
  • Docker networking issues → Resolved by defining a shared network in docker-compose.yml

Security: HTTPS, SSL, CORS

To make the site accessible via domain name, I configured DNS records through my provider (they offer free DNS). I pointed @ and www to the VDS IP address.

The domain provider also offered free SSL certificates (Let's Encrypt). Here's how I set them up: downloaded from the provider panel:

  • domain.crt — certificate
  • domain.key — private key
  • ca_bundle.crt — certificate chain

Nginx HTTPS configuration:

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;
Enter fullscreen mode Exit fullscreen mode

However, HTTPS failed to work initially. The Nginx logs showed SSL_CTX_use_PrivateKey_file errors. The issue? The certificate was in PKCS#7 format, but Nginx requires PEM. I converted it using OpenSSL:

openssl pkcs7 -print_certs -in domain.p7b -out domain.crt
Enter fullscreen mode Exit fullscreen mode

Also, since certificates expire, don't forget to renew them. Eventually, I plan to automate this (e.g. with Certbot).

On the Spring Boot side, CORS errors were solved like this:

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

Bot Attacks and Protection

Soon after launching the site, Nginx logs filled with automated bot requests probing for WordPress, PHPMyAdmin, etc. Example:
45.155.205.213 - - [01/Jan/2023:04:12:11 +0000] "GET /wp-login.php HTTP/1.1" 404 153
Nginx protection rules:

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

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

Final Configuration

After several iterations, here’s what my current setup looks like:

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
    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
Enter fullscreen mode Exit fullscreen mode

nginx.conf

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    upstream frontend {
        server frontend-service:9002;
    }

    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;

        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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

backend Dockerfile

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

frontend Dockerfile

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"]
Enter fullscreen mode Exit fullscreen mode

Conclusion

The website is now fully up and running. Splitting services made it more maintainable and scalable. Hopefully, this post will help others going down a similar path with Java and Angular.

Top comments (0)