Search code examples
dockerexpressnginxserver

Nginx + Express - serve static files from Docker container


I have an Express api and an Nginx together in a Docker Container. I want the Nginx to reverse proxy regular requests to the API, but I want it to serve the static files like images directly.

But I can't for the life of me seem to figure out the exact URI that I need to use here and the images keep 404-ing. Here's the docker-compose:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: api

  nginx-proxy:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    container_name: nginx-proxy
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api

And here's the nginx.conf

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://api:3000;
        proxy_set_header Host $host;
        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 $scheme;
    }
    
    location /images/ {
        alias /public/images/;
        # THIS PART JUST DOESN'T WORK
    }
}

I've tried multiple URIs but I can't figure out the logic of this. How does the Nginx image access the files from the Express image? This is the error I get:

2024-01-07 00:00:00 nginx-proxy  | 2024/01/06 00:00:00 [error] 30#30: *4 open() "/public/images/test.png" failed (2: No such file or directory), client: 172.18.0.1, server: localhost, request: "GET /images/test.png HTTP/1.1", host: "localhost:8080", referrer: "http://localhost:2222/"

Edit: I should add that I did copy the images folder in the API Dockerfile, they are served when I query them through the API.


Solution

  • You haven't shared with us the contents of your nginx Dockerfile, but it looks like you're never placing any content into the /public/images directory in the nginx container.

    Rather than building the images into the container image, we can bind mount a local images directory into the container at runtime. This lets us use the stock nginx image, rather than needing to build our own.

    Here's a slightly modified version of your Dockerfile that seems to work just fine:

    services:
      angles-api:
        build:
          context: api
        healthcheck:
          test: ["CMD", "curl", "-sSf", "http://localhost:3000"]
          interval: "5s"
          retries: 3
    
      nginx-proxy:
        image: docker.io/nginx:mainline
        ports:
          - "8080:80"
        volumes:
          - ./nginx.conf:/etc/nginx/conf.d/default.conf
          - ./images:/public/images
        depends_on:
          angles-api:
            condition: service_healthy
    

    Where:

    api/Dockerfile contains:

    FROM docker.io/alpine:latest
    
    USER root
    RUN apk add curl
    RUN curl -sSfL -o /tmp/whoami.tar.gz https://github.com/traefik/whoami/releases/download/v1.10.1/whoami_v1.10.1_linux_amd64.tar.gz
    RUN tar -C /usr/local/bin -xf /tmp/whoami.tar.gz whoami
    
    ENV WHOAMI_PORT_NUMBER=3000
    CMD ["/usr/local/bin/whoami"]
    

    ./nginx.conf contains:

    server {
        listen 80;
        server_name localhost;
    
        location / {
            proxy_pass http://angles-api:3000;
            proxy_set_header Host $host;
            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 $scheme;
        }
    
        location /images/ {
            alias /public/images/;
        autoindex on;
        }
    }
    

    And ./images contains:

    total 160
    -rw-r--r-- 1 lars lars 161352 Jan  6 21:15 uncle-deadly.png
    

    With the above configuration, a request to localhost:8080/images/ goes to the /public/images directory in the nginx container:

    $ curl localhost:8080/images/
    <html>
    <head><title>Index of /images/</title></head>
    <body>
    <h1>Index of /images/</h1><hr><pre><a href="../">../</a>
    <a href="uncle-deadly.png">uncle-deadly.png</a>                                   07-Jan-2024 02:15              161352
    </pre><hr></body>
    </html>
    

    A request for any path not under /images goes to the API container:

    $ curl localhost:8080/
    Hostname: 8c7638b8d4c2
    IP: 127.0.0.1
    IP: 192.168.144.2
    RemoteAddr: 192.168.144.3:39468
    GET / HTTP/1.1
    Host: localhost
    User-Agent: curl/8.0.1
    Accept: */*
    Connection: close
    X-Forwarded-For: 192.168.144.1
    X-Forwarded-Proto: http
    X-Real-Ip: 192.168.144.1
    

    And:

    $ curl localhost:8080/foo/bar
    Hostname: 8c7638b8d4c2
    IP: 127.0.0.1
    IP: 192.168.144.2
    RemoteAddr: 192.168.144.3:40144
    GET /foo/bar HTTP/1.1
    Host: localhost
    User-Agent: curl/8.0.1
    Accept: */*
    Connection: close
    X-Forwarded-For: 192.168.144.1
    X-Forwarded-Proto: http
    X-Real-Ip: 192.168.144.1
    

    Note that in the above compose.yaml, we're using the long form of the depends_on option so that the nginx container doesn't start up until the api container is healthy. The older short form of the depends_on option is effectively useless, since it doesn't know anything about the state of the application running inside the container (so it doesn't prevent a dependent container from trying to make requests before the target container is ready).


    All of the files referenced in this answer can be found online here.