# Nginx in front of OpenGit

OpenGit is usually run behind **nginx** (or another reverse proxy) so you can terminate TLS, raise upload limits, and load-balance several Gunicorn workers. The app ships template configs under `scripts/`:

- [`scripts/nginx-opengit.conf`](../scripts/nginx-opengit.conf) — nginx on the same host as OpenGit
- [`scripts/nginx-socketio.conf`](nginx-socketio.conf) — reference for Socket.IO upgrade headers (also embedded in the templates above)
- [`scripts/nginx-opengit-external.conf`](../scripts/nginx-opengit-external.conf) — nginx on a different machine from the Gunicorn processes

Replace placeholders:

- `__UPSTREAM_SERVERS__` — one `server host:port;` per Gunicorn worker (same ports as your `gunicorn` bind)
- `__INSTALL_DIR__` — filesystem path to the OpenGit install (for `location /static/` in the on-host template)
- `__OPENGIT_SERVER_IP__` — (external template) IP or hostname where Gunicorn and the update-status server listen

Use **`ip_hash`** on the upstream so Socket.IO and similar sticky sessions stay on one worker.

## Client IP (`X-Forwarded-For`, `X-Real-IP`)

OpenGit resolves the client IP from `X-Forwarded-For` (skipping leading `127.0.0.1` / `::1` hops — common when a **second** nginx on the app host proxies to `127.0.0.1:gunicorn`), then `X-Real-IP`, then `remote_addr` if it is not loopback. Nginx should pass:

- `proxy_set_header X-Real-IP $remote_addr;`
- `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`

**Edge nginx on one machine, OpenGit on another:** the **edge** server must set the headers above using the Internet client’s `$remote_addr`. If the OpenGit host also runs nginx and `proxy_pass`es to `127.0.0.1:808x`, Gunicorn’s TCP peer is loopback — that middle nginx must **forward** the edge values, not replace `X-Real-IP` with `127.0.0.1`. Typical pattern on the app host:

```nginx
proxy_set_header X-Real-IP $http_x_real_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
```

(If `$http_x_real_ip` is empty, fall back to `$remote_addr` only when you know that peer is the edge proxy, not the browser.)

**If the app always sees your router (e.g. `192.168.1.1`)** when you are on LTE or another network, nginx’s `$remote_addr` is already that address: something between the Internet and nginx (often the home router doing HTTP proxying, or another reverse proxy) is the TCP client. Plain port forwarding usually preserves the original source IP on the LAN packet; if you still see `.1`, check the router (disable HTTP proxy / “virtual server” proxy modes if any), or put nginx on the machine that is the **first** TLS hop.

**Trusted proxy in front of nginx** (same LAN IP every time, but that proxy sends the real client in `X-Forwarded-For`): use the real IP module so nginx rewrites `$remote_addr` before you proxy to Gunicorn:

```nginx
# Replace 192.168.1.1 with the actual IP that connects to nginx (your front proxy / router).
set_real_ip_from 192.168.1.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
```

Place this **inside** the same `server { ... }` block (before `location` is fine). Reload nginx after `nginx -t`.

**Cloudflare** (orange-cloud proxy): trust Cloudflare’s published IP ranges and use their connecting-IP header, e.g. `real_ip_header CF-Connecting-IP;` plus `set_real_ip_from` for [Cloudflare IP ranges](https://www.cloudflare.com/ips/). Then keep `proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for` so the app still receives a correct chain.

**Debugging:** add a custom `log_format` including `$remote_addr` and `$http_x_forwarded_for` on the `listen 8443` server and confirm what nginx sees on a request from LTE.

**Gunicorn:** By default Gunicorn only trusts `X-Forwarded-*` from **127.0.0.1**. If nginx runs on **another machine** and connects to Gunicorn over LAN, Gunicorn **strips** forwarded headers unless you allow that peer. The template [`scripts/opengit-instance.service`](../scripts/opengit-instance.service) passes `--forwarded-allow-ips=*` (safe when workers bind only on loopback/LAN, not the public Internet). Without this, 2FA emails and trusted IPs often show **Unknown** or **127.0.0.1** even though nginx sends correct headers. Reload systemd units after changing the service file.

When that is correct, **Trusted IPs** and 2FA emails show a **real public or carrier IP** (e.g. after LTE login), not `Unknown` or loopback.

## Double reverse proxy: `stream` (SNI / TLS passthrough) + HTTP nginx

If public **443** uses nginx **`stream { }`** with **`ssl_preread`** and **`proxy_pass`** to **`127.0.0.1:8443`** (or similar), that hop is **TCP only**. It cannot add HTTP headers. The HTTP nginx on **8443** then sees **`$remote_addr` = `127.0.0.1`** and an empty **`X-Forwarded-For`**, so OpenGit gets **Unknown** / **127.0.0.1** until you preserve the original client.

**Approach: PROXY protocol + `real_ip` on the HTTP server**

1. **`stream` `server`** (the one that `listen 443` and `proxy_pass`es to your local TLS port):

```nginx
server {
    listen 0.0.0.0:443;
    ssl_preread on;
    proxy_pass $target_backend;
    proxy_protocol on;
    proxy_timeout 10s;
    proxy_connect_timeout 10s;
}
```

2. **`upstream`** to the local HTTP/TLS nginx must **not** use invalid parameters (no `ssl` / `proxy_protocol` on the `server` line inside `upstream`):

```nginx
upstream local_backend {
    server 127.0.0.1:8443;
}
```

3. **HTTP `server`** that terminates TLS for OpenGit (e.g. `gitssy.example.com` on **8443**): accept PROXY, then treat the PROXY line as the client for **`$remote_addr`** (so your existing `proxy_set_header X-Real-IP` / `X-Forwarded-For` to Gunicorn work):

```nginx
server {
    listen 8443 ssl proxy_protocol;
    server_name gitssy.example.com;

    set_real_ip_from 127.0.0.1;
    real_ip_header proxy_protocol;

    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    # … existing proxy_set_header X-Real-IP / X-Forwarded-For / locations …
}
```

**`set_real_ip_from`** must match whoever opens the TCP connection to this `server` (here, the stream layer on the same host → **`127.0.0.1`**). **`real_ip_header proxy_protocol`** makes nginx use the address from the PROXY header as the client IP.

4. **Gunicorn** still needs **`--forwarded-allow-ips=*`** (or an explicit allow list) when the peer to Gunicorn is not loopback, as described in **Client IP** above.

**Important:** `proxy_protocol on` in **`stream`** applies to **every** upstream chosen by that `server` block (e.g. local **and** remote). Any other backend (another IP:443) must **also** be configured to accept the PROXY protocol on that listener, or those routes will break. If you cannot change a remote backend, you need a different split (separate ports/IPs, or only PROXY to backends you control).

**Sanity check:** With a debug `log_format`, **`$remote_addr`** on the HTTP server should show the **real client** after the change, not **`127.0.0.1`**, and OpenGit trusted IPs / 2FA email context should show that same public IP.

## TLS example (8443)

If you terminate HTTPS on nginx (e.g. Let’s Encrypt on `8443`), keep the same upstream and header pattern as the templates. Set `X-Forwarded-Proto $scheme` so OpenGit knows the client used HTTPS.

## Shell web preview (`/shells/webview/`)

When a user creates a shell, OpenGit publishes the in-container web port (default **8080**) to **`127.0.0.1:<random-host-port>`** on the **same machine as Gunicorn**. Only loopback is bound; the public URL is always under your site:

`/shells/webview/<container_id>/…`

Example: app in the shell listens on port **3000** → user sets “Web preview port” to **3000** → paths like `/shells/webview/<id>/settings` are proxied to `http://127.0.0.1:<hostPort>/settings`.

**Production debugging:** set environment variable **`OPENGIT_WEBVIEW_DEBUG=1`** (e.g. in `.opengit.env` or your systemd unit) and restart Gunicorn. Logs will include `[webview]` lines (path, container id, resolved host port, upstream status). If nginx returns your normal “Page Not Found” HTML, check Gunicorn logs for **`[webview] 404 (no Flask route matched)`** — that usually means the request never hit the webview view (e.g. old deploy, or trailing-slash mismatch before `strict_slashes=False` was added).

**Requirements**

- Docker/Podman and Gunicorn must run on the **same host** so `127.0.0.1:<publishedPort>` is reachable from the app. If workers run on other machines without that port forward, web preview will not work without a different design.

**Nginx**

Add a dedicated prefix `location` **after** `location /socket.io/` and **before** `location /`:

```nginx
location ^~ /shells/webview/ {
    proxy_pass http://opengit_backend;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}
```

This still forwards to the same OpenGit upstream as the rest of the app; nginx does **not** need per-shell upstreams. Long timeouts help slow dev servers; buffering off helps some streaming responses.

You do **not** need to change `map $http_upgrade` or the main `location /` block for routing.

## Frameworks and URL prefixes

The app sets `X-Forwarded-Prefix: /shells/webview/<id>` when talking to the process in the shell. Some frameworks need extra config to generate correct links when mounted under a prefix (Django `FORCE_SCRIPT_NAME`, Rails `relative_url_root`, `<base href="…">`, etc.).

OpenGit rewrites common **`Location`** redirects that point at `http://127.0.0.1:…` or absolute paths so the browser stays under `/shells/webview/<id>/`.

## WebSockets (Vite HMR, etc.)

The Flask handler primarily streams **HTTP**. Full WebSocket proxying to the in-shell dev server may require extra work (e.g. OpenResty, njs, or an `auth_request` + `auth_request_set` pattern to set a dynamic `proxy_pass` to `127.0.0.1:<port>`). The nginx block above forwards **Upgrade** headers to Gunicorn so a future app-level WS proxy can work without further nginx changes.

## Reload nginx

After editing the config:

```bash
sudo nginx -t && sudo systemctl reload nginx
```
