Three Ways to Run Your Java Locally with HTTPS

If you’re developing a web application, chances are you want your dev environment as close to production as possible. One of the often-overlooked differences between local development and production servers is the use of Transport Layer Security (TLS), or Hypertext Transfer Protocol Secure (HTTPS). In this post, I’ll cover three different options to get your local Java app running with TLS in no time!

Start with a simple Java application

To test out these options, you will, of course, need a Java web application. If you already have one handy, skip this step!

Bootstrap a new application using the Spring Initializer. Clicking on that link will automatically add the web and devtools dependencies.

  1. Download the project using the Generate button.

  2. Extract the zip file.

  3. Open the project with your favorite IDE.

The last thing to do is to add a basic RestController in src/main/java/com/example/demo/HelloController.java:

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/")
    public String sayHello() {
        return "Yes, this is another Hello World...";
    }
}

That’s it! Start the application either from your IDE or by running ./mvnw spring-boot:run.

Now access this boring, non-secure application in your browser at`\http://localhost:8080/`.

HTTPS using an internet proxy

One of the quickest ways to get a local application running with TLS is to use an internet proxy like ngrok. Download and install ngrok and then start up the service using:

ngrok http 8080

This command will expose your local service running port 8080 over the internet on a generated URL such as https://abcd-11-22-33-44.ngrok.io.

If you don’t want to install ngrok, you could use localhost.run, which works using SSH port forwarding. Try it out with:

ssh -R 80:localhost:8080 nokey@localhost.run

Similar to ngrok, a temporary URL will be generated. For example, https://152ccb7ac9369e.lhr.life.

The goal of these tools is to provide a publicly available URL for your local development, which is perfect if you want to share something via the internet. If you don’t want to share, it’s best not to expose your development machine to any potential attackers online. The following two options avoid this issue.

Create a certificate authority with mkcert

The next two options use a tool called mkcert, which sets up a locally trusted certificate authority (CA), installed into the trust stores on your computer. Any certificates issued by this CA will be trusted by the client of your choice (Chrome, Firefox, curl, etc.).

On macOS, you can install mkcert with Homebrew; for other operating systems you can find instructions in the mkcert docs.

brew install mkcert nss
# nss is only needed if you are using Firefox

Create and install the certificate authority:

mkcert -install
Created a new local CA 💥
Sudo password:
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in Java's trust store! ☕️

If you are on macOS and using HTTPie, you will need to add the CA to HTTPie manually:

cat "$(mkcert --CAROOT)/rootCA.pem" >> \
$(brew --prefix)/Cellar/httpie/*/libexec/lib/python*/site-packages/certifi/cacert.pem

That was mostly painless, now let’s secure the application you just created!

Reverse proxy with Nginx

Many production applications terminate TLS using a reverse proxy like Nginx. This can also be an excellent option for local development and has the added advantage of working with any web application framework.

To run Nginx, I’ll use the official Nginx Docker image. To keep things organized, create a new directory to hold the configuration:

mkdir localhost-tls
cd localhost-tls

Next, use mkcert to generate the certificates Nginx will use.

mkdir devcerts
mkcert -key-file devcerts/key.pem -cert-file devcerts/cert.pem localhost

This command generates a private key and certificate in the new devcerts directory for the host, localhost.

Create an nginx-default.conf.template configuration file:

server {
    listen 80;
    server_name _;
    rewrite ^/(.*)$ https://$host$request_uri? permanent; (1)
}

server {
  server_name _;

  location / {
    proxy_pass http://host.docker.internal:${TARGET_PORT}; (2)

    (3)
    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-Host $host:443;
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Proto https;
  }

  listen 443 ssl;
  ssl_certificate /etc/nginx/certs/cert.pem;
  ssl_certificate_key /etc/nginx/certs/key.pem;
}
1 Redirect all http requests to https.
2 Windows and Mac hosts run Docker in a VM, the hostname host.docker.internal is used to point back to the "host" OS and the port.
3 Set the proxy headers, so the downstream app will know what URL the client is using.

Tie it all together with Docker

Now that you have your Java application, the certificates, and the Nginx configuration ready to go, the only thing left is to start it up!

Use docker run to start Nginx with the above files as volumes that configure the service.

docker run \
    -v $(pwd)/nginx-default.conf.template:/etc/nginx/templates/default.conf.template \ (1)
    -v $(pwd)/devcerts:/etc/nginx/certs \ (2)
    -p 443:443 \
    -p 80:80 \
    --env TARGET_PORT=8080 \ (3)
    nginx
1 The Nginx configuration file template.
2 The path to the certificates.
3 The port where your application is running on localhost.
You can codify this solution using a docker-compose.yml file if you prefer!

Test it out! Open up your browser to https://localhost/.

Configure Spring Boot to use HTTPS

The previous options used a separate service to handle TLS; this time, I’ll configure Spring Boot’s embedded Tomcat server to terminate TLS directly.

Generate another set of certificates with mkcert. This time set the output format to pkcs12, which is a format the Java KeyStore understands.

# create the directory
mkdir -p ~/.config/spring-boot

# generate a certificate in pkcs12 format
mkcert -pkcs12 -p12-file ~/.config/spring-boot/local-tls.p12 localhost

Create a properties file ~/.config/spring-boot/spring-boot-devtools.properties containing the server port and newly generated keystore location.

# Set the port
server.port=8443

# configure the key store path
server.ssl.key-store=${user.home}/.config/spring-boot/local-tls.p12

Now restart your Spring Boot application, and open your browser to https://localhost:8443 to access your newly secured application!

I didn’t need to make any changes to my application because it’s using Spring Boot DevTools, which automatically includes the above properties file. If you are not using DevTools, you can mimic this behavior by setting an environment variable before starting your application:

SPRING_CONFIG_IMPORT='${user.home}/.config/spring-boot/spring-boot-devtools.properties'

Bonus: Use a different hostname

The above examples use localhost, which works, but you might want to use a different hostname. One easy way to do this is to add a hostname alias to your /etc/hosts file:

sudo sh -c 'echo "127.0.0.1 local.example" >> /etc/hosts'

In the above examples, when generating certificates with mkcert, replace localhost with your domain name. Here’s an example:

mkcert -key-file devcerts/key.pem -cert-file devcerts/cert.pem local.example
Use a domain name that you own or that is NOT in the official top-level domain list to ensure there are no conflicting or hijacked DNS entries.

Potential problems using HTTPS for local development

It’s not all sunshine and roses. If you are a developer that works on multiple applications there are a few things to watch out for. Your browser may automatically switch to HTTPS if you are switching between applications that run on HTTP and HTTPS prototypes, especially if one of your applications sets the HTTP Strict-Transport-Security (HTST) headers. If you run into this type of problem you can clear the SSL State and/or HSTS settings in your browser.

Use a different hostname for each application you are working on to avoid browser caching.

Learn more about building secure Java applications

This post has demonstrated three options for running applications locally using TLS/HTTPS. Each option has its own strengths and weaknesses.

  • Using an Internet proxy exposes your application to the world; however, this could be what you want if you are testing web hooks.

  • Running a reverse proxy will work for any application, but it’s one more service to manage (…​and remember to start 😉 ).

  • Running directly in the Spring Boot application, just works™️ once it’s set up. However, it cannot use the default HTTPS port (443) without some other workaround, like (iptables, authbind, setcap, etc.).

Now that your application is running securely with TLS, check out these posts to learn how to add secure user authentication!

If you have questions, please leave a comment below. If you liked this tutorial, follow @oktadev on Twitter, follow us on LinkedIn, or subscribe to our YouTube channel.

Brian Demers is a Developer Advocate at Okta and a PMC member for the Apache Shiro project. He spends much of his day contributing to OSS projects in the form of writing code, tutorials, blogs, and answering questions. Along with typical software development, Brian also has a passion for fast builds and automation. Away from the keyboard, Brian is a beekeeper and can likely be found playing board games. You can find him on Twitter at @briandemers.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.