|
| 1 | +# Deploying behind caddy proxy |
| 2 | + |
| 3 | +This tutorial describes how to use [the official API Platform distribution](../distribution/index.md) behind a caddy proxy (at a docker environment), e.g. if you want to use many apps at one server or at your dev machine. |
| 4 | + |
| 5 | +## Pre-conditions |
| 6 | + |
| 7 | +* A running caddy server, listen to ports 443 and 80: our proxy |
| 8 | +* An API Platform app using the [API Platform distribution](../distribution/index.md) |
| 9 | + |
| 10 | +In the tutorial I use the expression ''caddy proxy'' for the proxy itself and ''app caddy'' for the caddy server, that is used in the docker-compose.yaml of API Platform itself. |
| 11 | + |
| 12 | +## Configuration |
| 13 | + |
| 14 | +First, please remove all exposed ports from the app caddy service: |
| 15 | + |
| 16 | +```diff |
| 17 | +# docker-compose.yaml |
| 18 | +... |
| 19 | + caddy: |
| 20 | + image: ${IMAGES_PREFIX:-}app-caddy |
| 21 | + depends_on: |
| 22 | + - php |
| 23 | + - pwa |
| 24 | + environment: |
| 25 | + PWA_UPSTREAM: pwa:3000 |
| 26 | + SERVER_NAME: ${SERVER_NAME:-localhost}, caddy:80 |
| 27 | + MERCURE_PUBLISHER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} |
| 28 | + MERCURE_SUBSCRIBER_JWT_KEY: ${CADDY_MERCURE_JWT_SECRET:-!ChangeThisMercureHubJWTSecretKey!} |
| 29 | + restart: unless-stopped |
| 30 | + volumes: |
| 31 | + - php_socket:/var/run/php |
| 32 | + - caddy_data:/data |
| 33 | + - caddy_config:/config |
| 34 | +- ports: |
| 35 | +- # HTTP |
| 36 | +- - target: 80 |
| 37 | +- published: ${HTTP_PORT:-80} |
| 38 | +- protocol: tcp |
| 39 | +- # HTTPS |
| 40 | +- - target: 443 |
| 41 | +- published: ${HTTPS_PORT:-443} |
| 42 | +- protocol: tcp |
| 43 | +- # HTTP/3 |
| 44 | +- - target: 443 |
| 45 | +- published: ${HTTP3_PORT:-443} |
| 46 | +- protocol: udp |
| 47 | +... |
| 48 | +``` |
| 49 | + |
| 50 | +These ports are used by your caddy proxy and not by the API Platform app caddy. |
| 51 | + |
| 52 | +> All upcoming steps are shown in the main docker-compose.yaml. |
| 53 | +> |
| 54 | +> However, you probably want to implement them in docker-compose.overwrite.yaml or docker-compose.prod.yaml, depending on your environment. |
| 55 | +
|
| 56 | +### Adding networks |
| 57 | + |
| 58 | +We introduce two docker networks for our services, one for the API Platform docker services to connect to each other (default), the other to connect to the caddy proxy server (proxy): |
| 59 | + |
| 60 | +```yaml |
| 61 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 62 | + |
| 63 | +... |
| 64 | + |
| 65 | +networks: |
| 66 | + proxy: |
| 67 | + external: true |
| 68 | + default: |
| 69 | +``` |
| 70 | +
|
| 71 | +The caddy proxy is attached to the external `proxy` network, too. |
| 72 | + |
| 73 | +Now, each service in our `docker-compose.yaml` gets the following additional key: `networks` |
| 74 | + |
| 75 | +```yaml |
| 76 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 77 | +
|
| 78 | +... |
| 79 | +services: |
| 80 | + php: |
| 81 | + networks: |
| 82 | + - default |
| 83 | +... |
| 84 | +``` |
| 85 | + |
| 86 | +Only the `caddy` service gets the additional `proxy` network: |
| 87 | + |
| 88 | +```yaml |
| 89 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 90 | +... |
| 91 | +services: |
| 92 | +
|
| 93 | + caddy: |
| 94 | + networks: |
| 95 | + - default |
| 96 | + - proxy |
| 97 | +... |
| 98 | +``` |
| 99 | + |
| 100 | +### Configure caddy proxy |
| 101 | + |
| 102 | +Now you can add a reverse entry to your caddy proxies `Caddyfile`. If you use the [Caddy docker proxy module](https://github.com/lucaslorentz/caddy-docker-proxy), you only have to add the following labels to your app caddy service: |
| 103 | + |
| 104 | +```yaml |
| 105 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 106 | +... |
| 107 | +services: |
| 108 | +
|
| 109 | + caddy: |
| 110 | + labels: |
| 111 | + caddy: your-app.localhost |
| 112 | + caddy.reverse_proxy: "{{upstreams 80}}" |
| 113 | +``` |
| 114 | + |
| 115 | +It will add the needed `Caddyfile` entry to your caddy proxies `Caddyfile` (which you can write directly, if you don't want to use the caddy docker proxy module): |
| 116 | + |
| 117 | +```text |
| 118 | +[Your App Domain] { |
| 119 | + reverse_proxy [app caddy container IP, proxy network]:80 |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +e.g. |
| 124 | + |
| 125 | +```text |
| 126 | +your-app.localhost { |
| 127 | + reverse_proxy 172.19.0.2:80 |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +Now you can reach your app with your domain name, but... |
| 132 | + |
| 133 | +### Prevent redirect problems |
| 134 | + |
| 135 | +Your API Platform application is configured to work behind a https connection and the app caddy service should handle the ssl connection. |
| 136 | + |
| 137 | +In our setup the caddy proxy handles the ssl connection and internally redirects to an ''unsave'' port 80. This results in some problems that we have to solve: |
| 138 | + |
| 139 | +Your app caddy will redirect you everytime from port 80 to port 443 (https), the browser connects to https://your-app.localhost, caddy proxy redirects to 80, app caddy to 443 and so on. |
| 140 | + |
| 141 | +We can solve this by adding `http` in front of the server name (only) for the caddy app service. Then, caddy will not redirect to 443 internally: |
| 142 | + |
| 143 | +```yaml |
| 144 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 145 | +... |
| 146 | +services: |
| 147 | + caddy: |
| 148 | + environment: |
| 149 | + SERVER_NAME: http://${SERVER_NAME:-localhost}, caddy:80 |
| 150 | +... |
| 151 | +``` |
| 152 | + |
| 153 | +Wohoo: If you open your domain, you can see the welcome page of API Platform, open the API page and the mercure debugger, ... |
| 154 | + |
| 155 | +### Prevent CORS errors |
| 156 | + |
| 157 | +... but you cannot open the admin application because a CORS error occurs. |
| 158 | + |
| 159 | +The problem is that the caddy app service also works as a proxy for the PWA service, but this proxy runs with port 80 / http. We remember, all SSL connections are handled by the caddy proxy and not by our API Platform app. |
| 160 | + |
| 161 | +The PWA sends a request to https://your-app.localhost/ with Content-Type `application/ld+json`. The caddy proxy redirects to the app caddy (http/port 80) which uses the php service to answer. |
| 162 | +API Platform identifies the route, Content-Type and schema (which is http at the moment) and sends a redirect to http://your-app.localhost/docs.jsonld to your PWA. |
| 163 | +The PWA follows the redirect link and throw a CORS error because of the different schema (http !== https). |
| 164 | + |
| 165 | +To prevent this, we use a trick that is described in the symfony docs for [deployments behind a proxy](https://symfony.com/doc/current/deployment/proxies.html#custom-headers-when-using-a-reverse-proxy): |
| 166 | + |
| 167 | +We command the proxy caddy to add a custom header with the original schema and replace the `HTTP_X_FORWARDED_PROTO` with this header at the `index.php`: |
| 168 | + |
| 169 | +```php |
| 170 | +<?php |
| 171 | +// app/public/index.php |
| 172 | +
|
| 173 | +use App\Kernel; |
| 174 | +
|
| 175 | +// https://symfony.com/doc/current/deployment/proxies.html#custom-headers-when-using-a-reverse-proxy |
| 176 | +$_SERVER['HTTP_X_FORWARDED_PROTO'] = $_SERVER['HTTP_CUSTOM_FORWARDED_PROTO']; |
| 177 | +
|
| 178 | +... |
| 179 | +``` |
| 180 | + |
| 181 | +and |
| 182 | + |
| 183 | +```yaml |
| 184 | +# docker-compose.yaml (or docker-compose.overwrite.yaml / docker-compose.prod.yaml) |
| 185 | +... |
| 186 | +services: |
| 187 | + caddy: |
| 188 | + labels: |
| 189 | + caddy: your-app.localhost |
| 190 | + caddy.reverse_proxy: "{{upstreams 80}}" |
| 191 | + caddy.reverse_proxy.header_up: Custom-Forwarded-Proto {scheme} |
| 192 | +... |
| 193 | +``` |
| 194 | + |
| 195 | +This results in the following `Caddyfile` for the caddy proxy: |
| 196 | + |
| 197 | +```text |
| 198 | +your-app.localhost { |
| 199 | + reverse_proxy 172.19.0.2:80 { |
| 200 | + header_up Custom-Forwarded-Proto {scheme} |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +Now, symfony generates the redirect to https://your-app.localhost/docs.jsonld, and you can reach the API Platform admin without any CORS errors :-) |
0 commit comments