When i am trying to connect one of my services i get 404 Not Found nginx. I've configured dockerfile and docker-compose well, but have some issues on nginx configs side
the project structure is
./backend/subscription_service
./backend/subscription_service/Dockerfile
./nginx/nginx.conf
./docker-compose.yaml
Nginx log:
2025/06/08 09:24:32 [error] 22#22: *2 open() "/etc/nginx/html/index.php" failed (2: No such file or directory), client: 172.18.0.1, server: localhost, request: "POST /subscription/subscriptions/ HTTP/1.1", host: "localhost"
172.18.0.1 - - [08/Jun/2025:09:24:32 +0000] "POST /subscription/subscriptions/ HTTP/1.1" 404 153 "-" "PostmanRuntime/7.44.0" "-"
Nginx
server {
listen 80;
server_name localhost;
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
try_files $uri $uri/ /subscription/index.php?$query_string;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass subscription_service:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $request_filename;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
}
}
}
Dockerfile
...
WORKDIR /var/www/html
COPY . /var/www/html
...
EXPOSE 9000
docker-compose.yaml
subscription_service:
build:
context: ./backend/subscription_service
dockerfile: Dockerfile
container_name: subscription_service
depends_on:
- subscription_db
volumes:
- ./backend/subscription_service:/var/www/html
- ./backend/subscription_service/bootstrap/cache:/var/www/html/bootstrap/cache
- ./backend/subscription_service/storage:/var/www/html/storage
...
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./backend/subscription_service:/var/www/html:ro
depends_on:
- subscription_service
the project is laravel.
index.php is located in ./backend/subscription_service/public
folder
You've come upon a well-known issue related to the combined use of the try_files
and alias
directives. Here's a detailed explanation of the root cause of this problem.
When processing each argument of try_files
directive, the ngx_http_try_files_module
performs an additional conditional operation — but only if the argument contains variables. If the parent location where the try_files
directive is defined is a prefix location and uses the alias
directive instead of root
to define the document root, and if the beginning of the string obtained after variable interpolation matches the URI prefix used in the parent location, that prefix is stripped from the interpolated string. This transformation is essential for the correct functioning of the code that follows, where the argument value is mapped to a physical file system path — especially when arguments like $uri
or $uri/
are used in the try_files
directive. However, the check for whether the argument is the last one in the try_files
directive — that is, whether it defines the fallback action — occurs after the above-described transformation has taken place.
So, in your case, instead of the expected fallback URI /subscriptions/index.php?...
, nginx ends up using /index.php?...
. Since there's no defined location for handling that URI in your configuration, the so-called null location is used. The document root in that case, unless specified explicitly at the outer level, defaults to the root
directive's built-in default <prefix>/html
. The <prefix>
value is set at compile time and can be viewed with the nginx -V
command. Typically, it is either /etc/nginx
(as in your case) or /usr/share/nginx
.
This issue does not affect cases where the fallback URI is written without variables. For example, the following configuration — common for React-based applications — works just fine:
location /app {
alias /path/to/app;
try_files $uri /app/index.html;
}
There are several popular approaches to solve this issue, and to make the answer as comprehensive as possible, I'll try to list them all.
Use the error_page
directive with a customized 404 handler instead of try_files
:
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
error_page 404 = /subscription/index.php$is_args$args;
...
}
This approach is recommended by one of the former nginx developers, Valentin Bartenev. While being the least popular, it's the most efficient in terms of performance, as it avoids two unnecessary stat()
system calls that would otherwise be made by the try_files
directive handler.
Unfortunately, in your case, due to the presence of a nested location, this method can't be used as-is, because the 404 error handler would be inherited by all nested locations. Still, it can be applied with some configuration tweaks:
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
location /subscription {
error_page 404 = /subscription/index.php$is_args$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass subscription_service:9000;
# fastcgi_index won't be used in this location, ever, and can be omitted.
fastcgi_param SCRIPT_FILENAME $request_filename;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
}
}
Note, however, that using an additional regex location just to set Expires
and Cache-Control
headers may raise performance concerns. In some scenarios, a more performant alternative is to set caching headers based on the Content-Type
response header — as demonstrated in the last part of this ServerFault answer.
Manually prepend the URI prefix from the parent location
directive to the fallback argument of try_files
directive:
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
try_files $uri $uri/ /subscription/subscription/index.php?$query_string;
...
}
And if your location block were defined as location /subscribe/ { ... }
, then the fallback URI would need to be adjusted to:
try_files $uri $uri/ /subscribe//subscribe/index.php?$query_string;
The double slash in this case is required. Without it, the final fallback URI would be computed as subscribe/index.php?...
(note the missing leading slash), which again wouldn't match this location block — or even location / { ... }
block, if present — and would fall back to the default null location. In your particular case, it would lead to a somewhat similar but even more weird-looking error:
open() "/etc/nginx/htmlsubscription/index.php" failed (2: No such file or directory)
Use the if
directive instead of try_files
:
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
if (!-e $request_filename) {
rewrite ^ /subscription/index.php last;
}
...
}
While the use of if
inside location blocks is generally discouraged (there was a famous "If is Evil" nginx wiki article once, now available only through the archived wiki content on GitHub), this particular construct is completely safe.
Use an additional named location for the fallback action:
location /subscription {
alias /var/www/html/public;
index index.php index.html index.htm;
try_files $uri $uri/ @subscription;
...
}
location @subscription {
rewrite ^ /subscription/index.php last;
# or duplicate the PHP handler here:
# include fastcgi_params;
# fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
# fastcgi_pass subscription_service:9000;
}