# Reverse proxy & HTTPS (/en/docs/operations/reverse-proxy)

Put Caddy or Nginx in front of ImgYoung and get IMGYOUNG_TRUSTED_PROXIES, base_url and the upload size limit right.

## Why a reverse proxy [#why-a-reverse-proxy]

ImgYoung listens on plain HTTP and does not terminate TLS. When serving the public internet, put a reverse proxy in front of it:

- the proxy obtains certificates and serves HTTPS;
- whether the session cookie gets `Secure`, and which scheme returned links use when `base_url` is not set, both come from the proxy's `X-Forwarded-Proto`;
- the proxy blocks `/metrics` at the edge so metrics stay on the internal network;
- ImgYoung itself can then listen on loopback only.

## Caddy [#caddy]

Caddy obtains and renews certificates automatically, sets `X-Forwarded-For` / `X-Forwarded-Proto` / `X-Forwarded-Host` by default, and forwards `Host` unchanged.

```text title="deploy/Caddyfile"
# Caddy 反向代理示例:Caddy 自动申请并续期 HTTPS 证书。把域名换成你的,DNS 指向本机。
# Caddy 默认就会设置 X-Forwarded-For / -Proto / -Host,并忽略客户端自带的这些头。
# ImgYoung 直接运行在本机(非容器)时,Caddy 的连接来自回环地址,默认受信任,无需额外配置。
# ImgYoung 在容器里时,不论 Caddy 在宿主机还是另一个容器,ImgYoung 看到的对端都是 Docker 网络里的地址,
# 要给 ImgYoung 设 IMGYOUNG_TRUSTED_PROXIES 为该网络的网段(docker network inspect 查看);
# Caddy 也在容器里时,把 reverse_proxy 改成 imgyoung:8721。
img.example.com {
	# 指标只给内网抓取,公网入口一律不转发
	respond /metrics 404

	reverse_proxy 127.0.0.1:8721
}
```

## Nginx [#nginx]

```nginx title="deploy/nginx.conf"
# Nginx 反向代理示例:放进 http {} 块。域名与证书路径换成你的。
# ImgYoung 直接运行在本机(非容器)时,Nginx 的连接来自回环地址,默认受信任,无需额外配置。
# 其余情况要给 ImgYoung 设 IMGYOUNG_TRUSTED_PROXIES 为它看到的对端所在网段:ImgYoung 在容器里时,
# 不论 Nginx 在宿主机还是另一个容器,对端都是 Docker 网络里的地址(docker network inspect 查看);
# Nginx 在别的主机上时,是 Nginx 的地址。

server {
    listen 80;
    server_name img.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on; # nginx 1.25.1 起的写法;更早的版本改成 listen 443 ssl http2;
    server_name img.example.com;

    ssl_certificate     /etc/nginx/certs/img.example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/img.example.com/privkey.pem;

    # 不小于后台「单图上限」(出厂 50MB),多出的部分留给 multipart 表单开销;Nginx 默认只有 1MB
    client_max_body_size 60m;

    # 指标只给内网抓取,公网入口一律不转发
    location = /metrics {
        return 404;
    }

    location / {
        proxy_pass http://127.0.0.1:8721;
        proxy_set_header Host $host;
        # ImgYoung 从 X-Forwarded-For 自右向左取第一个非信任代理的地址作客户端地址,
        # 登录限速与 /metrics 放行都按它判定;少了这一行,所有请求都会被当成来自 Nginx 本身
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        # 决定会话 cookie 是否带 Secure,以及未配置 base_url 时返回链接的协议与域名
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
    }
}
```

None of the `proxy_set_header` lines in the example are optional:

- `Host $host`: S3 signature verification covers the `Host` header, so a proxy that rewrites it breaks S3 clients with signature mismatches;
- `X-Forwarded-For`: without it every request looks like it came from Nginx itself; see the next section;
- `X-Forwarded-Proto` and `X-Forwarded-Host`: ImgYoung reads their first value, so overwrite them with `proxy_set_header` instead of appending your value after whatever the client sent.

## Trusted proxies: IMGYOUNG_TRUSTED_PROXIES [#trusted-proxies-imgyoung_trusted_proxies]

ImgYoung derives three things from each request: the **client address** (the login rate limit key and the `/metrics` allow check), **whether the first hop was HTTPS** (the session cookie's `Secure` flag), and the **public host** (for returned links when `base_url` is not set). `X-Forwarded-*` headers are honored only when the TCP peer address is inside `IMGYOUNG_TRUSTED_PROXIES`; when the variable is unset, the default `127.0.0.0/8,::1/128` trusts a proxy on the same host only. Setting it to an empty string (or only whitespace) trusts no proxy at all, so forwarding headers are always ignored; the **Bootstrap configuration** card in the admin console then shows **Empty: forwarded headers are never trusted**.

When the peer is trusted:

1. All addresses from every `X-Forwarded-For` header are collected in order, split on commas; entries that do not parse are skipped.
2. They are checked **from right to left**, skipping addresses inside trusted networks. The first address outside them is the client.
3. If every hop is inside a trusted network (an internal client), the leftmost one is used; if the header is missing or has no usable address, the peer address is used.
4. The first value of `X-Forwarded-Proto` decides HTTPS: `https` (case-insensitive) means secure.
5. The first value of `X-Forwarded-Host` is used as the public host.

When the peer is not trusted, all `X-Forwarded-*` headers are ignored: the client is the TCP peer, and HTTPS is judged from the connection itself (always false, since ImgYoung does not terminate TLS).

**Why spoofing does not work.** Each proxy appends the peer it saw to the end of `X-Forwarded-For`; a client can only control the leftmost part. A client at `203.0.113.7` sending `X-Forwarded-For: 1.2.3.4` arrives through a local Nginx as `1.2.3.4, 203.0.113.7`; scanning from the right, the first untrusted address is `203.0.113.7`, and the forged `1.2.3.4` is ignored. A request that connects to ImgYoung directly has an untrusted peer, so none of its forwarding headers are believed.

Values by topology:

| Topology | Peer address ImgYoung sees | `IMGYOUNG_TRUSTED_PROXIES` |
|---|---|---|
| Proxy and ImgYoung (binary or systemd) on the same host | `127.0.0.1` / `::1` | The default |
| ImgYoung in Docker, proxy on the host | The Docker bridge gateway, not loopback | The gateway's network, e.g. `172.16.0.0/12`; check the actual gateway with `docker network inspect` |
| Proxy and ImgYoung as two containers in one Compose project | The proxy container's address on the project network | The project network's subnet |
| Proxy on another host | The proxy host's internal address | That address or its network |
| A CDN in front of the proxy | The proxy; the rightmost `X-Forwarded-For` hop is a CDN node | The proxy's network plus the CDN's origin-pull ranges, so the client resolves to the real visitor |

<Callout type="warn" title="What a wrong trust setting looks like">
If the peer is outside the trusted networks, every proxied request appears to come from the proxy itself: all visitors share one login rate limit, and if the proxy's address happens to be in `IMGYOUNG_METRICS_ALLOW` (Docker networks are by default) and the proxy forwards `/metrics`, anyone can read the metrics. A proxy that does not set `X-Forwarded-For` has the same effect.
</Callout>

## Listen on loopback only [#listen-on-loopback-only]

With a proxy in front, do not expose ImgYoung's port directly:

<Tabs items={['Binary / systemd', 'Docker', 'Compose']}>
<Tab value="Binary / systemd">

Set `IMGYOUNG_LISTEN=127.0.0.1:8721`. For systemd, add `Environment=IMGYOUNG_LISTEN=127.0.0.1:8721` with `sudo systemctl edit imgyoung` and restart; see [Configuration](../configuration.mdx).

</Tab>
<Tab value="Docker">

Keep the default listen address inside the container and publish the port on host loopback only:

```bash title="Terminal"
docker run -d --name imgyoung --restart unless-stopped \
  -p 127.0.0.1:8721:8721 -v imgyoung-data:/data \
  -e IMGYOUNG_TRUSTED_PROXIES=172.16.0.0/12 \
  ghcr.io/x-haose/imgyoung:latest
```

`-p 8721:8721` publishes on every interface, and Docker's own forwarding rules bypass host firewalls such as ufw.

</Tab>
<Tab value="Compose">

```yaml title="compose.yaml"
services:
  imgyoung:
    ports:
      - "127.0.0.1:8721:8721"
    environment:
      IMGYOUNG_TRUSTED_PROXIES: 172.16.0.0/12
```

If the proxy runs in the same Compose project, drop `ports` entirely and let the proxy reach `imgyoung:8721`.

</Tab>
</Tabs>

## When base_url is required [#when-base_url-is-required]

With `base_url` (**Settings → Runtime settings → Access & links → Public site address**) empty, ImgYoung derives the public address from each request: the scheme from the HTTPS check, the host from `X-Forwarded-Host` when the peer is trusted, otherwise from `Host`. Set it explicitly when:

- **the public domain differs from what reaches ImgYoung**: a CDN pulls from a different hostname, the proxy rewrites `Host` without sending `X-Forwarded-Host`, the proxy is not trusted, and so on. Otherwise the `url` returned by uploads, links copied in the admin console, and the endpoint and link prefix in the **S3 client settings** dialog are all wrong.
- **CDN cache purging is enabled**: purge jobs run in the background with no request to derive from, so they use `base_url` only; see [CDN](./cdn.mdx).
- **one-click cloud drive authorization is used**: the callback URL must match what is registered with the provider character for character and is built from `base_url` only; see [Cloud drives](../channels/cloud-drives.mdx).

## Keep the upload size limit in step with the proxy [#keep-the-upload-size-limit-in-step-with-the-proxy]

ImgYoung's max image size (`max_upload_mb`, 50 MB out of the box) applies to the file itself, so a file exactly at the limit is accepted; the whole request body may be up to the max image size plus 64 KiB, which leaves room for multipart boundaries and short fields such as `exif_policy` and `ttl`. The proxy's body limit should be at least that:

- Nginx allows only 1 MB by default; the example sets `client_max_body_size 60m;`.
- The Caddy example sets no body limit; if you add `request_body` with `max_size`, keep it at or above the max image size plus 64 KiB.

When you raise the max image size in the admin console, raise the proxy limit too, or large images are rejected with 413 by the proxy before they ever reach ImgYoung.
