Dockerizing multiple web applications hosted on EC2: Optimizing container size, and handling ports

Viewed 180

My employer is interested in using Docker to containerize their web applications. We use AWS EC2 to host our applications, which runs Tomcat serving multiple WARs. I'm new to Docker and need to figure out how best to handle this.

As I understand, it's not proper to have a single Docker container running multiple web applications. So for each application I should build a container from a Tomcat base image, and just copy the WAR to the tomcat/webapps directory, like so in the Dockerfile:

FROM tomcat:9.0.44-jdk11-openjdk

COPY ./target/my-app-1.00-SNAPSHOT.war $CATALINA_HOME/webapps/my-app.war  

Simple enough, works fine. But the Tomcat image is 440MB. So if we have Tomcat currently running 5 WAR files that are each 50 MB or so, and each one gets its own container running Tomcat, we're looking at a tenfold increase in size for each application. That seems...really inefficient. Is this really the cost of using Docker containers? How can I minimize this?

I'm also not sure how to handle ports. Right now port 8080 is mapped to subdomain.mydomain.com. But each container will obviously be running on a different port. So let's say I launch a container for my-app-1 on port 8080, and a container for my-app-2 on 8081. How can I allow users to simply navigate to subdomain.mydomain.com/my-app-1, and subdomain.mydomain.com/my-app-2?

2 Answers

Disk storage

A Docker image is basically a list of layers (cf. specification) mounted using a union filesystem (usually overlay2), which greatly reduces the amount of physical storage needed. The lower layers are read-only and can be shared between containers.

For 5 different applications you'll just need 50 * 5 + 440 MiB. The only difference with your current configuration is about 50 MiB of the Debian image (cf. repo info). You can reduce it further by exporting the common libraries used in your applications to Tomcat's common classloader and creating a Docker image shared by the applications: every Tomcat will have a single application anyway, so isolation between applications is guaranteed.

Memory

The main difference with your current configuration is memory consumption: you will have 5 JVMs running + Tomcat's libraries.

Network configuration

Internally every container will listen to the same port, but on a different private address (attached to a virtual network device, cf. networking). Docker can configure a port forwarding (NAT) from a port on the host system to the docker container, but that is useful only if you want to access directly a service running in a container from the Internet.

In your case you'll rather need to run a reverse proxy on the host system like NGINX or Apache Http Server. This will provide the equivalent of a union filesystem for your URL namespace.

The choice of reverse proxy is a matter of taste:

  • Apache2 can use the AJP protocol (through mod_jk or the more recent mod_proxy_ajp modules), which will forward every detail of the client connection to Tomcat without any configuration.
  • NGINX is smaller and only talks HTTP, so you'll need to configure a RemoteIpValve and SSLValve.

However AJP is slowly being abandoned by Tomcat in favor of plain HTTP (cf. this talk).

Totally agree with @Piotr P. Karwasz. Just wanted to point out that since now you have to manage multiple containers, you might want to utilise docker-compose:

version: '3.3'
services:
  app1:
    build: ./app1 # expose nothing, reverse proxy will have access to the host via docker-internal vnet
  app2:
    build: ./app2 # expose nothing, reverse proxy will have access to the host via docker-internal vnet
  proxy:
    image: nginx 
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro # config is pretty minimal, but I'll include it below for completeness
    ports:
      - 8081:80 # nginx entry point
# nginx config
worker_processes  2;

events {
    use           epoll;
    worker_connections  128;
}

http {        
    server {
        server_name   localhost;
        listen        0.0.0.0:80;
        
        location /app1 {
            proxy_pass             http://app1:8080/my-app/;
        }
        
        location /app2 {
            proxy_pass             http://app2:8080/my-app/;
        }
    }
}

then each app would have its own subdirectory and a Dockerfile as you have outlined

A bit more on reverse proxies

Apart from NGINX, you might want to consider Traefik. It provides interesting features like container auto-discovery and Let's Encrypt support:

  proxy:
    image: traefik
    command: --api.insecure=true --providers.docker
    ports:
      - 8081:80 # Traefik entry point
      - 8080:8080 # management UI, you probably don't want this exposed in production
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
Related