Adhin37
Adhin37

Reputation: 57

Expose multiple api uri on the same nginx server block

Goal

My goal is to setup multiple backend api container exposed on the same nginx server :

My backend container are based on php:7.2-fpm (symfony hosted on every apache container) and they don't have any route called api/${NAME_SERVICE} (i don't want to create some useless parent route in my backends).

So, my request is simple, i want that when i call for example :

That my backend account container serve this uri :

What i tried so far

Conf

Here is my nginx.conf :

...
server {
        server_name ~.*;
        client_max_body_size 50m;

        location / {
            try_files $uri /index.php$is_args$args;
        }
        # work if i want to serve account-service on http://localhost:80/
        # location ~ ^/index\.php(/|$) {
        #     fastcgi_pass account-service:9000;
        #     fastcgi_buffers 16 16k;
        #     fastcgi_buffer_size 32k;
        #     fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        #     include fastcgi_params;
        # }

        # my last attempt with alias
        location ~* ^/api/account {
            alias /;
            index index.php;
            
            location ~ index\.php(/|$) {
                fastcgi_pass account-service:9000;
                fastcgi_buffers 16 16k;
                fastcgi_buffer_size 32k;
                fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
                fastcgi_intercept_errors on;
                include fastcgi_params;
            }
        }
}
...

docker-compose.yml :

  nginx:
    image: nginx:1.15.3-alpine
    restart: on-failure
    volumes:
      - "./build/nginx/default.conf:/etc/nginx/nginx.conf:ro"
      - "./logs/nginx:/var/log/nginx"
    ports:
      - "80:80"
    depends_on:
      - account-service
      - account-db
      - cart-service
      - cart-db
      - order-service
      - order-db
      - product-service
      - product-db

  account-service:
    env_file:
      - config/account.env
    build: apps/account-service
    restart: on-failure
    expose:
      - "9000"
    volumes:
      - "./apps/account-service:/usr/src/app"
    depends_on:
      - account-db

  cart-service:
     ...

P.S: I known that you can split nginx conf into multiple server blocks that listen on different port/hostname, but that's not what i want to achieve here.

Upvotes: 0

Views: 2634

Answers (1)

Ivan Shatsky
Ivan Shatsky

Reputation: 15662

What do you mean by tweaking fastcgi_param REQUEST_URI? If you try to set some custom value to REQUEST_URI before you include the fastcgi_params file, value set by fastcgi_params would overwrite any of your tweakings:

fastcgi_pass service:9000;
fastcgi_param REQUEST_URI /some/path;
include fastcgi_params;
# REQUEST_URI passed as the real request URI

However this one would work as expected:

fastcgi_pass service:9000;
include fastcgi_params;
fastcgi_param REQUEST_URI /some/path;
# REQUEST_URI passed as "/some/path"

Trying to change this with rewrite won't work because the REQUEST_URI fastcgi parameter is set to $request_uri internal nginx variable value inside the fastcgi_params file, and that variable doesn't changed by rewrite directive rules, it is an $uri one that does.

Here is the most simple solution that should work:

server {
    ...
    location ~ ^/api(/(?:account|cart|order|product)/.*) {
        # strip "/api" part from the URI and search for the new location block
        rewrite ^ $1 last;
    }

    location /account {
        # strip "/account" part from the URI and continue processing within the current location block
        rewrite ^/account(.*) $1 break;
        # include default fastcgi parameters first
        include fastcgi_params;
        # all our tweakings goes after it
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        # use the rewrited $uri variable instead of the default $request_uri
        # $uri variable does not include query arguments, so add them manually if they exists
        fastcgi_param REQUEST_URI $uri$is_args$args;
        fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        fastcgi_intercept_errors on;
        fastcgi_pass account-service:9000;
    }
    location /cart {
        rewrite ^/cart(.*) $1 break;
        ...
        fastcgi_pass cart-service:9000;
    }
    location /order {
        rewrite ^/order(.*) $1 break;
        ...
        fastcgi_pass order-service:9000;
    }
    location /product {
        rewrite ^/product(.*) $1 break;
        ...
        fastcgi_pass product-service:9000;
    }
}

This solution could be greatly optimized using advanced nginx techniques:

server {
    ...
    # This is a very important one!
    # Since we are using variables for backend name, we need a resolver to resolve it at the runtime
    # Docker default internal resolver is 127.0.0.11
    resolver 127.0.0.11;

    location ~ ^/api/(?<api>account|cart|order|product)(?<path>/.*) {
        include fastcgi_params;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
        # note we are using the $path variable here instead of the $uri one
        fastcgi_param REQUEST_URI $path$is_args$args;
        # assuming this path is the same within all the backend services
        fastcgi_param SCRIPT_FILENAME /usr/src/app/public/index.php;
        fastcgi_intercept_errors on;
        # using $api variable as part of backend container name
        fastcgi_pass $api-service:9000;
    }
}

Note that we need a new resolver directive since we are using a variable to specify the backend name. You can read additional details here, and the resolver address for docker taken from this answer.


Update @ 2022.05.18

Actually there is a way to made the above config works without an additional resolver directive. To do it we need to define every service as an upstream. This way nginx will need to resolve the used service names to docker containers IP addresses only once at startup, eliminating all the internal DNS traffic and making the whole config somewhat more performant:

upstream account {
    server  account-service:9000;
}
upstream cart {
    server  cart-service:9000;
}
upstream order {
    server  order-service:9000;
}
upstream product {
    server  product-service:9000;
}

After doing that the last line from the config given above can be changed to

fastcgi_pass $api;

and the resolver directive can be safely removed from the nginx configuration.


If your script path vary upon the different API backend containers, you can use an additional map block to get the script path from the $api variable value:

map $api $script {
    account    /usr/src/app/public/index.php;
    cart       /some/other/path;
    ...
}

server {
    ...
    location ~ ^/api/(?<api>account|cart|order|product)(?<path>/.*) {
        ...
        fastcgi_param SCRIPT_FILENAME $script;
        ...
    }
}

Upvotes: 1

Related Questions