├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── docker-compose-traefik-only.yml ├── docker-compose.yml ├── docker-network-client ├── pom.xml └── src │ └── test │ └── java │ └── de │ └── jonashackt │ └── ClientTest.java ├── first-call-weatherbackend-through-traefik-with-docker-dns-configured.png ├── many-calls-in-nginx.png ├── nginx ├── Dockerfile └── nginx.conf ├── one-call-in-traefik.png ├── one-call-of-weatherbackend.png ├── pom.xml ├── renovate.json ├── weatherbackend ├── Dockerfile ├── README.md ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── jonashackt │ │ │ ├── WeatherBackendApplication.java │ │ │ ├── businesslogic │ │ │ └── IncredibleLogic.java │ │ │ ├── controller │ │ │ └── WeatherBackendController.java │ │ │ └── model │ │ │ ├── GeneralOutlook.java │ │ │ ├── MethodOfPayment.java │ │ │ ├── Product.java │ │ │ ├── User.java │ │ │ └── Weather.java │ └── resources │ │ ├── application.yml │ │ └── responses │ │ └── forecast.pdf │ └── test │ ├── java │ └── de │ │ └── jonashackt │ │ └── WeatherBackendApplicationTests.java │ └── resources │ ├── example-request.json │ └── example-response.json ├── weatherclient ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── jonashackt │ │ │ ├── WeatherclientApplication.java │ │ │ └── controller │ │ │ └── WeatherclientController.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── de │ └── jonashackt │ └── WeatherclientTest.java └── without-compose.sh /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: github 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up JDK 15 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'adopt' 16 | java-version: 15 17 | 18 | - name: Build with Maven 19 | run: mvn -B install --no-transfer-progress --file pom.xml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # Eclipse # 9 | .settings 10 | .project 11 | .classpath 12 | .studio 13 | target 14 | 15 | # Vagrant 16 | .vagrant 17 | 18 | # Apple # 19 | .DS_Store 20 | 21 | # Intellij # 22 | .idea 23 | *.iml 24 | *.log 25 | 26 | # Ansible 27 | *.retry 28 | 29 | # logback 30 | logback.out.xml 31 | 32 | # zips 33 | *.tar.gz 34 | 35 | # Vagrantboxes 36 | *.box 37 | step0-packer-windows-vagrantbox/Vagrantfile 38 | *.ISO -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jonas Hecht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | traefik-cache-nginx-spring-boot 2 | ============================= 3 | [![Build Status](https://github.com/jonashackt/traefik-cache-nginx-spring-boot/workflows/github/badge.svg)](https://github.com/jonashackt/traefik-cache-nginx-spring-boot/actions) 4 | [![License](http://img.shields.io/:license-mit-blue.svg)](https://github.com/jonashackt/traefik-cache-nginx-spring-boot/blob/master/LICENSE) 5 | [![renovateenabled](https://img.shields.io/badge/renovate-enabled-yellow)](https://renovatebot.com) 6 | [![versionspringboot](https://img.shields.io/badge/dynamic/xml?color=brightgreen&url=https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/master/pom.xml&query=%2F%2A%5Blocal-name%28%29%3D%27project%27%5D%2F%2A%5Blocal-name%28%29%3D%27parent%27%5D%2F%2A%5Blocal-name%28%29%3D%27version%27%5D&label=springboot)](https://github.com/spring-projects/spring-boot) 7 | [![versiontestcontainers](https://img.shields.io/badge/dynamic/xml?color=brightgreen&url=https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/master/docker-network-client/pom.xml&query=%2F%2A%5Blocal-name%28%29%3D%27project%27%5D%2F%2A%5Blocal-name%28%29%3D%27properties%27%5D%2F%2A%5Blocal-name%28%29%3D%27testcontainers.version%27%5D&label=testcontainers)](https://www.testcontainers.org/) 8 | 9 | As [Traefik](https://traefik.io/) is a really gread & modern loadbalancer, but it sadly [doesn´t feature caching right now](https://github.com/containous/traefik/issues/878). So we need to put something in front of it, that is able to do caching - like old [Nginx](https://nginx.org/en/). 10 | 11 | And as I like full examples, let´s bring in a client application (weatherclient), which want´s to call a server backend (weatherbackend). Both are implemented as simple Spring Boot microservices, as the following ASCII shows: 12 | 13 | ``` 14 | ----------------------------------------------------------------------------- 15 | | Docker Network scope | 16 | | | 17 | | | 18 | | | 19 | ============ | ============== ================ =============== ============ | 20 | = docker- = | = = = = = = = = | 21 | = network- = -----> = weather = --> = Nginx = --> = Traefik = --> = weather = | 22 | = client = | = client = = (caching) = = (loadbalan.)= = backend = | 23 | ============ | ============== ================ =============== ============ | 24 | | | 25 | | | 26 | | | 27 | ----------------------------------------------------------------------------- 28 | 29 | ``` 30 | 31 | It also shows, that we simulate the whole scenario with [Docker](https://www.docker.com/). To have the chance to execute everything within an intergration test, we use [docker-compose-rule](https://github.com/palantir/docker-compose-rule) and the docker-network-client app. Why? 32 | 33 | As the weatherclient only has access to the DNS alias `weatherbackend`, if it itself is part of the Docker (Compose) network, we need another way to run an Integration test inside the Docker network scope. Therefore we use the [docker-compose-rule](https://github.com/palantir/docker-compose-rule) and the __docker-network-client__ that just calls __weatherclient__ inside the Docker network. 34 | 35 | ## HowTo Use 36 | 37 | Everything you need to run a full build and __complete__ test (incl. Integrationtest of docker-network-client firing up all microservices that´ll call each other with client certificate support) is this: 38 | 39 | ``` 40 | mvn clean install 41 | ``` 42 | 43 | Only if you want to check everything manually - which you for sure want to do :) - fire up all components with: 44 | 45 | ``` 46 | docker-compose up -d 47 | ``` 48 | 49 | Now you can have a look at some of the components of our architecture: 50 | 51 | Traefik: http://localhost:8080/dashboard/#/ 52 | 53 | weatherclient: http://localhost:8085/swagger-ui.html 54 | 55 | Nginx: http://localhost:8088/ 56 | 57 | 58 | #### Alternative variant without Docker-Compose for the Dockerized Spring Boot backends 59 | 60 | In a real world DevOps scenario, you may want to run Traefik separate from the Dockerized Spring Boot backends - so that each Backend is able to have it's own CI/CD pipeline for example (e.g. GitLab CI). 61 | 62 | Now in this case you don't want a central `docker-compose.yml`, since you want to run and scale all the services independently. 63 | 64 | There's a way to run Traefik and the backends separately, as shown in the [docker-compose-traefik-only.yml](docker-compose-traefik-only.yml) [without-compose.sh](without-compose.sh) files: 65 | 66 | ```yaml 67 | version: '3.8' 68 | 69 | services: 70 | 71 | traefik: 72 | image: traefik 73 | command: 74 | # - "--logLevel=DEBUG" 75 | - "--api.insecure=true" 76 | # Enabling docker provider 77 | - "--providers.docker=true" 78 | # Do not expose containers unless explicitly told so 79 | - "--providers.docker.exposedbydefault=false" 80 | - "--entrypoints.web.address=:80" 81 | ports: 82 | - "80:80" 83 | - "8080:8080" 84 | networks: 85 | - traefiknet 86 | volumes: 87 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 88 | restart: 89 | always 90 | 91 | networks: 92 | traefiknet: 93 | ``` 94 | 95 | 96 | ```shell script 97 | #!/usr/bin/env bash 98 | 99 | echo "[-->] Start Traefik using Compose as usual" 100 | docker-compose -f docker-compose-traefik-only.yml up -d 101 | 102 | echo "[-->] Startup weatherbackend without Compose - but connect to Traefik" 103 | docker build ./weatherbackend --tag weatherbackend 104 | docker run \ 105 | --rm \ 106 | -d \ 107 | -p 8095 \ 108 | --label="traefik.enable=true" \ 109 | --label="traefik.http.routers.whoami.entrypoints=web" \ 110 | --label="traefik.http.routers.weatherbackend.rule=Host(\`weatherbackend.server.test\`)" \ 111 | --network="traefik-cache-nginx-spring-boot_traefiknet" \ 112 | --name weatherbackend \ 113 | weatherbackend 114 | 115 | echo "[-->] Startup weatherclient without Compose - but connect to Traefik" 116 | docker build ./weatherclient --tag weatherclient 117 | docker run \ 118 | --rm \ 119 | -d \ 120 | -p 8085:8085 \ 121 | --network="traefik-cache-nginx-spring-boot_traefiknet" \ 122 | --name weatherclient \ 123 | weatherclient 124 | ``` 125 | 126 | The downside of this approach is, that one cannot use the scaling option of Docker-Compose now so easily. 127 | 128 | 129 | ## Nginx + Traefik + weatherbackend in logical scope (aka host) with the help of Docker DNS 130 | 131 | Additionally, in real world scenarios, Nginx + Traefik + weatherbackend would reside on a separate host with their own DNS configuration. So there´s a second "logical" scope here, which we could have implemented with tools like Vagrant - but this would have been overkill here. 132 | 133 | Trying to imitate a machine, where Traefik + weatherbackend + Nginx are running all on one machine with DNS configured, we configure the [Docker DNS alias](https://docs.docker.com/v17.09/engine/userguide/networking/configure-dns/) for weatherbackend to the Traefik container, which routes it then to the weatherbackend. This is done with the help of this Docker Compose configuration ([see the docs](https://docs.docker.com/compose/compose-file/#aliases)): 134 | 135 | ``` 136 | traefik: 137 | ... 138 | networks: 139 | default: 140 | aliases: 141 | - weatherbackend.server.test 142 | ... 143 | ``` 144 | 145 | Instead of configuring the weatherbackend directly to have the DNS alias `weatherbackend.server.test`, we use the Traefik Docker Compose service here - which has a similar effect to a scoped machine around Nginx, Traefik & weatherbackend. We should see the call now in Traefik GUI: 146 | 147 | ![first-call-weatherbackend-through-traefik-with-docker-dns-configured](first-call-weatherbackend-through-traefik-with-docker-dns-configured.png) 148 | 149 | ## Now installing Nginx as cache 150 | 151 | To see what´s happening, we need to activate Nginx´ logging to the Docker log system. As we use the alpine image here, we need to [create our own Dockerfile for that](https://stackoverflow.com/a/42369571/4964553): 152 | 153 | ``` 154 | FROM nginx:alpine 155 | 156 | # forward request and error logs to docker log collector 157 | RUN ln -sf /dev/stdout /var/log/nginx/access.log \ 158 | && ln -sf /dev/stderr /var/log/nginx/error.log 159 | 160 | CMD ["nginx-debug", "-g", "daemon off;"] 161 | ``` 162 | 163 | Additionally, we need to imitate the Scope of Nginx + Traefik + weatherbackend again - now that Nginx is at front of all three: 164 | 165 | ``` 166 | nginx: 167 | build: ./nginx 168 | ... 169 | ... 170 | networks: 171 | default: 172 | aliases: 173 | - weatherbackend.server.test 174 | ... 175 | ``` 176 | 177 | Now, the [WeatherclientController.class](https://github.com/jonashackt/traefik-cache-nginx-spring-boot/blob/master/weatherclient/src/main/java/de/jonashackt/controller/WeatherclientController.java) needs to access Nginx instead of Traefik directly. This is done with the like the [first tutorial steps with Traefik discribe](https://blog.codecentric.de/en/2017/09/traefik-modern-reverse-proxy/) - like the famous curl: 178 | 179 | ``` 180 | curl -H Host:weatherbackend.server.test http://nginx:80 -v 181 | ``` 182 | 183 | We therefore set the `Host` header when using Spring´s RestTemplate: 184 | 185 | ``` 186 | import org.springframework.http.*; 187 | import org.springframework.web.bind.annotation.GetMapping; 188 | import org.springframework.web.bind.annotation.PathVariable; 189 | import org.springframework.web.bind.annotation.ResponseStatus; 190 | import org.springframework.web.bind.annotation.RestController; 191 | import org.springframework.web.client.RestTemplate; 192 | 193 | import javax.annotation.PostConstruct; 194 | 195 | @RestController 196 | public class WeatherclientController { 197 | 198 | private RestTemplate restTemplate = new RestTemplate(); 199 | 200 | /* 201 | * Without the System property, we won´t be able to set the Host header, see 202 | * https://stackoverflow.com/questions/43223261/setting-host-header-for-spring-resttemplate-doesnt-work/43224279 203 | * and https://stackoverflow.com/a/8172736/4964553 204 | */ 205 | @PostConstruct 206 | public void setProperty() { 207 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); 208 | } 209 | 210 | @GetMapping("/forecast/{cityname}") 211 | @ResponseStatus(HttpStatus.OK) 212 | public String forecast(@PathVariable("cityname") String cityname) { 213 | 214 | HttpHeaders headers = new HttpHeaders(); 215 | headers.set("Host", "weatherbackend.server.test"); 216 | 217 | ResponseEntity responseEntity = restTemplate.exchange("http://nginx:80/weather/" + cityname, 218 | HttpMethod.GET, 219 | new HttpEntity(null, headers), 220 | String.class); 221 | 222 | return responseEntity.getBody(); 223 | } 224 | } 225 | 226 | ``` 227 | 228 | As you maybe already noticed, setting the Host header in this way [is only possible](https://stackoverflow.com/a/43224279/4964553), if we set `System.setProperty("sun.net.http.allowRestrictedHeaders", "true");`, like we do it in Spring style: 229 | 230 | ``` 231 | /* 232 | * Without the System property, we won´t be able to set the Host header, see 233 | * https://stackoverflow.com/questions/43223261/setting-host-header-for-spring-resttemplate-doesnt-work/43224279 234 | * and https://stackoverflow.com/a/8172736/4964553 235 | */ 236 | @PostConstruct 237 | public void setProperty() { 238 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); 239 | } 240 | ``` 241 | 242 | Now our setup works perfectly fine. If you fire many requests with the help of `weatherclient`, using Springfox Swagger GUI at http://localhost:8085/swagger-ui.html#!/weatherclient45controller/forecastUsingGET and do a `docker logs NginxContainerIdHere --follow` to see the logs of Nginx and a `docker logs weatherbackendContainerIdHere` to see the logs of the `weatherbackend`, you´ll notice only __one__ call in Traefik and __one__ call of the weatherbackend: 243 | 244 | ![one-call-in-traefik](one-call-in-traefik.png) 245 | 246 | ![one-call-of-weatherbackend](one-call-of-weatherbackend.png) 247 | 248 | But you´ll notice many calls inside Nginx: 249 | 250 | ![many-calls-in-nginx](many-calls-in-nginx.png) 251 | 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /docker-compose-traefik-only.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | traefik: 6 | image: traefik 7 | command: 8 | # - "--logLevel=DEBUG" 9 | - "--api.insecure=true" 10 | # Enabling docker provider 11 | - "--providers.docker=true" 12 | # Do not expose containers unless explicitly told so 13 | - "--providers.docker.exposedbydefault=false" 14 | - "--entrypoints.web.address=:80" 15 | ports: 16 | - "80:80" 17 | - "8080:8080" 18 | networks: 19 | - traefiknet 20 | volumes: 21 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 22 | restart: 23 | always 24 | 25 | networks: 26 | traefiknet: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | 5 | weatherclient: 6 | build: ./weatherclient 7 | ports: 8 | - "8085:8085" 9 | tty: 10 | true 11 | restart: 12 | unless-stopped 13 | 14 | nginx: 15 | build: ./nginx 16 | ports: 17 | - "8088:80" 18 | # trying to imitate a machine, where Traefik + weatherbackend + Nginx are running all on one machine with DNS active 19 | # therefore trying to configure Docker DNS alias for weatherbackend to Traefik container, which should route it to 20 | # weatherbackend 21 | networks: 22 | default: 23 | aliases: 24 | - weatherbackend.server.test 25 | restart: 26 | always 27 | volumes: 28 | - ./nginx:/etc/nginx/conf.d:ro 29 | 30 | traefik: 31 | image: traefik 32 | command: 33 | # - "--logLevel=DEBUG" 34 | - "--api.insecure=true" 35 | # Enabling docker provider 36 | - "--providers.docker=true" 37 | # Do not expose containers unless explicitly told so 38 | - "--providers.docker.exposedbydefault=false" 39 | - "--entrypoints.web.address=:80" 40 | ports: 41 | - "80:80" 42 | - "8080:8080" 43 | volumes: 44 | - "/var/run/docker.sock:/var/run/docker.sock:ro" 45 | restart: 46 | always 47 | 48 | weatherbackend: 49 | build: ./weatherbackend 50 | labels: 51 | # Explicitly tell Traefik to expose this container 52 | - "traefik.enable=true" 53 | # Allow request only from the predefined entry point named "web" 54 | - "traefik.http.routers.weatherbackend.entrypoints=web" 55 | # The domain the service will respond to 56 | - "traefik.http.routers.weatherbackend.rule=Host(`weatherbackend.server.test`)" 57 | ports: 58 | - "8095" 59 | tty: 60 | true 61 | restart: 62 | unless-stopped 63 | 64 | -------------------------------------------------------------------------------- /docker-network-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | docker-network-client 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | 11 | de.jonashackt 12 | traefik-cache-nginx-spring-boot 13 | 0.0.1-SNAPSHOT 14 | 15 | 16 | 17 | 4.5.1 18 | 1.21.1 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-test 31 | test 32 | 33 | 34 | 35 | io.rest-assured 36 | rest-assured 37 | ${rest-assured.version} 38 | test 39 | 40 | 41 | 42 | org.testcontainers 43 | testcontainers 44 | ${testcontainers.version} 45 | test 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /docker-network-client/src/test/java/de/jonashackt/ClientTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.apache.http.HttpStatus; 4 | import org.junit.ClassRule; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.testcontainers.containers.DockerComposeContainer; 10 | import org.testcontainers.containers.wait.strategy.Wait; 11 | 12 | import java.io.File; 13 | 14 | import static io.restassured.RestAssured.given; 15 | import static org.hamcrest.Matchers.containsString; 16 | 17 | @RunWith(SpringRunner.class) 18 | @ContextConfiguration() 19 | public class ClientTest { 20 | 21 | @ClassRule 22 | public static DockerComposeContainer services = 23 | new DockerComposeContainer(new File("../docker-compose.yml")) 24 | .withExposedService("weatherbackend", 8095, Wait.forListeningPort()) 25 | .withExposedService("traefik", 8080, Wait.forListeningPort()) 26 | .withExposedService("nginx", 80, Wait.forListeningPort()) 27 | .withExposedService("weatherclient", 8085, Wait.forHttp("/swagger-ui.html").forStatusCode(200)); 28 | 29 | 30 | @Test 31 | public void is_weatherclient_able_to_call_weatherbackend_through_nginx_and_traefik() { 32 | given() 33 | .pathParam("cityname", "Weimar") 34 | .when() 35 | .get("http://localhost:8085/forecast/{cityname}") 36 | .then() 37 | .statusCode(HttpStatus.SC_OK) 38 | .assertThat() 39 | .body(containsString("Hello Weimar! This is a RESTful HttpService written in Spring.")); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /first-call-weatherbackend-through-traefik-with-docker-dns-configured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/6d9a7efdb4ab1413cfa8b902d6434949fa0b3eef/first-call-weatherbackend-through-traefik-with-docker-dns-configured.png -------------------------------------------------------------------------------- /many-calls-in-nginx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/6d9a7efdb4ab1413cfa8b902d6434949fa0b3eef/many-calls-in-nginx.png -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # we need our own Dockerfile here, because we want Nginx to log into the Docker log system 2 | # see https://stackoverflow.com/a/42369571/4964553 3 | FROM nginx:alpine 4 | 5 | # forward request and error logs to docker log collector 6 | RUN ln -sf /dev/stdout /var/log/nginx/access.log \ 7 | && ln -sf /dev/stderr /var/log/nginx/error.log 8 | 9 | CMD ["nginx-debug", "-g", "daemon off;"] -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Diese Datei wird automatisch von ansible erzeugt. 2 | # Dokumentation kann dort gefunden werden: http://nginx.org/en/docs/http/ngx_http_proxy_module.html 3 | 4 | proxy_cache_path /var/cache/nginx/main-cache levels=1:2 keys_zone=main-cache:10m inactive=24h max_size=1g; 5 | 6 | upstream traefik { 7 | least_conn; 8 | server traefik:80; 9 | } 10 | 11 | server { 12 | listen 80; 13 | server_name nginx; 14 | proxy_cache main-cache; 15 | 16 | location / { 17 | proxy_pass http://traefik; 18 | # pass Host header fuer Traefik 19 | proxy_set_header Host $http_host; 20 | 21 | proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; 22 | proxy_cache_valid 200 5m; 23 | proxy_cache_lock on; 24 | proxy_cache_lock_age 20s; 25 | proxy_cache_methods GET HEAD POST; 26 | proxy_cache_key "$request_uri|$request_body"; 27 | client_body_buffer_size 500k; 28 | proxy_buffers 8 32k; 29 | proxy_buffer_size 64k; 30 | add_header X-Cache-Status $upstream_cache_status; 31 | add_header X-Upstream $upstream_addr; 32 | } 33 | } -------------------------------------------------------------------------------- /one-call-in-traefik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/6d9a7efdb4ab1413cfa8b902d6434949fa0b3eef/one-call-in-traefik.png -------------------------------------------------------------------------------- /one-call-of-weatherbackend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/6d9a7efdb4ab1413cfa8b902d6434949fa0b3eef/one-call-of-weatherbackend.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | de.jonashackt 7 | traefik-cache-nginx-spring-boot 8 | 0.0.1-SNAPSHOT 9 | pom 10 | 11 | Example project showing how to use Nginx as a simple cache for Traefik - incl. a Spring Boot client and server 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 2.7.18 17 | 18 | 19 | 20 | 21 | ${project.basedir} 22 | UTF-8 23 | UTF-8 24 | 11 25 | 11 26 | 11 27 | 28 | 29 | 30 | weatherbackend 31 | weatherclient 32 | docker-network-client 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "automergeType": "branch", 7 | "major": { 8 | "automerge": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /weatherbackend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11.0.16-jdk-slim 2 | 3 | MAINTAINER Jonas Hecht 4 | 5 | VOLUME /tmp 6 | 7 | # Add Spring Boot app.jar to Container 8 | ADD "target/*.jar" app.jar 9 | 10 | ENV JAVA_OPTS="" 11 | 12 | # Fire up our Spring Boot app by default 13 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] -------------------------------------------------------------------------------- /weatherbackend/README.md: -------------------------------------------------------------------------------- 1 | ### Just a simple REST backend 2 | 3 | Try a GET to this resource: 4 | 5 | [http://localhost:8090/weather/general/outlook](http://localhost:8090/weather/general/outlook) 6 | 7 | I added the really nice Swaggerfox-UI - just fire up WeatherBackendApplication and go to [http://localhost:8090/swagger-ui.html](http://localhost:8090/swagger-ui.html) -------------------------------------------------------------------------------- /weatherbackend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | de.jonashackt.weatherbackend 5 | weatherbackend 6 | jar 7 | Demo backend providing some demo weather services 8 | 9 | 10 | de.jonashackt 11 | traefik-cache-nginx-spring-boot 12 | 0.0.1-SNAPSHOT 13 | ../ 14 | 15 | 16 | 17 | 4.5.1 18 | 1.8.0 19 | 2.19.0 20 | 2.0.34 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | 31 | commons-io 32 | commons-io 33 | ${commons-io.version} 34 | 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-actuator 40 | 41 | 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-devtools 46 | optional 47 | 48 | 49 | 50 | 51 | org.springdoc 52 | springdoc-openapi-ui 53 | ${springdoc-openapi.version} 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-test 60 | test 61 | 62 | 63 | 64 | io.rest-assured 65 | rest-assured 66 | ${rest-assured.version} 67 | test 68 | 69 | 70 | 71 | org.apache.pdfbox 72 | pdfbox 73 | ${pdfbox.version} 74 | test 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-maven-plugin 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/WeatherBackendApplication.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WeatherBackendApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(WeatherBackendApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/businesslogic/IncredibleLogic.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.businesslogic; 2 | 3 | import java.time.Instant; 4 | import java.util.Date; 5 | 6 | import de.jonashackt.model.GeneralOutlook; 7 | 8 | public class IncredibleLogic { 9 | 10 | public static GeneralOutlook generateGeneralOutlook() { 11 | GeneralOutlook generalOutlook = new GeneralOutlook(); 12 | generalOutlook.setCity("Weimar"); 13 | generalOutlook.setDate(Date.from(Instant.now())); 14 | generalOutlook.setState("Germany"); 15 | generalOutlook.setWeatherStation("BestStationInTown"); 16 | return generalOutlook; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/controller/WeatherBackendController.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.controller; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import de.jonashackt.businesslogic.IncredibleLogic; 6 | import de.jonashackt.model.*; 7 | import org.apache.commons.io.IOUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.core.io.Resource; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | import java.io.IOException; 16 | 17 | @RestController 18 | @RequestMapping("/weather") 19 | public class WeatherBackendController { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(WeatherBackendController.class); 22 | 23 | @Value(value="classpath:responses/forecast.pdf") 24 | private Resource forecastPdf; 25 | 26 | @RequestMapping(path = "/general/outlook", method=RequestMethod.POST, produces="application/json") 27 | @ResponseStatus(HttpStatus.OK) 28 | public @ResponseBody GeneralOutlook generateGeneralOutlook(@RequestBody Weather weather) throws JsonProcessingException { 29 | LOG.info("Request for /general/outlook with POST"); 30 | /* 31 | * Some incredible Businesslogic... 32 | */ 33 | LOG.info("Called Backend"); 34 | return IncredibleLogic.generateGeneralOutlook(); 35 | } 36 | 37 | @RequestMapping(path = "/general/outlook", method=RequestMethod.GET, produces="application/json") 38 | @ResponseStatus(HttpStatus.OK) 39 | public @ResponseBody String infoAboutGeneralOutlook() throws JsonProcessingException { 40 | LOG.info("Request for /general/outlook with GET"); 41 | 42 | Weather weather = new Weather(); 43 | weather.setFlagColor("blue"); 44 | weather.setPostalCode("99425"); 45 | weather.setUser(new User(55, 5634500, MethodOfPayment.Bitcoin)); 46 | weather.setProduct(Product.ForecastBasic); 47 | 48 | ObjectMapper mapper = new ObjectMapper(); 49 | String weatherJson = mapper.writeValueAsString(weather); 50 | 51 | return "Try a POST also against this URL! Just send some body with it like: '" + weatherJson + "'"; 52 | } 53 | 54 | @RequestMapping(value = "/{name}", method = RequestMethod.GET, produces = "text/plain") 55 | public String whatsTheSenseInThat(@PathVariable("name") String name) { 56 | LOG.info("Request for /weather/{" + name + "} with GET"); 57 | return "Hello " + name + "! This is a RESTful HttpService written in Spring. Try to use some other HTTP verbs (don´t say 'methods' :P ) :)"; 58 | } 59 | 60 | @RequestMapping(path = "/general/outlook/{zip}", method=RequestMethod.GET, produces="application/pdf") 61 | @ResponseStatus(HttpStatus.OK) 62 | public byte[] getWeatherInformationPdf(@PathVariable("zip") String zip) throws IOException { 63 | LOG.info("Request for /general/outlook/{zip} with GET"); 64 | 65 | return IOUtils.toByteArray(forecastPdf.getInputStream()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/model/GeneralOutlook.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.model; 2 | 3 | import java.util.Date; 4 | 5 | public class GeneralOutlook { 6 | 7 | private String city; 8 | private String state; 9 | private String weatherStation; 10 | private Date date; 11 | 12 | public String getCity() { 13 | return city; 14 | } 15 | public void setCity(String city) { 16 | this.city = city; 17 | } 18 | public String getState() { 19 | return state; 20 | } 21 | public void setState(String state) { 22 | this.state = state; 23 | } 24 | public String getWeatherStation() { 25 | return weatherStation; 26 | } 27 | public void setWeatherStation(String weatherStation) { 28 | this.weatherStation = weatherStation; 29 | } 30 | public Date getDate() { 31 | return date; 32 | } 33 | public void setDate(Date date) { 34 | this.date = date; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/model/MethodOfPayment.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.model; 2 | 3 | public enum MethodOfPayment { 4 | 5 | Paypal("Paypal"), Bitcoin("Bitcoin"), Unknown("Unknown"); 6 | 7 | private String name; 8 | 9 | private MethodOfPayment(String name) { 10 | this.name = name; 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/model/Product.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.model; 2 | 3 | public enum Product { 4 | 5 | ForecastBasic("ForecastBasic"), 6 | ForecastProfessional("ForecastProfessional"), 7 | ForecastUltimateXL("ForecastUltimateXL"), 8 | Unknown("Unknown"); 9 | 10 | private String name; 11 | 12 | private Product(String name) { 13 | this.name = name; 14 | } 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/model/User.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.model; 2 | 3 | public class User { 4 | 5 | public User(int age, int contribution, MethodOfPayment methodOfPayment) { 6 | this.age = age; 7 | this.contribution = contribution; 8 | this.methodOfPayment = methodOfPayment; 9 | } 10 | 11 | public User() {}; 12 | 13 | private int age; 14 | private int contribution; 15 | private MethodOfPayment methodOfPayment; 16 | 17 | public int getAge() { 18 | return age; 19 | } 20 | public void setAge(int age) { 21 | this.age = age; 22 | } 23 | public int getContribution() { 24 | return contribution; 25 | } 26 | public void setContribution(int contribution) { 27 | this.contribution = contribution; 28 | } 29 | public MethodOfPayment getMethodOfPayment() { 30 | return methodOfPayment; 31 | } 32 | public void setMethodOfPayment(MethodOfPayment methodOfPayment) { 33 | this.methodOfPayment = methodOfPayment; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /weatherbackend/src/main/java/de/jonashackt/model/Weather.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.model; 2 | 3 | 4 | public class Weather { 5 | 6 | private String postalCode; 7 | private String flagColor; 8 | private Product product; 9 | private User user; 10 | 11 | public String getPostalCode() { 12 | return postalCode; 13 | } 14 | public void setPostalCode(String zipCode) { 15 | this.postalCode = zipCode; 16 | } 17 | public String getFlagColor() { 18 | return flagColor; 19 | } 20 | public void setFlagColor(String flagColor) { 21 | this.flagColor = flagColor; 22 | } 23 | public User getUser() { 24 | return user; 25 | } 26 | public void setUser(User user) { 27 | this.user = user; 28 | } 29 | public Product getProduct() { 30 | return product; 31 | } 32 | public void setProduct(Product product) { 33 | this.product = product; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /weatherbackend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8095 3 | -------------------------------------------------------------------------------- /weatherbackend/src/main/resources/responses/forecast.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonashackt/traefik-cache-nginx-spring-boot/6d9a7efdb4ab1413cfa8b902d6434949fa0b3eef/weatherbackend/src/main/resources/responses/forecast.pdf -------------------------------------------------------------------------------- /weatherbackend/src/test/java/de/jonashackt/WeatherBackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import de.jonashackt.businesslogic.IncredibleLogic; 4 | import de.jonashackt.model.GeneralOutlook; 5 | import de.jonashackt.model.Product; 6 | import de.jonashackt.model.Weather; 7 | import io.restassured.http.ContentType; 8 | import org.apache.http.HttpStatus; 9 | import org.apache.pdfbox.pdmodel.PDDocument; 10 | import org.apache.pdfbox.text.PDFTextStripper; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | 14 | import java.io.ByteArrayInputStream; 15 | import java.io.IOException; 16 | 17 | import static io.restassured.RestAssured.given; 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.containsString; 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | @SpringBootTest( 23 | classes = WeatherBackendApplication.class, 24 | webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, 25 | properties = {"server.port=8080"} 26 | ) 27 | public class WeatherBackendApplicationTests { 28 | 29 | @Test 30 | public void testWithRestAssured() { 31 | 32 | Weather weather = new Weather(); 33 | weather.setFlagColor("blue"); 34 | weather.setPostalCode("99425"); 35 | weather.setProduct(Product.ForecastBasic); 36 | 37 | given() // can be ommited when GET only 38 | .contentType(ContentType.JSON) 39 | .body(weather) 40 | .when() // can be ommited when GET only 41 | .post("http://localhost:8080/weather/general/outlook") 42 | .then() 43 | .statusCode(HttpStatus.SC_OK) 44 | .contentType(ContentType.JSON) 45 | .assertThat() 46 | .equals(IncredibleLogic.generateGeneralOutlook()); 47 | 48 | GeneralOutlook outlook = given() // can be ommited when GET only 49 | .contentType(ContentType.JSON) 50 | .body(weather).post("http://localhost:8080/weather/general/outlook").as(GeneralOutlook.class); 51 | 52 | assertEquals("Weimar", outlook.getCity()); 53 | } 54 | 55 | @Test 56 | public void getWeatherInformationPdf_should_return_correct_Pdf() throws IOException { 57 | 58 | byte[] pdf = given() 59 | .contentType(ContentType.JSON) 60 | .pathParam("zip", "99425") 61 | .when() 62 | .get("http://localhost:8080/weather/general/outlook/{zip}") 63 | .then() 64 | .statusCode(HttpStatus.SC_OK).extract().asByteArray(); 65 | 66 | String textInPdf = extractPdfText(pdf); 67 | assertThat(textInPdf, containsString("Weather in your city")); 68 | assertThat(textInPdf, containsString("Weimar")); 69 | assertThat(textInPdf, containsString("18.1")); 70 | assertThat(textInPdf, containsString("Wind Gentle Breeze 3.6 m/s")); 71 | assertThat(textInPdf, containsString("West-southwest")); 72 | assertThat(textInPdf, containsString("Cloudiness scattered clouds")); 73 | assertThat(textInPdf, containsString("Pressure 1018 hpa")); 74 | assertThat(textInPdf, containsString("Humidity 55 %")); 75 | assertThat(textInPdf, containsString("Sunrise 23:30")); 76 | assertThat(textInPdf, containsString("Sunset 13:1")); 77 | 78 | } 79 | 80 | /** 81 | * Extracts all the Text inside a Pdf 82 | */ 83 | private static String extractPdfText(byte[] pdfData) throws IOException { 84 | PDDocument pdfDocument = PDDocument.load(new ByteArrayInputStream(pdfData)); 85 | try { 86 | return new PDFTextStripper().getText(pdfDocument); 87 | } finally { 88 | pdfDocument.close(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /weatherbackend/src/test/resources/example-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "postalCode" : "99425", 3 | "flagColor" : "blue", 4 | "product" : "ForecastBasic", 5 | "user" : null 6 | } -------------------------------------------------------------------------------- /weatherbackend/src/test/resources/example-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "city": "Weimar", 3 | "date": 1456991480588, 4 | "state": "Germany", 5 | "weatherStation": "BestStationInTown" 6 | } -------------------------------------------------------------------------------- /weatherclient/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11.0.16-jdk-slim 2 | 3 | MAINTAINER Jonas Hecht 4 | 5 | VOLUME /tmp 6 | 7 | # Add Spring Boot app.jar to Container 8 | ADD "target/*.jar" app.jar 9 | 10 | ENV JAVA_OPTS="" 11 | 12 | # Fire up our Spring Boot app by default 13 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] -------------------------------------------------------------------------------- /weatherclient/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | weatherclient 6 | jar 7 | Just a simple Client for our Dockerized weatherbackend 8 | 9 | 10 | de.jonashackt 11 | traefik-cache-nginx-spring-boot 12 | 0.0.1-SNAPSHOT 13 | ../ 14 | 15 | 16 | 17 | 4.5.1 18 | 1.8.0 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | 34 | 35 | org.springdoc 36 | springdoc-openapi-ui 37 | ${springdoc-openapi.version} 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | 48 | io.rest-assured 49 | rest-assured 50 | ${rest-assured.version} 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-maven-plugin 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /weatherclient/src/main/java/de/jonashackt/WeatherclientApplication.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class WeatherclientApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(WeatherclientApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /weatherclient/src/main/java/de/jonashackt/controller/WeatherclientController.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt.controller; 2 | 3 | import org.springframework.http.*; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import org.springframework.web.client.RestTemplate; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | @RestController 13 | public class WeatherclientController { 14 | 15 | private RestTemplate restTemplate = new RestTemplate(); 16 | 17 | /* 18 | * Without the System property, we won´t be able to set the Host header, see 19 | * https://stackoverflow.com/questions/43223261/setting-host-header-for-spring-resttemplate-doesnt-work/43224279 20 | * and https://stackoverflow.com/a/8172736/4964553 21 | */ 22 | @PostConstruct 23 | public void setProperty() { 24 | System.setProperty("sun.net.http.allowRestrictedHeaders", "true"); 25 | } 26 | 27 | @GetMapping("/forecast/{cityname}") 28 | @ResponseStatus(HttpStatus.OK) 29 | public String forecast(@PathVariable("cityname") String cityname) { 30 | 31 | HttpHeaders headers = new HttpHeaders(); 32 | headers.set("Host", "weatherbackend.server.test"); 33 | 34 | ResponseEntity responseEntity = restTemplate.exchange("http://nginx:80/weather/" + cityname, 35 | HttpMethod.GET, 36 | new HttpEntity(null, headers), 37 | String.class); 38 | 39 | return responseEntity.getBody(); 40 | } 41 | 42 | @GetMapping("/traefik/forecast/{cityname}") 43 | @ResponseStatus(HttpStatus.OK) 44 | public String forecastTraefikOnly(@PathVariable("cityname") String cityname) { 45 | 46 | return restTemplate.getForEntity("http://weatherbackend.server.test/weather/" + cityname, String.class).getBody(); 47 | } 48 | 49 | @GetMapping("/noproxy/forecast/{cityname}") 50 | @ResponseStatus(HttpStatus.OK) 51 | public String forecastWithoutProxy(@PathVariable("cityname") String cityname) { 52 | 53 | return restTemplate.getForEntity("http://weatherbackend:8095/weather/" + cityname, String.class).getBody(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /weatherclient/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8085 -------------------------------------------------------------------------------- /weatherclient/src/test/java/de/jonashackt/WeatherclientTest.java: -------------------------------------------------------------------------------- 1 | package de.jonashackt; 2 | 3 | import io.restassured.http.ContentType; 4 | import org.apache.http.HttpStatus; 5 | import org.junit.jupiter.api.Disabled; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | 9 | import static io.restassured.RestAssured.given; 10 | 11 | @SpringBootTest( 12 | classes = WeatherclientApplication.class, 13 | webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, 14 | properties = {"server.port=8087"} 15 | ) 16 | public class WeatherclientTest { 17 | 18 | @Disabled 19 | @Test 20 | public void 21 | should_retrieve_simple_response_form_weatherbackend() { 22 | 23 | given() 24 | .contentType(ContentType.JSON) 25 | .pathParam("cityname", "Weimar") 26 | .when() 27 | .get("http://localhost:8087/forecast/{cityname}") 28 | .then() 29 | .statusCode(HttpStatus.SC_OK); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /without-compose.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "[-->] Start Traefik using Compose as usual" 4 | docker-compose -f docker-compose-traefik-only.yml up -d 5 | 6 | echo "[-->] Startup weatherbackend without Compose - but connect to Traefik" 7 | docker build ./weatherbackend --tag weatherbackend 8 | docker run \ 9 | --rm \ 10 | -d \ 11 | -p 8095 \ 12 | --label="traefik.enable=true" \ 13 | --label="traefik.http.routers.weatherbackend.entrypoints=web" \ 14 | --label="traefik.http.routers.weatherbackend.rule=Host(\`weatherbackend.server.test\`)" \ 15 | --network="traefik-cache-nginx-spring-boot_traefiknet" \ 16 | --name weatherbackend \ 17 | weatherbackend 18 | 19 | echo "[-->] Startup weatherclient without Compose - but connect to Traefik" 20 | docker build ./weatherclient --tag weatherclient 21 | docker run \ 22 | --rm \ 23 | -d \ 24 | -p 8085:8085 \ 25 | --network="traefik-cache-nginx-spring-boot_traefiknet" \ 26 | --name weatherclient \ 27 | weatherclient 28 | --------------------------------------------------------------------------------