I have a strange issue with js files that cannot be found in a flask app, for which I use nginx in a docker container.
In the HTML template, I have this ref, using the url_for function
<script type="text/javascript" src="{{ url_for('static', filename='func.js') }}"></script>
In the nginx file, this location is registered:
upstream flask_app {
server web:5000;
}
server {
listen 80;
location / {
proxy_pass http://flask_app;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /home/app/web/project/static/;
#root /home/app/web/project;
}
location ~* \.js$ {
expires -1;
}
}
Flask config.py, from which the app is configured, has this entry:
STATIC_FOLDER = f"{os.getenv('APP_FOLDER')}/project/static"
with the environment variable being set to APP_FOLDER=/home/app/web
The dockerfile creates the necessary dirs:
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
In the docker-compose.yml file, the static folder is listed as volume both for web and nginx service:
web:
build:
context: ./services/web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:5000 manage:app
volumes:
- static_volume:/home/app/web/project/static
In the rendered HTML template, the resulting URL is /static/func.js, but it gives me 404 not found error and the js functions, of course, are not executed.
Interestingly, when I add a simple text file to the static folder, and check the URL http://127.0.0.1:1337/static/hi.txt instead of http://127.0.0.1:1337/someAppRoute, the text file is found and displayed in the browser.
What am I doing wrong??
As I suspected, you are using an additional regex location to manage caching headers. Unfortunately, this is a very common practice, though it is actually one of the worst ways to solve such problems.
Every request is ultimately processed in one specific location. In your case, any request whose request path ends with .js will be processed in your last location (regardless of whether it starts with the /static/ prefix or not). That's because regex locations have greater priority than prefix locations, unless the latter are defined using the ^~ modifier.
Since you have not specified a web server root in this specific location nor at the server level, nginx will use the default web server root. The root directive defaults to the html directory, relative to the prefix compile-time option value. You can check the actual prefix value for your nginx binary using the nginx -V command. The output will contain a line like:
configure arguments: --prefix=...
For most packaged nginx distributions, the prefix is usually either /var/www or /usr/share/nginx, so nginx will search the requested .js file under one of these two directories.
What is funny is that a much better example is given directly in the documentation (which no one read nowadays) for the expires directive, right at the bottom. Instead of declaring a separate location to process .js files just to add the cache control headers within it, you can add these headers conditionally, based on the content of the Content-Type response header that nginx will send to the client (see the $sent_http_ variable documentation):
map $sent_http_content_type $expires {
application/javascript -1; # check 'mime.types' file for the proper value
default off;
}
server {
...
location /static/ {
# this is better than 'alias' when the URI prefix
# literally matches directory name!
root /home/app/web/project;
# 'Cache-Control: no-cache' header
# will be added for javascript files only
expires $expires;
}
...
}
Why did I call your original approach "one of the worst ways," you ask? Besides possibly interfering with the main configuration (as in the case you've just faced), there are also performance considerations. Generally, you should avoid using regex locations whenever possible. Nginx uses a highly optimized algorithm to find the best matching prefix location. After the best matching prefix location is identified, all declared regex locations will be checked one by one for a match, using the much more expensive libpcre library calls.