I recently migrated my nextjs + Expo project over to pure Expo, and decided to switch from nginx to static-web-server while I was at it. I always somehow end up in pain when I use nginx, so I thought it was worth checking out a promising alternative.

Here’s what the Dockerfile that I ended up with looks like:

FROM node:16 AS base
WORKDIR /base
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
ENV NODE_ENV=production
RUN npx expo export:web

FROM joseluisq/static-web-server:2-alpine
COPY --from=base /base/web-build /public
COPY ./entrypoint.sh /
COPY ./static-web-server.toml /
ENTRYPOINT ["/entrypoint.sh"]

It’s a multi-stage build so we’re not shipping a huge docker container around. The first stage builds and exports the app, and the second stage sets up static-web-server to serve our exported build. Node versions greater than 16 gave me issues with some ssl thing, so that was the latest release that worked for me. Also it only copies over the package.json and yarn.lock files first, so that the yarn install step will be cached unless a dependency changes, which speeds up builds a lot.

For the static-web-server stuff, I ended up adding an entrypoint.sh file that just looks like this:

#!/bin/sh
SERVER_PORT=80 static-web-server --page-fallback /public/index.html --cache-control-headers=false -w /static-web-server.toml

Then the static-web-server.toml config file looks like this:

[general]
host = "::"
port = 80
root = "/public"
cache-control-headers = false
page-fallback = "/public/index.html"

[[advanced.headers]]
source = "*"
[advanced.headers.headers]
Cache-Control = "no-cache, no-store, must-revalidate"
Pragma = "no-cache"
Expires = "0"

[[advanced.headers]]
source = "*.{js,css,png,jpg,jpeg}"
[advanced.headers.headers]
Cache-Control = "public, max-age=36000"
Pragma = ""
Expires = ""

The options that are interesting here:

  • cache-control-headers should be off for SPA apps, since the default caching behavior from static-web-server is to cache html files for a day, which you definitely don’t want since your clients will be seeing an old app for a day
  • page-fallback is needed since the app will use client-side routing, so when someone goes to a route like /my/page, we want to still serve the index.html file.

Since we’ve disabled the cache-control-headers option, we need to re-enable caching for non-html stuff. Also it’s good to be very sure that clients don’t cache the html, by sending the right headers for that. So we disable caching for all requests, then re-enable it for “{js,css,pgn,jpg,jpeg}”. It’s sort of unfortunate that you have to specify all the things you want cached, instead of specifying the one thing you do want cached, but it’s still 1000x better than dealing with Nginx.

This was done for a free/open source chess site I work on, Chess Madra. Check it out if that’s something you’re into.