The front end needs no build step: its image is the official nginx 75 :1.30-alpine (Nginx 1.30.5, the stable branch, 27 MB compressed) plus public/, built from web/Dockerfile with the project root as context:
# syntax=docker/dockerfile:1
# BookNest front end: Nginx serves public/ and proxies /api/ to the API service.
FROM nginx:1.30-alpine
# The entrypoint renders /etc/nginx/templates/*.template with these variables.
ENV API_UPSTREAM=api:3000 \
NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1
COPY web/default.conf.template /etc/nginx/templates/
COPY public/ /usr/share/nginx/html/
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --start-interval=1s \
CMD ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1/healthz"]At startup the image's entrypoint runs envsubst over /etc/nginx/templates/*.template, substituting only variables that exist in the environment, so Nginx's own $uri survives and the API's address becomes a run-time setting, as Kubernetes 5,150 (Kubernetes) will need:
server {
listen 80;
root /usr/share/nginx/html;
resolver ${NGINX_LOCAL_RESOLVERS} valid=10s;
set $api http://${API_UPSTREAM};
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass $api;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /healthz {
access_log off;
return 200 "ok\n";
}
}Under the hood: a plain proxy_pass http://api:3000; resolves api once, at startup; if up --build recreates the API on a different address, Nginx keeps the old one and answers 502 until it restarts. With the address in a variable, Nginx re-resolves it through resolver, caching each answer for 10 seconds, and NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 makes the entrypoint fill NGINX_LOCAL_RESOLVERS from the container's /etc/resolv.conf: Docker 514 's DNS server 127.0.0.11 here, the cluster DNS in Kubernetes, where Nginx needs the Service's full name because it ignores search domains. /healthz lets the healthcheck test the web tier alone.