From field experience, I must affirm that one of the greatest and stable combinations I've seen is Java Application Servers + Reverse Proxies. Although some of the functionality is a clear overlap, I tend to put reverse proxies in front of application servers for the following reasons (please see this NGINX page for more details):
- Load balancing: The reverse proxy acts as a traffic cop and could be used as an API gateway for clustered instances/backing services
- Web acceleration: Most of our modern applications use SPA frameworks, hence it is worth caching all the JS/CSS/HTML files and freeing the application server from that responsibility
- Security: Most HTTP requests could be intercepted by the reverse proxy before any attempt against the application server, increasing the opportunity to define rules
- SSL Management: It is easier to install/manage/deploy OpenSSL certificates in Apache/NGINX compared to Java KeyStores. Besides this, Let's Encrypt officially supports NGINX with plugins.
Requirements
To demonstrate this functionality, this tutorial combines the following stack in a classic (non-Docker) way, though most of the concepts could be useful for Docker deployments: