Skip to main content
The Roots UI Docker image uses nginx:stable-alpine as its base and ships with a custom Nginx site configuration. The file is copied into the image at /etc/nginx/conf.d/default.conf during the Docker build.

Full configuration

nginx.conf
server {
    listen       80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    # redirect server error pages to the static page /50x.html
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Configuration walkthrough

Server block

listen       80;
server_name  localhost;
The server listens on port 80 (HTTP). server_name localhost is suitable for containerised deployments where the container hostname is not publicly resolved. When placing Nginx behind a reverse proxy or load balancer, this value can remain as-is; the outer proxy handles the public hostname.

Static file serving

location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
}
  • root /usr/share/nginx/html — serves files from the directory where the Vite build output (dist/) is copied during the Docker build.
  • index index.html index.htm — serves index.html when a directory is requested.
  • try_files $uri $uri/ /index.html — the SPA fallback. Nginx first tries to serve the exact file requested, then a directory with that name, and finally falls back to index.html. This ensures that React Router routes like /explore/destinations/123 are handled by the client-side router rather than returning a 404.

Error pages

error_page   500 502 503 504  /50x.html;
location = /50x.html {
    root   /usr/share/nginx/html;
}
Server errors (500, 502, 503, 504) are redirected to the static /50x.html page that Nginx includes by default. The page is served from the same web root as the application.

Customising the configuration

Changing the listening port

To serve on a port other than 80, update listen and the EXPOSE directive in the Dockerfile:
listen 8080;
Then rebuild the image and adjust the docker run port mapping accordingly:
docker run -p 8080:8080 roots-ui

Adding an API reverse proxy

If the backend API is on the same Docker network, you can proxy /api/ requests directly from Nginx rather than requiring the browser to make cross-origin requests:
location /api/ {
    proxy_pass         http://att-backend:3000/;
    proxy_http_version 1.1;
    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;
}
Replace att-backend:3000 with the hostname and port of your backend container.
When using this proxy approach, set VITE_API_BASE_URL to /api (a relative path) so the browser sends requests to the same origin, which Nginx then forwards upstream.

Gzip compression

The base nginx:stable-alpine image has the gzip module compiled in. Add the following inside the server block to compress text-based assets:
gzip            on;
gzip_types      text/plain text/css application/javascript application/json
                image/svg+xml;
gzip_min_length 1024;
gzip_vary       on;

Cache headers for static assets

Vite fingerprints JavaScript and CSS bundles with content hashes, making them safe to cache indefinitely. Add a location block to set long-lived cache headers for those assets:
location ~* \.(js|css|woff2?|png|jpg|jpeg|svg|ico)$ {
    root       /usr/share/nginx/html;
    expires    1y;
    add_header Cache-Control "public, immutable";
    try_files  $uri =404;
}
Place this block before the catch-all location / block so Nginx matches it first.

Adding SSL/TLS

The containerised Nginx does not handle TLS termination by default. The recommended approach for production is to terminate TLS at an upstream load balancer or reverse proxy (such as AWS ALB, GCP Cloud Load Balancing, Caddy, or a separate Nginx instance), and forward plain HTTP to the container on port 80. If you need TLS directly in the container, refer to the Nginx HTTPS server documentation and mount your certificates as Docker volumes:
docker run -p 443:443 \
  -v /path/to/cert.pem:/etc/nginx/certs/cert.pem:ro \
  -v /path/to/key.pem:/etc/nginx/certs/key.pem:ro \
  roots-ui

Applying a custom configuration

To replace the default configuration without rebuilding the image, mount your own nginx.conf at runtime:
docker run -p 80:80 \
  -v $(pwd)/my-nginx.conf:/etc/nginx/conf.d/default.conf:ro \
  roots-ui
After editing the configuration file, validate it before deploying:
docker run --rm -v $(pwd)/my-nginx.conf:/etc/nginx/conf.d/default.conf:ro \
  nginx:stable-alpine nginx -t

Build docs developers (and LLMs) love