From 3ace667b6c46c577e5d033744031550b05c6fcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Tue, 2 Jun 2026 13:44:08 +0200 Subject: [PATCH] feat(services): refine split-horizon OIDC routing and harden nextcloud patch - authentik: address the rewrite service by compose service name instead of a network alias on the public FQDN, which shadowed extra_hosts pins and broke OIDC discovery for c-ares-based (Node) resolvers - homarr: add homarr_extra_hosts to pin the IdP FQDN to a LAN IP so OIDC discovery stays in-network while the issuer matches the browser-facing URL - opnform: add opnform_oidc_sso_redirect_root to 302 the root URL to the SSO path (deep-links untouched, /login?bypass=1 break-glass); restart ingress via container restart so envsubst re-renders nginx.conf - nextcloud: make the UserConfig sed workaround fail loud on upstream drift instead of silently skipping (nextcloud/server#59629) - gitignore: exclude the local .ansible/ collection cache --- .gitignore | 3 + .../authentik/templates/docker-compose.yml.j2 | 31 ++-- roles/homarr/README.md | 1 + roles/homarr/defaults/main.yml | 4 + roles/homarr/templates/docker-compose.yml.j2 | 6 + roles/nextcloud/tasks/main.yml | 23 ++- roles/opnform/README.md | 10 +- roles/opnform/defaults/main.yml | 7 + roles/opnform/handlers/main.yml | 10 ++ roles/opnform/meta/argument_specs.yml | 10 ++ roles/opnform/tasks/main.yml | 159 +++++++++++++++--- roles/opnform/templates/nginx.conf.j2 | 49 +++++- 12 files changed, 264 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index a84afb0..e510871 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ __pycache__/ *.pyc plugins/lookup/__pycache__/ + +# Local Ansible collection cache (galaxy/collection resolver) +/.ansible/ diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index e5b8a11..cd3ef1e 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -44,13 +44,14 @@ services: condition: service_healthy networks: {{ authentik_backend_network }}: {} - # Network alias so traefik (which shares this network) can resolve - # the canonical FQDN to this container directly. The URL-based - # service below uses that to send upstream traffic with a fixed - # Host header equal to the canonical hostname. - {{ authentik_traefik_network }}: - aliases: - - {{ authentik_domains[0] }} + # No alias for the public FQDN here: that would shadow `/etc/hosts` + # pins (extra_hosts) in other containers sharing this network and + # break OIDC discovery for Node-based clients (c-ares-based + # resolvers consult Docker DNS before /etc/hosts). The URL-based + # service below addresses this container by its compose service + # name `server`, which Docker exposes as an alias on every network + # the container joins. + {{ authentik_traefik_network }}: {} labels: - traefik.enable=true - traefik.docker.network={{ authentik_traefik_network }} @@ -68,14 +69,14 @@ services: - traefik.http.services.{{ authentik_service_name }}.loadbalancer.server.port={{ authentik_port }} {% if authentik_host_rewrite_domains | length > 0 %} # Server-to-server entry: a separate service points at this very - # container by the canonical FQDN (resolved via the network alias - # above) and disables passHostHeader so the upstream Host header - # becomes `{{ authentik_domains[0] }}`. Authentik builds OIDC issuer - # URLs from X-Forwarded-Host (not Host), so we also pin that header - # via middleware. Together this keeps the iss claim aligned with - # the public hostname browsers see during login, even when the - # request itself arrived on an internal *.int.* FQDN. - - traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.server.url=http://{{ authentik_domains[0] }}:{{ authentik_port }} + # container by its compose service name `server` and disables + # passHostHeader so the upstream Host header becomes + # `{{ authentik_domains[0] }}`. Authentik builds OIDC issuer URLs + # from X-Forwarded-Host (not Host), so we also pin that header via + # middleware. Together this keeps the iss claim aligned with the + # public hostname browsers see during login, even when the request + # itself arrived on an internal *.int.* FQDN. + - traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.server.url=http://server:{{ authentik_port }} - traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.passhostheader=false - traefik.http.middlewares.{{ authentik_service_name }}-xfh-rewrite.headers.customrequestheaders.X-Forwarded-Host={{ authentik_domains[0] }} {% for d in authentik_host_rewrite_domains %} diff --git a/roles/homarr/README.md b/roles/homarr/README.md index 774b598..a1e1dcf 100644 --- a/roles/homarr/README.md +++ b/roles/homarr/README.md @@ -49,6 +49,7 @@ See `defaults/main.yml` for the full list. Most useful overrides: |---|---|---| | `homarr_domain` | `homarr.local.test` | Traefik Host rule | | `homarr_extra_domains` | `[]` | Extra Host-rule hostnames (OR-combined), e.g. internal `*.int.*` FQDN | +| `homarr_extra_hosts` | `[]` | Container `/etc/hosts` overrides (`host:ip`) — pin IdP FQDN to LAN IP | | `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL | | `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both | | `homarr_oidc_issuer` | empty | Identity provider issuer URL | diff --git a/roles/homarr/defaults/main.yml b/roles/homarr/defaults/main.yml index 3d22ee7..a7b2f2b 100644 --- a/roles/homarr/defaults/main.yml +++ b/roles/homarr/defaults/main.yml @@ -19,6 +19,10 @@ homarr_domain: "homarr.local.test" # *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered # by the cert). homarr_extra_domains: [] +# Extra /etc/hosts entries inside the homarr container (format "host:ip"). +# Used to pin the IdP's public FQDN to a LAN IP so OIDC discovery stays +# in-network while the issuer URL matches what browsers see. +homarr_extra_hosts: [] homarr_image: "ghcr.io/homarr-labs/homarr:latest" homarr_port: 7575 homarr_use_docker: false diff --git a/roles/homarr/templates/docker-compose.yml.j2 b/roles/homarr/templates/docker-compose.yml.j2 index 5907763..2021de0 100644 --- a/roles/homarr/templates/docker-compose.yml.j2 +++ b/roles/homarr/templates/docker-compose.yml.j2 @@ -26,6 +26,12 @@ services: AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}" networks: - {{ homarr_traefik_network }} +{% if homarr_extra_hosts | default([]) | length > 0 %} + extra_hosts: +{% for h in homarr_extra_hosts %} + - "{{ h }}" +{% endfor %} +{% endif %} labels: - traefik.enable=true - traefik.docker.network={{ homarr_traefik_network }} diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index 7ee804c..c43bc4d 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -66,13 +66,32 @@ - name: Check UserConfig.php patch status per container ansible.builtin.shell: + # rc 0 -> already patched; rc 1 -> still the unpatched original; rc 2 -> + # neither marker present (upstream drift -> the guard task below fails loud). cmd: >- - docker exec {{ item }} grep -q "strtolower((string)" /var/www/html/lib/private/Config/UserConfig.php + docker exec {{ item }} sh -c ' + grep -q "strtolower((string)\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 0; + grep -q "strtolower(\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 1; + exit 2' loop: "{{ _nextcloud_php_containers.stdout_lines }}" register: _nextcloud_userconfig_check changed_when: false failed_when: false +- name: Fail if the UserConfig.php source drifted from the expected upstream line + ansible.builtin.fail: + msg: >- + Neither the patched nor the expected original strtolower($this->getTypedValue(...)) + line was found in {{ item.item }}:/var/www/html/lib/private/Config/UserConfig.php. + The nextcloud/server#59629 workaround can no longer locate its target — the upstream + source likely changed. Re-verify whether the fix shipped (then drop this block) or + update the sed expression. Silently skipping would let the TypeError regress. + loop: "{{ _nextcloud_userconfig_check.results }}" + loop_control: + label: "{{ item.item }}" + when: + - item.rc | default(2) == 2 + - name: Apply UserConfig::getValueBool string-cast workaround ansible.builtin.shell: cmd: >- @@ -83,7 +102,7 @@ loop_control: label: "{{ item.item }}" when: - - item.rc | default(1) != 0 + - item.rc | default(2) == 1 - name: Wait for Nextcloud to be ready ansible.builtin.shell: diff --git a/roles/opnform/README.md b/roles/opnform/README.md index 3773ed7..707d736 100644 --- a/roles/opnform/README.md +++ b/roles/opnform/README.md @@ -167,10 +167,16 @@ calls `/api/auth/{slug}/redirect` (no domain check) and forwards the browser to the IdP authorize URL. ```yaml -opnform_oidc_sso_entrypoint: true # default false -opnform_oidc_sso_path: "/sso" # link users to https:///sso +opnform_oidc_sso_entrypoint: true # default false +opnform_oidc_sso_path: "/sso" # link users to https:///sso +opnform_oidc_sso_redirect_root: true # default false — root URL 302s to ``` +With `opnform_oidc_sso_redirect_root` enabled both the bare hostname +and `/login` jump straight to the IdP. Public form deep-links +(`/forms/`, `/admin/...`) are not touched. The email form remains +reachable as a break-glass path via `/login?bypass=1`. + ## Networking / split-horizon ```yaml diff --git a/roles/opnform/defaults/main.yml b/roles/opnform/defaults/main.yml index 9a79b07..25529a5 100644 --- a/roles/opnform/defaults/main.yml +++ b/roles/opnform/defaults/main.yml @@ -130,6 +130,13 @@ opnform_oidc_group_role_mappings: [] opnform_oidc_sso_entrypoint: false opnform_oidc_sso_path: "/sso" +# When true, the ingress 302-redirects the root URL (exact-match on `/`) +# to opnform_oidc_sso_path so visiting https:/// jumps straight +# to the IdP login without showing OpnForm's email form. Public form +# deep-links (`/forms/`, `/login`, etc.) are untouched. +# Requires opnform_oidc_sso_entrypoint=true. +opnform_oidc_sso_redirect_root: false + # Traefik configuration opnform_traefik_network: "proxy" opnform_use_ssl: true diff --git a/roles/opnform/handlers/main.yml b/roles/opnform/handlers/main.yml index 1c0b422..03c44b9 100644 --- a/roles/opnform/handlers/main.yml +++ b/roles/opnform/handlers/main.yml @@ -6,3 +6,13 @@ community.docker.docker_compose_v2: project_src: "{{ opnform_docker_compose_dir }}" state: restarted + +# nginx.conf is bind-mounted into the ingress container and rendered to +# /etc/nginx/conf.d/default.conf by the envsubst entrypoint on container +# start. Plain `docker restart` re-runs that entrypoint, so the new +# template is picked up without bouncing db/redis/api/ui. +- name: restart opnform ingress + community.docker.docker_container: + name: opnform-ingress + state: started + restart: true diff --git a/roles/opnform/meta/argument_specs.yml b/roles/opnform/meta/argument_specs.yml index 5e1248d..1f91e71 100644 --- a/roles/opnform/meta/argument_specs.yml +++ b/roles/opnform/meta/argument_specs.yml @@ -256,6 +256,16 @@ argument_specs: - Path (on C(opnform_domain)) where the direct-SSO redirect page is served when C(opnform_oidc_sso_entrypoint=true). Must start with C(/) and not collide with OpnForm's own routes. + opnform_oidc_sso_redirect_root: + type: bool + default: false + description: + - When true, the nginx ingress 302-redirects the root URL + (exact-match on C(/)) to C(opnform_oidc_sso_path), so visiting + C(https:///) jumps straight to the IdP without + OpnForm's email login form. Public form deep-links + (C(/forms/), C(/login), C(/admin/...)) are untouched. + Requires C(opnform_oidc_sso_entrypoint=true). opnform_traefik_network: type: str diff --git a/roles/opnform/tasks/main.yml b/roles/opnform/tasks/main.yml index 71048e5..cd5efe4 100644 --- a/roles/opnform/tasks/main.yml +++ b/roles/opnform/tasks/main.yml @@ -75,22 +75,97 @@ src: nginx.conf.j2 dest: "{{ opnform_docker_compose_dir }}/nginx.conf" mode: '0644' - notify: restart opnform + notify: restart opnform ingress # OIDC_FORCE_LOGIN disables OpnForm's password login — including the -# password-based admin/OIDC bootstrap this role performs below. So the -# first compose render always keeps force-login OFF; it is switched on -# only after the bootstrap completes (see step 7). This keeps a first -# deploy on a fresh host working even when opnform_oidc_force_login=true. -- name: Render compose with force-login disabled during bootstrap +# password-based admin/OIDC bootstrap this role performs below. The +# bootstrap must therefore run with force-login OFF. To stay idempotent +# on re-runs (avoid recreating api containers on every apply), we only +# turn force-login OFF when the bootstrap is actually needed (first run +# on a fresh host, no OIDC connection yet). Once the connection exists +# we render the final force-login value straight away, so the compose +# file is byte-identical across re-runs. +- name: Probe whether OpnForm is already bootstrapped + block: + - name: Check if opnform-api container exists and is healthy + ansible.builtin.command: + cmd: docker inspect --format='{% raw %}{{.State.Health.Status}}{% endraw %}' opnform-api + register: _opnform_api_health_probe + changed_when: false + failed_when: false + + - name: Attempt admin login (only when api is healthy) + ansible.builtin.uri: + url: "https://127.0.0.1/api/login" + method: POST + headers: + Host: "{{ opnform_domain }}" + body_format: json + body: + email: "{{ opnform_admin_email }}" + password: "{{ opnform_admin_password }}" + status_code: [200, 401, 422] + validate_certs: false + register: _opnform_probe_login + no_log: true + when: + - _opnform_api_health_probe.rc == 0 + - _opnform_api_health_probe.stdout == "healthy" + - opnform_admin_email | length > 0 + - opnform_admin_password | length > 0 + + - name: Probe for existing OIDC connection + ansible.builtin.uri: + url: "https://127.0.0.1/api/open/workspaces" + method: GET + headers: + Host: "{{ opnform_domain }}" + Authorization: "Bearer {{ _opnform_probe_login.json.token }}" + status_code: 200 + validate_certs: false + register: _opnform_probe_workspaces + no_log: true + when: + - opnform_oidc_enabled | bool + - _opnform_probe_login is defined + - _opnform_probe_login.status | default(0) == 200 + + - name: Probe OIDC connections on default workspace + ansible.builtin.uri: + url: "https://127.0.0.1/api/open/workspaces/{{ _opnform_probe_workspaces.json[0].id }}/oidc-connections" + method: GET + headers: + Host: "{{ opnform_domain }}" + Authorization: "Bearer {{ _opnform_probe_login.json.token }}" + status_code: 200 + validate_certs: false + register: _opnform_probe_oidc + no_log: true + when: + - opnform_oidc_enabled | bool + - _opnform_probe_workspaces is defined + - _opnform_probe_workspaces.json | default([]) | length > 0 + +- name: Decide whether force-login can render in its final state ansible.builtin.set_fact: - _opnform_force_login_effective: false + # True when force-login is desired AND admin+OIDC bootstrap has + # already completed (admin user exists with the configured password, + # OIDC connection is present). On a fresh host both checks fail and + # we fall back to false so the bootstrap below can run. + _opnform_force_login_effective: >- + {{ + (opnform_oidc_enabled | bool) + and (opnform_oidc_force_login | bool) + and (_opnform_probe_login.status | default(0) == 200) + and ((_opnform_probe_oidc.json | default([])) | length > 0) + }} - name: Deploy docker-compose file ansible.builtin.template: src: docker-compose.yml.j2 dest: "{{ opnform_docker_compose_dir }}/docker-compose.yml" mode: '0644' + register: _opnform_compose_rendered notify: restart opnform # ===================================================================== @@ -123,6 +198,12 @@ # Skips the self-hosted setup page by registering the first user via # OpnForm's /api/register endpoint. Idempotent: a successful login # attempt with the same credentials means the user already exists. +# +# Skipped entirely when force-login already rendered in its final state +# (probe in step 2 confirmed admin + connection exist). Re-running the +# /api/login probe on a force-login-enabled api would 401 and 422, so +# avoid the noise — and avoid spurious "changed" status from a register +# call that won't help anyway. - name: Check if OpnForm admin user already exists ansible.builtin.uri: @@ -140,6 +221,7 @@ when: - opnform_admin_email | length > 0 - opnform_admin_password | length > 0 + - not (_opnform_force_login_effective | bool) - name: Create OpnForm admin user via /api/register ansible.builtin.uri: @@ -160,7 +242,8 @@ when: - opnform_admin_email | length > 0 - opnform_admin_password | length > 0 - - opnform_admin_login.status != 200 + - not (_opnform_force_login_effective | bool) + - opnform_admin_login.status | default(0) != 200 # ===================================================================== # 6. OIDC IDENTITY CONNECTION (optional) @@ -171,6 +254,13 @@ # existing one to the desired state. PATCHing (rather than skipping when # one exists) keeps inventory changes — e.g. a corrected issuer — applied # on re-runs instead of leaving stale values in the DB forever. +# +# Skipped on re-applies when force-login is already enabled — the API +# password login required for these calls is disabled, and the connection +# is known to exist (otherwise force-login wouldn't have rendered in its +# final state in step 2). To intentionally re-provision the connection +# from inventory changes on such a host: temporarily set +# opnform_oidc_force_login=false, re-apply, then set it back to true. - name: Log in as admin to obtain OIDC API token ansible.builtin.uri: @@ -186,7 +276,9 @@ validate_certs: false register: opnform_oidc_token no_log: true - when: opnform_oidc_enabled | bool + when: + - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - name: Fetch admin's workspaces ansible.builtin.uri: @@ -199,7 +291,9 @@ validate_certs: false register: opnform_workspaces no_log: true - when: opnform_oidc_enabled | bool + when: + - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - name: Fetch existing OIDC connections for the default workspace ansible.builtin.uri: @@ -212,7 +306,9 @@ validate_certs: false register: opnform_existing_oidc no_log: true - when: opnform_oidc_enabled | bool + when: + - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - name: Resolve OIDC group-role mappings ansible.builtin.set_fact: @@ -224,7 +320,9 @@ ([{'idp_group': opnform_oidc_admin_group, 'role': 'admin'}] if (opnform_oidc_admin_group | length > 0) else []) }} - when: opnform_oidc_enabled | bool + when: + - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) # Desired connection state shared by both the create (POST) and update # (PATCH) calls below. client_secret is always sent: OpnForm's update @@ -244,7 +342,9 @@ require_state: true group_role_mappings: "{{ _opnform_oidc_group_role_mappings }}" no_log: true - when: opnform_oidc_enabled | bool + when: + - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - name: Create OIDC identity connection ansible.builtin.uri: @@ -260,6 +360,7 @@ no_log: true when: - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - opnform_existing_oidc.json | length == 0 # An OIDC connection already exists: PATCH it to the desired state so @@ -280,20 +381,26 @@ no_log: true when: - opnform_oidc_enabled | bool + - not (_opnform_force_login_effective | bool) - opnform_existing_oidc.json | length > 0 # ===================================================================== -# 7. ENABLE FORCE LOGIN (optional, must run last) +# 7. ENABLE FORCE LOGIN (first-run only) # ===================================================================== -# OIDC_FORCE_LOGIN disables password login — including the password-based -# admin/OIDC bootstrap above — so it is switched on only now, after the -# connection is provisioned. OpnForm itself only enforces force-login when -# an enabled OIDC connection exists, so the order matters: connection -# first, force-login second. -- name: Enable force login now that the OIDC connection exists +# On the very first apply, step 2 rendered the compose file with +# force-login disabled (so the bootstrap above could use the password +# login). Now that the OIDC connection exists, re-render the compose +# file with force-login in its final state and recreate the api +# containers once. +# +# On all subsequent applies the probe in step 2 already rendered the +# final value, the compose file is byte-identical here, and this block +# is a no-op (the template task reports "ok", no recreate). +- name: Enable force login (first run, after OIDC bootstrap) when: - opnform_oidc_enabled | bool - opnform_oidc_force_login | bool + - not (_opnform_force_login_effective | bool) block: - name: Re-render compose with force-login enabled ansible.builtin.set_fact: @@ -314,6 +421,13 @@ wait_timeout: 180 when: _opnform_force_login_compose is changed + - name: Restart ingress so nginx picks up the new api container IPs + community.docker.docker_container: + name: opnform-ingress + state: started + restart: true + when: _opnform_force_login_compose is changed + - name: Display deployment info ansible.builtin.debug: msg: |- @@ -335,8 +449,9 @@ Users with @{{ opnform_oidc_domain }} addresses will be redirected to {{ opnform_oidc_issuer }} on login. {% if opnform_oidc_sso_entrypoint %} - Direct-SSO entrypoint: {{ opnform_base_url }}{{ opnform_oidc_sso_path }} - (link users here to skip the email login form) + Login intercept active: {{ opnform_base_url }}/login forwards + directly to the IdP. Use {{ opnform_base_url }}/login?bypass=1 + as a break-glass path for the email form when the IdP is down. {% endif %} {% else %} OIDC: disabled (set opnform_oidc_enabled=true to auto-configure) diff --git a/roles/opnform/templates/nginx.conf.j2 b/roles/opnform/templates/nginx.conf.j2 index 6f62840..5aeb1bc 100644 --- a/roles/opnform/templates/nginx.conf.j2 +++ b/roles/opnform/templates/nginx.conf.j2 @@ -15,20 +15,53 @@ server { index index.html index.htm index.php; + # Re-resolve upstream container hostnames via Docker's embedded DNS + # at request time. Without this, nginx caches the first resolution + # forever; if `api` or `ui` get recreated and pick up a new IP, every + # request 502s until the ingress itself is restarted. + resolver 127.0.0.11 valid=10s ipv6=off; + set $upstream_api api; + set $upstream_ui ui; + {% if opnform_oidc_enabled and opnform_oidc_sso_entrypoint %} - # Direct-SSO entrypoint: a tiny page that asks the API for the IdP - # authorize URL (no email/domain check on this endpoint) and forwards - # the browser there. Link users here instead of /login to skip the - # email field entirely. Exact-match so it wins over the `/` prefix. - location = {{ opnform_oidc_sso_path }} { + # Root → /login. Public forms live under /forms/, so the bare + # hostname only serves the authenticated dashboard — sending it + # straight to /login (which then jumps to the IdP) saves an extra + # UI-side redirect for anyone who lands there. + location = / { + return 302 /login; + } + + # /login intercept: serve a tiny HTML page that calls OpnForm's + # /api/auth/{slug}/redirect endpoint and forwards the browser to the + # IdP authorize URL — skipping the email-based login form entirely. + # Break-glass: /login?bypass=1 falls through to the UI's own login + # form so the email/password path stays reachable when the IdP is + # down. Bypass branches to a named location (`@login_bypass`) because + # `proxy_pass` inside an `if` block is invalid nginx config. + location = /login { + if ($arg_bypass = "1") { + error_page 418 = @login_bypass; + return 418; + } default_type text/html; - return 200 'Redirecting to sign-in…

Redirecting to sign-in…

'; + return 200 'Redirecting to sign-in…

Redirecting to sign-in…

'; + } + + location @login_bypass { + proxy_http_version 1.1; + proxy_pass http://$upstream_ui:3000/login; + 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 X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; } {% endif %} location / { proxy_http_version 1.1; - proxy_pass http://ui:3000; + proxy_pass http://$upstream_ui:3000; 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; @@ -45,7 +78,7 @@ server { location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass api:9000; + fastcgi_pass $upstream_api:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/public/index.php;