We used to host our SPAs in Nginx for years, but several months ago we switched to Caddy. Long story short it’s smaller and has fewer vulnerabilities.
In our (microk8s) cluster we require all apps to run as a non-root user with a readonly file system. In addition, we remove all Linux capabilities from the container, ensuring the process runs with the minimum possible privileges.
In other words we require all our helm chart to have securityContext below:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 2000
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- all
I figured I’d document the Dockerfile we came up with. It’s mostly self-explanatory, but I added some comments just in case.
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
ARG BUILD_NUMBER=1.0.0.0
COPY ./ ./
# optional: we prefer to set version
RUN npm run set-version ${BUILD_NUMBER}
RUN npm run build -- --configuration production --output-path=./dist --base-href="/"
FROM caddy:2-alpine
# remove file capabilities from the caddy binary so that it can run with `capabilities: drop: all`
RUN apk add --no-cache libcap-setcap && setcap -r /usr/bin/caddy && apk del libcap-setcap
# copy files and set ownership
COPY --chown=1001:2000 --from=build /app/dist/browser/ /app
# switch to our non root user
USER 1001:2000
# use non priveleged port
EXPOSE 8080
# disable all features we don't really need
ENV CADDY_ADMIN=off \
CADDY_STORAGE_CHECK=off \
CADDY_STORAGE_CLEAN_INTERVAL=off \
XDG_DATA_HOME=/tmp \
XDG_CONFIG_HOME=/tmp \
HOME=/tmp
CMD ["caddy", "file-server", "--listen", ":8080", "--root", "/app"]