Hardening React Deployments: Security-First Frontend Delivery with Nginx and Docker
Frontend security is often treated as an afterthought. A few lint rules, maybe a library, and a lot of optimism.
This post walks through a production-grade React deployment where security is enforced at the infrastructure layer, not scattered across JavaScript files and developer discipline.
The goal is simple: serve static assets fast, reduce attack surface, and make security defaults unavoidable.
What Problem This Setup Solves
Typical frontend deployments suffer from a few recurring issues:
- Node.js running in production for no reason
- Large container images with unnecessary tooling
- Client-side routing breaking on refresh
- Security headers applied inconsistently or not at all
This setup addresses those directly by:
- Building React once, serving only static assets
- Removing Node.js entirely from the runtime image
- Enforcing security headers at the Nginx layer
- Making the deployment predictable and auditable
If JavaScript fails, Nginx still enforces policy. That’s the point.
Architecture Overview
The deployment uses a multi-stage Docker build with a strict separation of concerns.
Build Stage
- Uses Node.js solely to compile the React application
- Outputs static assets (
/dist) - Never ships to production
Runtime Stage
- Uses Alpine-based Nginx
- Serves static files only
- Terminates all HTTP requests
- Applies security headers centrally
This separation ensures:
- Smaller images
- Faster startup
- Fewer vulnerabilities
- No runtime dependency sprawl
Nginx as a Security Boundary
Nginx is not just a file server here. It’s the security enforcement point.
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
add_header Content-Security-Policy "
default-src 'self';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
}
}
Dockerfile
# ---- build stage ----
FROM node:18-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# ---- runtime stage ----
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]