nginx-proxy / nginx-proxy

Automated nginx proxy for Docker containers using docker-gen
MIT License
18.08k stars 3k forks source link

Allow per-hostname ports #1504

Closed dajester2013 closed 1 week ago

dajester2013 commented 3 years ago

The current template allows for separate hostnames, but they all use the same port for the upstream.

I want to proxy the sonatype/nexus3 container, which starts the docker registry on a separate port from the standard service (8082 for docker vs 8081 for maven).

I need to be able to configure two hostnames for the proxy, with two separate upstream ports.

I would think that you since you can do -e VIRTUAL_HOST=host1,host2, you should also be able to do -e VIRTUAL_PORT=8081,8082

dajester2013 commented 3 years ago

I have a workaround - I modified the template and mounted it as a volume when starting the proxy container.

workaround template:

{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }}

{{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }}
{{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }}

{{ define "upstream" }}
    {{ if .Address }}
        {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}}
        {{ if and .Container.Node.ID .Address.HostPort }}
            # {{ .Container.Node.Name }}/{{ .Container.Name }}
            server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }};
        {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}}
        {{ else if .Network }}
            # {{ .Container.Name }}
            server {{ .Network.IP }}:{{ .Address.Port }};
        {{ end }}
    {{ else if .Network }}
        # {{ .Container.Name }}
        {{ if .Network.IP }}
            server {{ .Network.IP }} down;
        {{ else }}
            server 127.0.0.1 down;
        {{ end }}
    {{ end }}

{{ end }}

{{ define "ssl_policy" }}
    {{ if eq .ssl_policy "Mozilla-Modern" }}
        ssl_protocols TLSv1.3;
        {{/* nginx currently lacks ability to choose ciphers in TLS 1.3 in configuration, see https://trac.nginx.org/nginx/ticket/1529 /*}}
        {{/* a possible workaround can be modify /etc/ssl/openssl.cnf to change it globally (see https://trac.nginx.org/nginx/ticket/1529#comment:12 ) /*}}
        {{/* explicitly set ngnix default value in order to allow single servers to override the global http value */}}
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers off;
    {{ else if eq .ssl_policy "Mozilla-Intermediate" }}
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
        ssl_prefer_server_ciphers off;
    {{ else if eq .ssl_policy "Mozilla-Old" }}
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-TLS-1-2-2017-01" }}
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:AES128-GCM-SHA256:AES128-SHA256:AES256-GCM-SHA384:AES256-SHA256';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-TLS-1-1-2017-01" }}
        ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-2016-08" }}
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-2015-05" }}
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DES-CBC3-SHA';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-2015-03" }}
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA:DES-CBC3-SHA';
        ssl_prefer_server_ciphers on;
    {{ else if eq .ssl_policy "AWS-2015-02" }}
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
        ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:DHE-DSS-AES128-SHA';
        ssl_prefer_server_ciphers on;
    {{ end }}
{{ end }}

# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
# scheme used to connect to this server
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
  default $http_x_forwarded_proto;
  ''      $scheme;
}

# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
# server port the client connected to
map $http_x_forwarded_port $proxy_x_forwarded_port {
  default $http_x_forwarded_port;
  ''      $server_port;
}

# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
# Connection header that may have been passed to this server
map $http_upgrade $proxy_connection {
  default upgrade;
  '' close;
}

# Apply fix for very long server names
server_names_hash_bucket_size 128;

# Default dhparam
{{ if (exists "/etc/nginx/dhparam/dhparam.pem") }}
ssl_dhparam /etc/nginx/dhparam/dhparam.pem;
{{ end }}

# Set appropriate X-Forwarded-Ssl header
map $scheme $proxy_x_forwarded_ssl {
  default off;
  https on;
}

gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

log_format vhost '$host $remote_addr - $remote_user [$time_local] '
                 '"$request" $status $body_bytes_sent '
                 '"$http_referer" "$http_user_agent"';

access_log off;

{{/* Get the SSL_POLICY defined by this container, falling back to "Mozilla-Intermediate" */}}
{{ $ssl_policy := or ($.Env.SSL_POLICY) "Mozilla-Intermediate" }}
{{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}

{{ if $.Env.RESOLVERS }}
resolver {{ $.Env.RESOLVERS }};
{{ end }}

{{ if (exists "/etc/nginx/proxy.conf") }}
include /etc/nginx/proxy.conf;
{{ else }}
# HTTP 1.1 support
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $proxy_connection;
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 $proxy_x_forwarded_proto;
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;

# Mitigate httpoxy attack (see README for details)
proxy_set_header Proxy "";
{{ end }}

{{ $access_log := (or (and (not $.Env.DISABLE_ACCESS_LOGS) "access_log /var/log/nginx/access.log vhost;") "") }}

{{ $enable_ipv6 := eq (or ($.Env.ENABLE_IPV6) "") "true" }}
server {
    server_name _; # This is just an invalid value which will never trigger on a real hostname.
    listen {{ $external_http_port }};
    {{ if $enable_ipv6 }}
    listen [::]:{{ $external_http_port }};
    {{ end }}
    {{ $access_log }}
    return 503;
}

{{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
    server_name _; # This is just an invalid value which will never trigger on a real hostname.
    listen {{ $external_https_port }} ssl http2;
    {{ if $enable_ipv6 }}
    listen [::]:{{ $external_https_port }} ssl http2;
    {{ end }}
    {{ $access_log }}
    return 503;

    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_certificate /etc/nginx/certs/default.crt;
    ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}

{{ range $host, $containers := groupByMulti $ "Env.VIRTUAL_HOST" "," }}

{{ $host := trim $host }}
{{ $is_regexp := hasPrefix "~" $host }}
{{ $upstream_name := when $is_regexp (sha1 $host) $host }}

# {{ $host }}
upstream {{ $upstream_name }} {

{{ range $container := $containers }}
    {{ $addrLen := len $container.Addresses }}

    {{ range $knownNetwork := $CurrentContainer.Networks }}
        {{ range $containerNetwork := $container.Networks }}
            {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }}
                ## Can be connected with "{{ $containerNetwork.Name }}" network

                {{ $chosts := split $container.Env.VIRTUAL_HOST "," }}
                                {{ $cports := split $container.Env.VIRTUAL_PORT "," }}

                {{/* if multiple virtual_host and virtual_ports specified for the container */}}
                {{ if and ( gt (len $chosts) 1 )  ( eq (len $chosts) (len $cports) ) }}
                {{ range $chix, $chost := $chosts }}
                {{ if eq $chost $host }}
                    {{ $port := index $cports $chix }}
                    {{ $address := coalesce (where $container.Addresses "Port" $port | first) (dict "Port" $port) }}

                    {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork ) }}
                {{ end }}
                {{ end }}   
                {{/* If only 1 port exposed, use that */}}
                {{ else if eq $addrLen 1 }}
                    {{ $address := index $container.Addresses 0 }}
                    {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }}
                {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}}
                {{ else }}
                    {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }}
                    {{ $address := where $container.Addresses "Port" $port | first }}
                    {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork) }}
                {{ end }}
            {{ else }}
                # Cannot connect to network of this container
                server 127.0.0.1 down;
            {{ end }}
        {{ end }}
    {{ end }}
{{ end }}
}

{{ $default_host := or ($.Env.DEFAULT_HOST) "" }}
{{ $default_server := index (dict $host "" $default_host "default_server") $host }}

{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}}
{{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }}

{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}}
{{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }}

{{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}}
{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) (or $.Env.HTTPS_METHOD "redirect") }}

{{/* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to empty string (use default) */}}
{{ $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "" }}

{{/* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000" */}}
{{ $hsts := or (first (groupByKeys $containers "Env.HSTS")) (or $.Env.HSTS "max-age=31536000") }}

{{/* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}}
{{ $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }}

{{/* Get the first cert name defined by containers w/ the same vhost */}}
{{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }}

{{/* Get the best matching cert  by name for the vhost. */}}
{{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}}

{{/* vhostCert is actually a filename so remove any suffixes since they are added later */}}
{{ $vhostCert := trimSuffix ".crt" $vhostCert }}
{{ $vhostCert := trimSuffix ".key" $vhostCert }}

{{/* Use the cert specified on the container or fallback to the best vhost match */}}
{{ $cert := (coalesce $certName $vhostCert) }}

{{ $is_https := (and (ne $https_method "nohttps") (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }}

{{ if $is_https }}

{{ if eq $https_method "redirect" }}
server {
    server_name {{ $host }};
    listen {{ $external_http_port }} {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:{{ $external_http_port }} {{ $default_server }};
    {{ end }}
    {{ $access_log }}

    # Do not HTTPS redirect Let'sEncrypt ACME challenge
    location /.well-known/acme-challenge/ {
        auth_basic off;
        allow all;
        root /usr/share/nginx/html;
        try_files $uri =404;
        break;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
{{ end }}

server {
    server_name {{ $host }};
    listen {{ $external_https_port }} ssl http2 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }};
    {{ end }}
    {{ $access_log }}

    {{ if eq $network_tag "internal" }}
    # Only allow traffic from internal clients
    include /etc/nginx/network_internal.conf;
    {{ end }}

    {{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }}

    ssl_session_timeout 5m;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }};
    ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }};

    {{ if (exists (printf "/etc/nginx/certs/%s.dhparam.pem" $cert)) }}
    ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }};
    {{ end }}

    {{ if (exists (printf "/etc/nginx/certs/%s.chain.pem" $cert)) }}
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate {{ printf "/etc/nginx/certs/%s.chain.pem" $cert }};
    {{ end }}

    {{ if (not (or (eq $https_method "noredirect") (eq $hsts "off"))) }}
    add_header Strict-Transport-Security "{{ trim $hsts }}" always;
    {{ end }}

    {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
    include {{ printf "/etc/nginx/vhost.d/%s" $host }};
    {{ else if (exists "/etc/nginx/vhost.d/default") }}
    include /etc/nginx/vhost.d/default;
    {{ end }}

    location / {
        {{ if eq $proto "uwsgi" }}
        include uwsgi_params;
        uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else if eq $proto "fastcgi" }}
        root   {{ trim $vhost_root }};
        include fastcgi_params;
        fastcgi_pass {{ trim $upstream_name }};
        {{ else if eq $proto "grpc" }}
        grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else }}
        proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ end }}

        {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
        auth_basic  "Restricted {{ $host }}";
        auth_basic_user_file    {{ (printf "/etc/nginx/htpasswd/%s" $host) }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
        include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
        {{ else if (exists "/etc/nginx/vhost.d/default_location") }}
        include /etc/nginx/vhost.d/default_location;
        {{ end }}
    }
}

{{ end }}

{{ if or (not $is_https) (eq $https_method "noredirect") }}

server {
    server_name {{ $host }};
    listen {{ $external_http_port }} {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:80 {{ $default_server }};
    {{ end }}
    {{ $access_log }}

    {{ if eq $network_tag "internal" }}
    # Only allow traffic from internal clients
    include /etc/nginx/network_internal.conf;
    {{ end }}

    {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }}
    include {{ printf "/etc/nginx/vhost.d/%s" $host }};
    {{ else if (exists "/etc/nginx/vhost.d/default") }}
    include /etc/nginx/vhost.d/default;
    {{ end }}

    location / {
        {{ if eq $proto "uwsgi" }}
        include uwsgi_params;
        uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else if eq $proto "fastcgi" }}
        root   {{ trim $vhost_root }};
        include fastcgi_params;
        fastcgi_pass {{ trim $upstream_name }};
        {{ else if eq $proto "grpc" }}
        grpc_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ else }}
        proxy_pass {{ trim $proto }}://{{ trim $upstream_name }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }}
        auth_basic  "Restricted {{ $host }}";
        auth_basic_user_file    {{ (printf "/etc/nginx/htpasswd/%s" $host) }};
        {{ end }}
        {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }}
        include {{ printf "/etc/nginx/vhost.d/%s_location" $host}};
        {{ else if (exists "/etc/nginx/vhost.d/default_location") }}
        include /etc/nginx/vhost.d/default_location;
        {{ end }}
    }
}

{{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }}
server {
    server_name {{ $host }};
    listen {{ $external_https_port }} ssl http2 {{ $default_server }};
    {{ if $enable_ipv6 }}
    listen [::]:{{ $external_https_port }} ssl http2 {{ $default_server }};
    {{ end }}
    {{ $access_log }}
    return 500;

    ssl_certificate /etc/nginx/certs/default.crt;
    ssl_certificate_key /etc/nginx/certs/default.key;
}
{{ end }}

{{ end }}
{{ end }}
maxnoe commented 2 years ago

This would be great, some services expose multiple ports that need to each have their own virtualhost assigned to.

maxkratz commented 2 years ago

This is also interesting for MinIO S3, because the service exposes a management interface on a separate port: https://docs.min.io/docs/minio-docker-quickstart-guide.html

tkw1536 commented 2 years ago

This issue looks like a duplicate of #1463.

For a workaround, see https://github.com/nginx-proxy/nginx-proxy/issues/1463#issuecomment-652351459.

pini-gh commented 1 year ago

I'm changing my mind about this topic.

For services exposing more that one port it should be possible to declare only one host and have nginx-proxy set one upstream block per port. The routing can then be handled depending on the location.

To this end I use a new environment variable VIRTUAL_MULTIPORT with this syntax:

VIRTUAL_MULTIPORT = port, { ",",  port };
port = <virtual_port> [ ":",<virtual_ path> [ ":", <virtual_dest> ]]

<virtual_port>, <virtual_path>, and <virtual_dest> accept the same values than VIRTUAL_{PORT,PATH,DEST}. VIRTUAL_MULTIPORT must not be used along VIRTUAL_{PORT,PATH,DEST} for the same service.

Example:

VIRTUAL_MULTIPORT: "9220:~ ^/(admin|fonts?|images|webmin)/,10901,20901:/ws2p,30901:/gva/playground"

The above example will create four upstream blocks:

# multiport.example.com:10901/
upstream multiport.example.com-10901 {
        ## Can be connected with "docker-gen-bridge" network
        # blah
        server 172.28.0.5:10901;
}
# multiport.example.com:20901/ws2p
upstream multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901 {
        ## Can be connected with "docker-gen-bridge" network
        # blah
        server 172.28.0.5:20901;
}
# multiport.example.com:30901/gva/playground
upstream multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901 {
        ## Can be connected with "docker-gen-bridge" network
        # blah
        server 172.28.0.5:30901;
}
# multiport.example.com:9220~ ^/(admin|fonts?|images|webmin)/
upstream multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220 {
        ## Can be connected with "docker-gen-bridge" network
        # blah
        server 172.28.0.5:9220;
}

And four location blocs for the related server multiport.example.com:

server {
        server_name multiport.example.com;
        listen 443 ssl http2  ;
        access_log /var/log/nginx/access.log vhost;
        ssl_session_timeout 5m;
        ssl_session_cache shared:SSL:50m;
        ssl_session_tickets off;
        ssl_certificate /etc/nginx/certs/default.crt;
        ssl_certificate_key /etc/nginx/certs/default.key;
        add_header Strict-Transport-Security "max-age=31536000" always;
        location / {
                proxy_pass http://multiport.example.com-10901;
        }
        location /ws2p {
                proxy_pass http://multiport.example.com-5c7ebef820fe004e45e3af1d0c47971594d028b2-20901;
                include /etc/nginx/vhost.d/multiport.example.com_5c7ebef820fe004e45e3af1d0c47971594d028b2_location;
        }
        location /gva/playground {
                proxy_pass http://multiport.example.com-1f02ce2421b17d828edaabfc3014360891bb0be3-30901;
        }
        location ~ ^/(admin|fonts?|images|webmin)/ {
                proxy_pass http://multiport.example.com-cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2-9220;
                include /etc/nginx/vhost.d/multiport.example.com_cae8bfc2ea1fe6bb6fda08727ab065e8fed98aa2_location;
        }
}

As with the VIRTUAL_PATH it is possible to define per path location configuration files.

This fulfill my needs for the few multiport services I host.

Method Pros Cons
Per hostname ports Must declare a fake host for each added port
Pollute the nginx configuration with fake servers
Cannot work without setting up custom nginx location files
VIRTUAL_MULTIPORT Default routing through generated location blocs
Simplest configurations work without custom location files
Yet another environment variable

@buchdag Would you consider a MR provided I write some documentation and test cases?

pini-gh commented 1 year ago

I've found a way to use multiport syntax with VIRTUAL_PORT while keeping compatibility with legacy syntax. This way there is no more need to introduce a new variable.

rhansen commented 1 year ago

@pini-gh It sounds like you are proposing support for different container ports for different location blocks, but all virtual hosts are configured identically. I think this bug is originally about having different virtual hosts proxy to different ports in a single application container. It would be good to come up with a scheme that is general enough to support both. For example, it should be possible to configure nginx-proxy like this:

If there was no need to support regular expressions, then I think the above example could be expressed like:

VIRTUAL_HOST: "https://foo.example/a/=>/xyz/,https://foo.example/b/=>81,https://bar.example/=>https:4430”

The above is a comma-separated list, where the left side of the => of each entry describes handled URLs and the right side describes how requests for those URLs are passed to the application container (default protocol is http, default IP is the container's IP if it has only one, default port depends on the protocol, and default destpath is the empty string).

Thoughts?

pini-gh commented 1 year ago

@rhansen:

I think this bug is originally about having different virtual hosts proxy to different ports in a single application container.

You're right. But the main problem people express in this thread is dealing with containers exposing more than one port as you can see in these quotes:

I want to proxy the sonatype/nexus3 container, which starts the docker registry on a separate port from the standard service (8082 for docker vs 8081 for maven).

This would be great, some services expose multiple ports that need to each have their own virtualhost assigned to.

This is also interesting for MinIO S3, because the service exposes a management interface on a separate port

Routing to the different ports via dedicated paths seems good enough a solution to me.

I understand one may prefer using different virtual hosts for different ports of the same container. I plan to address this issue as well, but I'd like not to mix it with the current PR.

pini-gh commented 1 year ago

Thinking about it, it is easy to mix both solutions:

For virtual hosts defined with a dedicated port (say app.example.com:1234), the variable VIRTUAL_PORT is ignored and the dedicated port is used as its virtual port.

Say we have a container with:

VIRTUAL_HOST: "web.nginx-proxy.tld, web1.nginx-proxy.tld:84"
VIRTUAL_PORT: "80,81:/81:/,82:/82:/port,83:~ ^/[8][3]"

It would generate something like this for virtual host web1.nginx-proxy.tld:

# web1.nginx-proxy.tld
upstream web1.nginx-proxy.tld_84 {
        ## Can be connected with "bridge" network
        # test_multiport_syntax_web_1
        server 172.17.0.2:84;
}
server {
        server_name web1.nginx-proxy.tld;
        listen 80 ;
        access_log /var/log/nginx/access.log vhost;
        location / {
                proxy_pass http://web1.nginx-proxy.tld_84;
        }
}
rhansen commented 1 year ago

But the main problem people express in this thread is dealing with containers exposing more than one port as you can see in these quotes:

I agree that there's a need to handle containers that expose multiple ports. I'm just calling attention to how those multiple ports are handled. I interpret the comments as calling for separate virtual hosts for each upstream port, not separate paths (of a common virtual host) for each upstream port:

I need to be able to configure two hostnames for the proxy, with two separate upstream ports.

multiple ports that need to each have their own virtualhost assigned to.

PR #2130 only supports a single virtual host (or multiple identical virtual hosts) for all upstream ports, demuxed by path. That is not sufficient for the original bug report.

Routing to the different ports via dedicated paths seems good enough a solution to me.

I disagree that it is good enough. Many applications have a hard-coded path scheme that does not support namespacing by path. For example, some users must do https://foo.example/ and https://bar.example/ for two upstream ports, not https://example/foo/ and https://example/bar/.

I do think that path-based routing (like #2130) is useful, just not as useful as host-based routing. Regardless of relative importance, the solution for path-based routing should not make it awkward to solve the host-based routing problem, and vice-versa. That's why I would like to find a more general syntax than the syntax in #2130.

pini-gh commented 1 year ago

See my last answer just above yours.

rhansen commented 1 year ago

For virtual hosts defined with a dedicated port (say app.example.com:1234), the variable VIRTUAL_PORT is ignored

To make sure I understand your proposal, you're saying that any VIRTUAL_HOST without a port uses VIRTUAL_PORT as in #2130. Right?

pini-gh commented 1 year ago

Indeed. See the branch pini-multiport-syntax-plus-per-host-port-syntax on my repo.

pini-gh commented 1 year ago

Easy:

VIRTUAL_HOST = foo.example, bar.example:4430
VIRTUAL_PORT = 80:/a:/xyz,81:b
rhansen commented 1 year ago

How would you express this example:

  • https://foo.example/a/ proxies to http://app:80/xyz/
  • https://foo.example/b/ proxies to http://app:81/b/
  • https://bar.example/a/ proxies to https://app:443/
  • https://bar.example/b/ proxies to https://app:444/

(I deleted my original example because I realized that would be easy to express.)

pini-gh commented 1 year ago

I can't. But I fail to link it to any real life example I've encountered so far.

Either you choose the by path routing strategy, or the multi host with respective dedicated ports strategy. You can mix both but for the latter you cannot reuse the same host for different dedicated ports in the same container.

pini-gh commented 1 year ago

I can't

Actually I could:

VIRTUAL_HOST = foo.example, bar.example:443, baz.example:444
VIRTUAL_PORT = 80:/a:/xyz,81:b
VIRTUAL_PATH = /a
VIRTUAL_DEST = /

plus a custom location file for bar.example to proxy pass location /b to upstream baz.example/.

rhansen commented 1 year ago

What's the downside to the syntax I proposed in https://github.com/nginx-proxy/nginx-proxy/issues/1504#issuecomment-1379464211?

Here is an attempt at formalizing it (ABNF, with host, reg-name, port, and path as defined in RFC 3986 and uri defined by nginx):

VirtualHosts      = VhostSpec *(Sep VhostSpec) [Sep]
Sep               = *SP ("," / CR / CRLF) *SP
VhostSpec         = [Proto "://"] reg-name ["@" host] [":" port] [Location] [UpstreamSpec]
Proto             = "http" / "https"
Location          = [("=" / "~" / "~*" / "^~") SP] uri
UpstreamSpec      = *SP "=>" *SP [UpstreamProtoPort] [path]
UpstreamProtoPort = UpstreamProto
                  / port
                  / UpstreamProto ":" port
UpstreamProto     = Proto / "uwsgi" / "fastcgi" / "grpc"

With this syntax, the example in https://github.com/nginx-proxy/nginx-proxy/issues/1504#issuecomment-1381068640 can be expressed in YAML as:

VIRTUAL_HOST: |
  https://foo.example/a/ => /xyz/
  https://foo.example/b/ => 81/b/
  https://bar.example/a/ => https/
  https://bar.example/b/ => https:444/

The above is getting dangerously close to inventing a new markup language, and docker-gen can already decode JSON, so maybe we should just accept a JSON object (used as a map from string to string) or a jSON array of strings.

pini-gh commented 1 year ago

What's the downside to the syntax I proposed in #1504 (comment)?

I don't know. I just did differently and I like my solution because it is backward compatible with current nginx-proxy version. I don't think any PR breaking backward compatibility has any chance to be accepted.

Feel free to give a try at implementing your proposal. You'll understand then that the Go Template feature set of docker-gen is not that simple to use :)

You'll have to consider the actual use cases where the same virtual host appears in several containers with different configurations.

rhansen commented 1 year ago

I don't know. I just did differently and I like my solution because it is backward compatible with current nginx-proxy version. I don't think any PR breaking backward compatibility has any chance to be accepted.

My proposal is also backwards compatible. (JSON would not be, though it would be easy to add compatibility: if JSON parsing fails, fall back to parsing the string as a comma-separated list of hostnames.)

Feel free to give a try at implementing your proposal. You'll understand then that the Go Template feature set of docker-gen is not that simple to use :)

I have an implementation about 80% done, including support for multiple containers in an upstream and multiple upstreams for the same virtual host. Before I spend more time on it, I would like to get input on this issue from more people.

rhansen commented 1 year ago

Heh, #259 is almost identical to what I propose, just simplified.

pini-gh commented 1 year ago

Before I spend more time on it, I would like to get input on this issue from more people.

I have no problem using whichever implementation provided it is backward compatible and supports both strategies (per path or per dedicated vhost routing). Thanks for working on this :)

buchdag commented 1 year ago

@rhansen @pini-gh maybe we should convert this into a discussion or create one and try to gather feedback there ?

The above is getting dangerously close to inventing a new markup language, and docker-gen can already decode JSON, so maybe we should just accept a JSON object (used as a map from string to string) or a jSON array of strings.

I tend to agree on this one, if we're getting to the point where we're trying to parse a string into a complex object, we might as well stick to proven JSON rather than re-invent the wheel ourselves in an effort to emulate the "look and feel" of the already existing configuration variables.

JSON also plays quite well with multi line YAML in compose files (syntax is just for the example):

version: '3'

services:
  app:
    #[...]
    environment:
      VIRTUAL_HOST: >
        [
          {
            "virtual": {
              "host": "foo.example.com",
              "path": "/a"
            },
            "upstream": {
              "path": "/xyz"
            }
          },
          {
            "virtual": {
              "host": "foo.example.com",
              "path": "/b"
            },
            "upstream": {
              "path": "/b",
              "port": 81
            }
          },
          {
            "virtual": {
              "host": "bar.example",
              "path": "/a"
            },
            "upstream": {
              "proto": "https"
            }
          },
          {
            "virtual": {
              "host": "bar.example",
              "path": "/b"
            },
            "upstream": {
              "proto": "https",
              "port": 444
            }
          }
        ]

If JSON with comma-separated list parsing as a fallback is not possible, I think I'm more inclined toward @rhansen / #259 like syntax with some simplification if they're possible, like dropping the VhostSpec proto.

Given that VIRTUAL_PATH is fairly recent I believe that most people will mostly want vhost based routing over vpath based routing for multiple ports containers, but that's not much more that a feeling. I agree with @pini-gh that we should support both.

maxkratz commented 1 year ago

Given that VIRTUAL_PATH is fairly recent I believe that most people will mostly want vhost based routing over vpath based routing for multiple ports containers, but that's not much more that a feeling. I agree with @pini-gh that we should support both.

I agree (at least for my use cases). My example is the minio docker image: It serves two ports: One for the actual S3 access, e.g., as a storage backend for other services, and one as a management console. At least for my setup, I want to be able to easily configure nginx-proxy to serve two sub domains:

pini-gh commented 1 year ago

How this format instead which is closer to the legacy VITRUAL_{HOST,PORT,PATH,DEST,PROTO} ?

version: '3'

services:
  app:
    #[...]
    environment:
      VIRTUAL_HOST: >
        [
          {
            "host": "foo.example.com",
            "path": "/a",
            "dest": "/xyz"
          },
          {
            "host": "foo.example.com",
            "path": "/b"
            "dest": "/b",
            "port": 81
          },
          {
            "host": "bar.example",
            "path": "/a",
            "proto": "https"
          },
          {
            "host": "bar.example",
            "path": "/b",
            "proto": "https",
            "port": 444
          }
        ]
buchdag commented 1 year ago

@pini-gh that works for me too, that's probably less confusing for existing users and easier to use.

maxnoe commented 1 year ago

At that point, you could leave VIRTUAL_HOST as it is and introduce a new VIRTUAL_HOST_JSON_CONFIG or similar?

That would remove the need for some kind of "detection code" for whether the users passed in a simple or a json config

rhansen commented 1 year ago

At that point, you could leave VIRTUAL_HOST as it is and introduce a new VIRTUAL_HOST_JSON_CONFIG or similar?

Given that we want to move to labels (#2148), we could leave the VIRTUAL_HOST environment variable alone (except deprecated) and introduce a new JSON-only label that supercedes VIRTUAL_HOST. This would also avoid the need to fall back to an alternative parsing if JSON parsing fails.

VincentSC commented 1 year ago

@rhansen I looked into labels and I'm not sure this will be an easy introduction. I'd suggest to leave that problem out, to avoid delaying this feature.

IMO, the solution that @pini-gh suggested is covering all problems discussed in this and comparable issues, with the note that I do not really understand the dest usecase.

I would strongly suggest to use YAML Arrays of Objects instead, to avoid mixing up YAML and JSON:

VIRTUAL_HOST:
   - host: foo.example.com
     path: /a
     dest: /xyz
   - host: foo.example.com
     path: /b
     dest: /b
     port: 81
   - host: bar.example
     path: /a
     proto: https
   - host: bar.example
     path: /b
     proto: https
     port: 444
rhansen commented 1 year ago

@VincentSC

I looked into labels and I'm not sure this will be an easy introduction. I'd suggest to leave that problem out, to avoid delaying this feature.

Labels are easy to deal with; see #1934 for an example. The tough part is the infrastructure changes required to collect and organize all of the information before outputting anything, and maintaining backwards compatibility.

I would strongly suggest to use YAML Arrays of Objects instead, to avoid mixing up YAML and JSON:

That would be my preference too, but docker-gen does not currently support YAML parsing. We could add support, but I don't want that to block this. Fortunately, YAML (v1.2) is a superset of JSON, so we can extend this new feature to support YAML later without breaking backwards compatibility.

VincentSC commented 1 year ago

@rhansen

Labels are easy to deal with; see #1934 for an example. The tough part is the infrastructure changes required to collect and organize all of the information before outputting anything, and maintaining backwards compatibility.

I'm very focused on scope-creep. Also "this solves X and only X" is easier to accept than "this is all work of the past months". But if you say that it's a non-issue, then I should not be worried that it delays the acceptance of this important PR.

That would be my preference too, but docker-gen does not currently support YAML parsing. We could add support, but I don't want that to block this. Fortunately, YAML (v1.2) is a superset of JSON, so we can extend this new feature to support YAML later without breaking backwards compatibility.

Thanks for this explanation.

VincentSC commented 1 year ago

Did some thinking. We're actually solving badly designed dockers, and adding the complexity to nginx-proxy. What if the complexity is solved by virtually splitting dockers, such that nginx-proxy can remain straightforward?

All below is untested! Just my notes for different approaches. I will update this issue when I have actually implemented it.

NOPE, NOT WORKING - I MISUNDERSTOOD. A possible solution for a docker with two ports, is to use a dummy docker and use this trick:

splitoffport:
   image: alpine:latest
   command: /bin/sh -c "tail -f /dev/null"
   extra_hosts:
      - "webhost:127.0.0.1"
   environment:
       - VIRTUAL_HOST=foo.bar.com
       - VIRTUAL_PORT=8080
   network:
      - nginx-proxy

So here the additional docker is only there for ensuring separating of concerns, again.

This docker can be even smaller, when using FROM scratch.

A possible work-around for a docker with two sub-paths is a double proxy. Here is an example for two ports (as this is my primary problem), but this could be reworked to handle paths.

This config would separate webhost:8008 and then forward to nginx-proxy to make it reachable via https://foo.bar.com:

httpsimpleproxy:
   image: picoded/http-simple-proxy:latest
   environment:
    - FORWARD_HOST=webhost
    - FORWARD_PORT=8008
    - FORWARD_PROT="http"
    - PROXY_READ_TIMEOUT=600
    - VIRTUAL_HOST=foo.bar.com
  network:
    - nginx-proxy

I find the double proxy not ideal.

When wanting to have a separate port (using https, like https://foo.bar.com:10443), an additional nginx-proxy can be created with custom external ports on a separate network. No idea if this works with Let's Encrypt, but maybe certain folders can just be shared between the two.

Would these approaches work for handling all special cases discussed in this thread? Is there an option to have a separate project "nginx-proxy/splitoff" that can handle different types of separattion?

buchdag commented 11 months ago

That would be my preference too, but docker-gen does not currently support YAML parsing. We could add support, but I don't want that to block this. Fortunately, YAML (v1.2) is a superset of JSON, so we can extend this new feature to support YAML later without breaking backwards compatibility.

There is an open PR for YAML parsing in Sprig, but unfortunately it hasn't moved since february and I have my doubts that Sprig is still actively maintained.

It should however be doable to add this directly to docker-gen.

rachmataditiya commented 10 months ago

I have real case example for this feature, this is odoo deployment from odoo official website:

#odoo server
upstream odoo {
  server 127.0.0.1:8069;
}
upstream odoochat {
  server 127.0.0.1:8072;
}
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

# http -> https
server {
  listen 80;
  server_name odoo.mycompany.com;
  rewrite ^(.*) https://$host$1 permanent;
}

server {
  listen 443 ssl;
  server_name odoo.mycompany.com;
  proxy_read_timeout 720s;
  proxy_connect_timeout 720s;
  proxy_send_timeout 720s;

  # SSL parameters
  ssl_certificate /etc/ssl/nginx/server.crt;
  ssl_certificate_key /etc/ssl/nginx/server.key;
  ssl_session_timeout 30m;
  ssl_protocols TLSv1.2;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;

  # log
  access_log /var/log/nginx/odoo.access.log;
  error_log /var/log/nginx/odoo.error.log;

  # Redirect websocket requests to odoo gevent port
  location /websocket {
    proxy_pass http://odoochat;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
  }

  # Redirect requests to odoo backend server
  location / {
    # Add Headers for odoo proxy mode
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_redirect off;
    proxy_pass http://odoo;
  }

  # common gzip
  gzip_types text/css text/scss text/plain text/xml application/xml application/json application/javascript;
  gzip on;
}

https://www.odoo.com/documentation/16.0/administration/install/deploy.html how can I implement it wtih this docker container with multiple port?

version: '3.1'
services:
  web:
    image: odoo:16.0
    depends_on:
      - mydb
    ports:
      - "8069:8069"
      - "8072:8072"
    environment:
    - HOST=mydb
    - USER=odoo
    - PASSWORD=myodoo
  mydb:
    image: postgres:15
    environment:
      - POSTGRES_DB=postgres
      - POSTGRES_PASSWORD=myodoo
      - POSTGRES_USER=odoo
CtrlCarlitos commented 6 months ago

First of all, I would like to thank the team behind this magnificent project!!! It's really cool and saves so much work at the development stage as well as in the production stage.

I work with odoo. I tried modifying the nginx.tmpl to accommodate the generation of the nginx configuration for odoo. It has been pointed out by @rachmataditiya here.

My workaround is very simple:

  nginx:
    image: nginxproxy/nginx-proxy:latest
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    healthcheck:
      test: service nginx status || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - ./.nginx/odoo.conf:/etc/nginx/conf.d/odoo.conf
      - ./.nginx/nginx.tmpl:/app/nginx.tmpl
      - ./.certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro

This allows for very precise and specific fine tunning for the odoo service. All the other services (portainer, pgadmin, and roundcube) in my docker compose file are generated flawlessly.

Is there a way to specify which template to use for a specific service? Maybe this is the best way to control the generation of specialized services like odoo?

The docker-mailserver is another service that is a bit wild to get the nginx configuration as required.

buchdag commented 3 months ago

I'd like to resume work on this.

Regarding @VincentSC proposal to use YAML instead of JSON, this syntax is not accepted by compose:

VIRTUAL_HOST_YAML:
   - host: foo.example.com
   - host: bar.example.com
     port: 81
services.foo.environment.VIRTUAL_HOST_YAML must be a string, number, boolean or null

Converting it a to a multiline string (with conservation of the newlines) works

VIRTUAL_HOST_YAML: |
   - host: foo.example.com
   - host: bar.example.com
     port: 81

It result in this string - host: foo.example.com\n- host: bar.example.com\n port: 8080, which is correctly parsed by docker-gen with an added fromYaml function.

My only issue is that it's rather unpleasant and error prone (because of the sensitivity to white spaces) to write directly on a terminal, unlike its JSON equivalent [{"host": "foo.example.com"},{"host": "bar.example.com","port": 8080}], but maybe that's just me.

I think that's a non issue anyway because the ability to parse both JSON and YAML in the template would be trivial. I tested with this compose file:

version: "3"

services:
  test1:
    image: nginx:alpine
    container_name: test1
    environment:
      VIRTUAL_HOST_NEW: |-
        [
          {
            "host": "foo.example.com"
          },{
            "host": "bar.example.com",
            "port": 8080
          }
        ]
  test2:
    image: nginx:alpine
    container_name: test2
    environment:
      VIRTUAL_HOST_NEW: |-
        - host: foo.example.com
        - host: bar.example.com
          port: 8080

and this template:

{{ range $host, $containers := groupBy $ "Env.VIRTUAL_HOST_NEW" }}
    {{ range $containers }}
        {{ .Name }}
    {{ end }}
    {{ $decoded_host := or (fromJson $host) (fromYaml $host) }}
    {{ $decoded_host }}
{{ end }}

resulting in:

test2
[map[host:foo.example.com] map[host:bar.example.com port:8080]]
test1
[map[host:foo.example.com] map[host:bar.example.com port:8080]]

@pini-gh do you think #2130 would be adaptable to a new env var / label with JSON / YAML syntax instead of the backward compatible VIRTUAL_HOST syntax, or should we start from scratch ?

VincentSC commented 3 months ago

Use yaml like this:

Blah:
  - one=1
  - two=2
  - three=3

or

Blah:
  one: 1
  two: 2
  three: 3

Just pick one, but as you found out, mixing the two layouts does not work.

pini-gh commented 3 months ago

@pini-gh do you think #2130 would be adaptable to a new env var / label with JSON / YAML syntax instead of the backward compatible VIRTUAL_HOST syntax, or should we start from scratch ?

I don't know. I'll have to think about it.

The thing that bothers me is how do you handle the case where the same virtual host is defined in separate containers with mixed syntaxes? Say legacy VIRTUAL_HOST in the first container, and VIRTUAL_HOST_NEW in a second container.

maxnoe commented 3 months ago

My only issue is that it's rather unpleasant and error prone (because of the sensitivity to white spaces) to write directly on a terminal, unlike its JSON equivalent [{"host": "foo.example.com"},{"host": "bar.example.com","port": 8080}], but maybe that's just me.

Just a note because it seems you are not aware of this: YAML is a superset of JSON, any valid json document is also a valid yaml document, so if you use fromYAML, also your json example should parse correctly and would be the nicer option for a cli.

You could even use your version with brackets and curly braces and leave out the quotes around the string keys, using python:

In [1]: import yaml

In [2]: hosts_yaml_longform = """
   ...: - host: foo.example.com
   ...: - host: bar.example.com
   ...:   port: 8080
   ...: """

In [3]: hosts_yaml_shortform = '[{host: foo.example.com}, {host: bar.example.com, port: 8080}]'

In [4]: hosts_json = '[{"host": "foo.example.com"}, {"host": "bar.example.com", "port": 8080}]'

In [5]: yaml.safe_load(hosts_yaml_longform)
Out[5]: [{'host': 'foo.example.com'}, {'host': 'bar.example.com', 'port': 8080}]

In [6]: yaml.safe_load(hosts_yaml_shortform)
Out[6]: [{'host': 'foo.example.com'}, {'host': 'bar.example.com', 'port': 8080}]

In [7]: yaml.safe_load(hosts_json)
Out[7]: [{'host': 'foo.example.com'}, {'host': 'bar.example.com', 'port': 8080}]
VincentSC commented 3 months ago

Any suggestion to have it all without "blocks"? The \ can be avoided and will reduce future "why doesn't this work?"-issues.

buchdag commented 3 months ago

@maxnoe I was aware that YAML is a superset of JSON but I did not even think to test this, and indeed fromYAML does parse JSON perfectly. Thanks for the reminder ! 🙏

@VincentSC I'm not sure I understood your question, are you asking if there is a way to drop the |- in the compose file ?

The thing that bothers me is how do you handle the case where the same virtual host is defined in separate containers with mixed syntaxes? Say legacy VIRTUAL_HOST in the first container, and VIRTUAL_HOST_NEW in a second container.

Do you have a use case with this in mind ? 🤔

I'll think about it too.

maxnoe commented 3 months ago

The thing that bothers me is how do you handle the case where the same virtual host is defined in separate containers with mixed syntaxes? Say legacy VIRTUAL_HOST in the first container, and VIRTUAL_HOST_NEW in a second container.

Is this an issue? I'd expect that you parse the configs independent of the way how they are passed and check if there is a conflict between all the configured hosts, right?

buchdag commented 3 months ago

I'd expect that you parse the configs independent of the way how they are passed and check if there is a conflict between all the configured hosts, right?

We don't really check for conflicts, nginx-proxy generate nginx configuration with a template processor, so we're limited in the logic we can implement without making the template file an arcane unmaintainable mess (I feel like we're already stretching it a bit, and as @pini-gh pointed out in https://github.com/nginx-proxy/nginx-proxy/issues/1504#issuecomment-1381182204 implementing new advanced features is not an easy task).

The repo language is displayed as ~85% Python on GitHub because our test suite is in Python, and is way bigger than the actual template file it is testing. The template file itself is written in Go template.

pini-gh commented 3 months ago

Do you have a use case with this in mind ? 🤔

Not for myself. I just know that it is a nginx-proxy feature to allow setting the same virtual host for different containers, and I wonder how the template should behave if only a subset of the related containers is migrated to the new syntax.

buchdag commented 3 months ago

@pini-gh maybe we could start by defining the data structure we'd ideally like to iter over in the template, then try to populate this data structure correctly with both the legacy variables and the "new" one ? "Ideally" as in what would both make the most sense and be easier to manipulate within a go template, with docker-gen and sprig functions.

pini-gh commented 3 months ago

As I understand it the data structure could be:

map:
  key: virtual_host
  value: map:
    key: virtual_path
    value: list [ container ]

From each container we need to obtain:

* container_ip
* virtual_port
* virtual_dest

The caveat is that for a given virtual_host + virtual_path we cannot handle different virtual_dest values. Am I correct? If so I propose to use the first in the row, and issue a warning for every differing values.

buchdag commented 3 months ago

You're correct and that's already what we do for other features like keepalive (take the config value from the first container in the list), but without a warning.

pini-gh commented 3 months ago

From each container we need to obtain:

* container_ip
* virtual_port
* virtual_dest

We might want to handle several ports for the same container:

* container_ip
* map:
    key: virtual_port
    value: virtual_dest
pini-gh commented 2 months ago

Thinking again about it and testing, I've came up with this data structure:

vhosts (map):
- key: virtual_host
- value (struct):
  - paths (map):
    - key: virtual_path
    - value (struct):
      - dest: virtual_dest (the first in the row)
      - containers (map):
        - key: container_id
        - value (struct):
          - port:  virtual_port
          - dest: virtual_dest
          - ip: container_ip
          - ip_debug: debug text from the 'container_ip' template

Of course some data is omitted, but this structure reflects what we want, as I understand it.

buchdag commented 2 months ago

YAML parsing functions added to docker-gen and docker-gen version updated in nginx-proxy.