Angular + Spring + Docker

1. Caddy version (caddy version):

caddy:2.2.1-alpine

2. How I run Caddy:

with docker compose (see below):

a. System environment:

Using Docker (compose)

b. Command:

docker-compose -f docker-compose-caddy-angular.yml up -d

c. Service/unit/compose file:

version: "3.7"

services:

  duckdb:
    image: mysql:latest
    container_name: duckdb
    volumes:
      - duck:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_USER: ****
      MYSQL_ROOT_PASSWORD: *****
      MYSQL_DATABASE: duckdb

  spring-app:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    depends_on:
      - duckdb
    environment:
      - SPRING_PROFILES_ACTIVE=prod

  angular-ui:
    build: ../chuck
    container_name: chuck
    ports:
      - 4200:4200

  # Run the caddy server
  caddy:
    image: caddy/caddy:2.2.1-alpine
    container_name: caddy-service
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - $PWD/Caddyfile:/etc/caddy/Caddyfile
      - $PWD/site:/srv
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
  duck:

d. My complete Caddyfile or JSON config:

kvasen.xyz {
    reverse_proxy spring-app:8080  {
      header_down Strict-Transport-Security max-age=31536000;
    }

    encode zstd gzip

    file_server

    root * /app/dist/chuck

    rewrite * /app/dist/chuck/index.html
}

3. The problem I’m having:

Originally I tried Caddy because I was having too much trouble configuring Nginx with Let’s encrypt. It worked great when I tried it only with the Spring Api, my Caddyfile being:

kvasen.xyz {
    reverse_proxy spring-app:8080  {
      header_down Strict-Transport-Security max-age=31536000;
    }
}

I correctly get the exposed endpoints and their data.
Next step is adding a client (Angular). But I am missing something somewhere and there aren’t many examples with Docker. My Dockerfile for Angular is:

FROM node:lts-alpine as node
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build --configuration=prod

#stage 2
FROM caddy/caddy:2.2.1-alpine
COPY --from=node /app/dist/chuck /srv

# old stage 2 with NGINX
#FROM nginx:stable-alpine
#COPY --from=node /app/dist/chuck /usr/share/nginx/html
#EXPOSE 80
#EXPOSE 443
#CMD ["nginx", "-g", "daemon off;"]

4. Error messages and/or full log output:

5. What I already tried:

Tried different ways of “reaching” the angular code, but before adding:

 rewrite * /app/dist/chuck/index.html

I always got the api responses, and after adding the rewrite I get 403 everywhere.

Probably a working example of an Angular + any Backend in a Docker environment would be enough as an answer…

6. Links to relevant resources:

Thoughts: Is there a problem? Your code in /srv but your docroot points to root * /app/dist/chuck?

You may need a matcher for reverse_proxy to route some requests to the proxy but others from the local file_server.

See this example from the api platform, especially the @pwa matcher.

1 Like

May be to be more specific of what is not clear:

  • From what I’ve seen, it would be a good idea to change my Api to always have a specific path like “/api/” in front of my endpoints and then change re reverse_proxy to
reverse_proxy /api/* spring-app:8080 {
  • The root should point to /srv and the rewrite to /srv/index.html in order to reach my Angular home page?
  • Do I need to add anything else to the Dockerfile for Angular?
  • Do I need these ports for Angular in the docker-compose file?

That’s a pretty old version at this point. Please upgrade to v2.4.5!

In this config, you’re not using any request matchers, so that means all the handlers will try to handle the requests unconditionally.

When the Caddyfile is parsed, Caddy will sort the directives according to this predetermined directive order, so in-order, your config would actually be executed top-down like this, after the sorting step happens:

kvasen.xyz {
    root * /app/dist/chuck

    rewrite * /app/dist/chuck/index.html

    encode zstd gzip

    reverse_proxy spring-app:8080  {
      header_down Strict-Transport-Security max-age=31536000;
    }

    file_server
}

So basically, the rewrite will always happen, rewriting any request path to /app/dist/chuck/index.html.

But the first actual request handler directive that will be hit is the reverse_proxy, so it will try to ask your spring-app to handle a request to /app/dist/chuck/index.html, which it most likely will not know how to handle.

Your file_server will never get reached in this case.

What you need to do is use handle blocks for mutual exclusivity, and request matchers to tell Caddy which requests to send where. You mentioned your spring app is meant to only handle requests to /api/*?

This will likely do what you need:

kvasen.xyz {
	header {
		Strict-Transport-Security max-age=31536000;
	}

	encode zstd gzip

	handle /api/* {
		reverse_proxy spring-app:8080
	}
	
	handle {
		root * /app/dist/chuck
		try_files {path} index.html
		file_server
	}
}

So to explain what’s going on:

  • I moved your Strict-Transport-Security header out to a header directive, because if you were using header_down on the reverse_proxy, then that header would only be set if the request was handled by reverse_proxy, and not otherwise. Using header makes sure the header is set for all requests instead.

  • If the request starts with /api/, then it will be proxied to your spring app.

  • Any other request will fall through to the next handle block:

    • First, the root variable will be set so that any subsequent handlers or matchers that want to know where your site is located will know (this is important for both try_files and file_server to work correctly)

    • Then try_files will first check if there’s a path on disk that exists at the given request path. This works by taking the defined root, and appending the path to it. So for a request to a path like /foo/app.js, Caddy will look for a file /app/dist/chuck/foo/js. If that file does exist, then Caddy will rewrite the request path to that (which in this case is a no-op, since that’s already what the path is, and that’s ok).

    • Then if the file at {path} doesn’t exist, it will try to look for a file on disk at root + index.html, in other words, /app/dist/chuck/index.html. If that exists, it will rewrite to it. This rule is what will make sure all requests that aren’t to some JS/CSS/image assets will actually just load the angular app and let your angular app do its frontend routing.

    • Finally, file_server will do its job and actually server the file at the requested path, be it a static asset, or your index.html.

Note that before, you had rewrite * /app/dist/chuck/index.html. This wouldn’t have worked, because it would also rewrite requests to assets, so assets wouldn’t load but instead serve index.html. But actually, it wouldn’t have done that, because file_server takes the defined root and appends the request path to it, so you’d end up with file_server looking for a file at /app/dist/chuck/app/dist/chuck/index.html which obviously doesn’t make too much sense. Rewrites shouldn’t include the root, basically.

Hope that does it for you!

3 Likes

Thank you very much @francislavoie !
After some changes, everything is working. Here’s the main files, if someone else meets a similar problem:
Angular Dockerfile:

FROM node:lts-alpine as node
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build --configuration=prod

#stage 2
FROM caddy/caddy:2.4.5-alpine
COPY --from=node /app/dist/chuck /srv
EXPOSE 80

Docker-compose:

version: "3.7"

services:

  duckdb:
    image: mysql:latest
    container_name: duckdb
    volumes:
      - duck_db:/var/lib/mysql
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_HOST: '%'
      MYSQL_ROOT_USER: ****
      MYSQL_ROOT_PASSWORD: *****
      MYSQL_DATABASE: duckdb

  spring-app:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    depends_on:
      - duckdb
    environment:
      - SPRING_PROFILES_ACTIVE=prod

  angular-ui:
    build: ../chuck
    container_name: chuck
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - $PWD/Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
  duck_db:

Caddyfile:

kvasen.xyz {
	header {
		Strict-Transport-Security max-age=31536000;
	}

	encode zstd gzip

	handle /api/* {
		reverse_proxy spring-app:8080
	}

	handle {
		root * /srv
		try_files {path} index.html
		file_server
	}
}
1 Like

This is incorrect, it should be:

      - caddy_data:/data
      - caddy_config:/config

See the docs on Docker Hub

3 Likes

This topic was automatically closed after 30 days. New replies are no longer allowed.