Back to publications

Habr

Deploying a Spring Boot Application with Nginx, Let's Encrypt and Docker Compose

Step-by-step guide to deploying a Spring Boot application with Nginx, Let's Encrypt SSL and Docker Compose on an Ubuntu server

7 min read 31K readers
JavaSpring BootNginxDockerDevOps
Read on Habr ↗ GitHub Repository ↗

Introduction

Hello, Habr! In my first article, I would like to share my experience deploying a Spring Boot application. But first, a small digression that should answer the questions of why and what for.

Recently, I faced the task of developing a Telegram bot. It may seem simple enough. But there was a problem: I had not previously dealt with deploying a project, and I had many questions about obtaining an SSL certificate because the Telegram API works only over HTTPS. Unfortunately, after a long search, I still could not find an article that answered all my questions, so the deployment process dragged on because I had to collect the material piece by piece.

You can find the repository with the final project on GitHub. For convenience, I made three branches; their purpose will become clear after you read the article.

Preparing the server

For my tests, I used the simplest Ubuntu cloud server from Timeweb.

The first thing we need to do is prepare the server, namely:

  • Create a new user with administrator privileges
  • Install Docker and Docker Compose
  • Install git and authenticate

Cloning the application

For testing, I made a simple Spring Boot application and, to make things more interesting, used PostgreSQL + Flyway instead of H2.

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

In this project, you can find a prepared Dockerfile. It is very simple, without multistage builds, but we do not need anything more for a simple test project.

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"]

Configuring the web server

We will use Nginx, so let us set up a minimal configuration and move on.

mkdir -p nginx/conf.d
vim nginx/conf.d/app.conf

And write the server block:

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;
    }
}

Do not forget to replace domen.ru with your real domain.

Database initialization

Create an init.sql file in the init directory:

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

Open the docker-compose.yml file:

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

Do not forget to change the domain name domen.ru to your own and replace the email address as well.

Create a .env file:

vim .env
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

Ready, steady, go!

docker-compose up -d --build

If everything went according to plan, visiting http://domen.ru/person will show the response from our application.

Useful debugging commands:

  • docker-compose logs service_name — read logs
  • docker system df — view used disk space
  • docker container ls -a / docker rm <container_id> — view or clean containers
  • docker image ls -a / docker rmi <image_id> — view or clean images
  • docker system prune — remove everything Docker has accumulated

Getting an SSL certificate

Check that the credentials are mounted correctly:

docker-compose exec nginx ls -la /etc/letsencrypt/live

If everything went well, you will see the files README and domen.ru.

Change the --staging flag in docker-compose.yml to --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

Restart certbot:

docker-compose up --force-recreate --no-deps certbot

Final configuration

Stop nginx:

docker-compose stop nginx

Generate a Diffie-Hellman key:

mkdir dhparam
sudo openssl dhparam -out ./dhparam/dhparam-2048.pem 2048

Update the nginx configuration (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;
}

Add port 443 and the dhparam volume to 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

Final command:

docker-compose up -d --force-recreate --no-deps nginx

What is worth knowing

SSL certificates from Certbot must be renewed every 90 days, and preferably every 60 days. Certbot does not like being bothered more than five times per week.

The renewal process can be automated with 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

Conclusion

I hope this article saved you some time! For me, it was an interesting experience: after all, it was my first article rather than dry notes in a README for the future. I will be glad to hear your advice in the comments.