Search code examples
dockernginxfpm

Dockerized Nginx Serve Separate Url For Hosts


Do we have any nginx gurus here?

I have a dockerized network with 6 containers running in it:

  • a react app container
  • a laravel app container
  • a phpMyAdmin container
  • a mysql container
  • nginx container
  • redis container

What I'm trying to achieve is to configure nginx, so I can serve the react and laravel app the following way:

  • http://localhost:8000/react -> will return the react app
  • http://localhost:8000/laravel -> will return the laravel app

docker-compose.yaml:

services:
  react:
    container_name: react
    build:
      context: ./react
      dockerfile: ./Dockerfile
    expose:
      - 3000
    networks:
      - dev_network
    volumes:
      - ./react/src:/app/src
    platform: linux/amd64
  laravel:
    container_name: laravel
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
    expose:
      - 9000
    volumes:
      - .:/usr/src/app
      - ./:/usr/src/app
    depends_on:
      - dev_db
    networks:
      - dev_network
    platform: linux/amd64

  dev_nginx:
    container_name: dev_nginx
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    volumes:
      - ./public:/usr/src/app/public
    ports:
      - 8000:80
    depends_on:
      - laravel
      - react
    environment:
      NGINX_FPM_HOST: laravel
      NGINX_NODE_HOST: react
      NGINX_ROOT: /usr/src/app
    networks:
  - dev_network
    platform: linux/amd64

  dev_db:
    container_name: dev_db
    image: mysql:8.0.20
   platform: linux/amd64
   restart: always
    volumes:
     - ./storage/db-data:/var/lib/mysql
    ports:
     - 3307:3306
    environment:
      MYSQL_DATABASE: "${DB_DATABASE}"
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: "${DB_USERNAME}"
      MYSQL_PASSWORD: "${DB_PASSWORD}"
    networks:
      - dev_network
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    depends_on:
      - dev_db
    environment:
      - PMA_HOST=dev_db
      - PMA_PORT=3306
    networks:
      - dev_network
    ports:
  - 8085:80
    platform: linux/amd64

  dev_redis:
    container_name: dev_redis
    image: redis:latest
    ports:
      - 6379:6379
    networks:
  - dev_network
    platform: linux/amd64

networks:
   dev_network:
   driver: bridge

Nginx Dockerfile:

FROM nginx:latest

ENV NGINX_REACT_ROOT /usr/src/app/public
ENV NGINX_LARAVEL_ROOT /usr/src/app/
ENV NGINX_FPM_HOST localhost
ENV NGINX_FPM_PORT 9000
ENV NGINX_NODE_PORT 3000

RUN rm -f /etc/nginx/conf.d/default.conf
RUN rm -f /etc/nginx/nginx.conf

COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/nginx/fpm-template.conf /etc/nginx/fpm.tmpl
COPY ./docker/nginx/entrypoint.sh /usr/local/bin/entrypoint.sh

RUN chmod +x /usr/local/bin/entrypoint.sh

EXPOSE 80

ENTRYPOINT ["entrypoint.sh"]

entrypoint.sh:

#!/bin/bash

envsubst '$NGINX_REACT_ROOT $NGINX_LARAVEL_ROOT $NGINX_FPM_HOST $NGINX_FPM_PORT $NGINX_NODE_HOST $NGINX_NODE_PORT' < /etc/nginx/fpm.tmpl > /etc/nginx/conf.d/default.conf
exec nginx -g "daemon off;"

fpm-template.conf:

server {
    listen 80;

    # this path MUST be exactly the same as your path in FPM even if it doesn't
    # exist here. Nginx will send the full path of the file to render to fpm.
    # root ${NGINX_ROOT};

    gzip on;
    gzip_disable "msie6";
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/x-javascript text/xml 
application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

    location /react {
        root /usr/src/app/public;
        proxy_pass http://${NGINX_NODE_HOST}:${NGINX_NODE_PORT};
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    location /laravel {
        root /usr/src/app;
        index index.php;

        try_files $uri $uri/ /index.php?$query_string;

        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass ${NGINX_FPM_HOST}:${NGINX_FPM_PORT};
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
        }
    }

    client_max_body_size 2m;
}

In my laravel application the index.php is placed in the root directory, so I had to indicate nginx to not look in the public directory. Now when I access localhost:8000/react, it works as expected. But when I access localhost:8000/laravel it returns 404 and the nginx container gives me this log error:

[error] 9#9: *1 open() "/etc/nginx/html/index.php" failed (2: No such file or directory)

Any idea why it keeps looking in /etc/nginx/html and not the root directory I configured (usr/src/app)? My devops skills have its limitations, so possible that I have a misunderstanding of how the nginx container works here. Thanks.


Solution

  • If you had read the try_files directive documentation more carefully, you would have found that the last argument in try_files is treated completely differently from the others:

    If none of the files were found, an internal redirect to the uri specified in the last parameter is made.
    ...
    The last parameter can also point to a named location ... Starting from version 0.7.51, the last parameter can also be a code.

    This means that if the request does not correspond to a physical file but instead, for example, to a laravel route, the URI to be processed after a failed physical file check will be /index.php?$query_string. However, according to your nginx configuration, it should be /laravel/index.php?$query_string. Furthermore, you don't even have a location to handle requests that don't start with /react or /laravel (which is considered bad practice, BTW). Additionally, since you haven't defined a web root at the server context level, the default root directory for such requests will be {prefix}/html, where {prefix} is a precompiled value (which can be viewed using the nginx -V command), and in your case it is most likely equal to /etc/nginx.

    That is, the minimal change you need to make to your nginx configuration is to update the try_files directive as follows:

    try_files $uri $uri/ /laravel/index.php?$query_string;
    

    With this configuration, nginx will look for static files in the /usr/src/app/laravel directory. If this is not the desired behavior, you should use an alias directive instead of the root one (be sure to carefully read the documentation to understand the difference).

    However, serving a PHP web application under a URI prefix with the alias directive can be a bit tricky due to longstanding side effects when using try_files in combination with alias. Refer to this answer for potential workaround methods. Note that the only reliable method to correctly set the SCRIPT_FILENAME FastCGI parameter is to use the $request_filename variable. The commonly used combination $document_root$fastcgi_script_name will not produce the correct result when an alias directive is used.