Skip to content

Commit 1845e94

Browse files
committed
Adding deployment docs for using a caddy proxy
1 parent a96e94f commit 1845e94

File tree

1 file changed

+205
-0
lines changed

1 file changed

+205
-0
lines changed

deployment/caddy-proxy.md

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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

Comments
 (0)