Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/non-root-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"seamless-templates": patch
---

Run the production images as non-root. The web templates (react-vite, react-oauth, vue, svelte, angular) now serve from `nginxinc/nginx-unprivileged` as the `nginx` user, and the Next.js `runtime` target runs as the `node` user. Every web container now listens on port 8080 instead of 80, so map `5173:8080` (and point healthchecks at 8080) in any compose file or deployment that ran these images.
26 changes: 15 additions & 11 deletions templates/fullstack/nextjs/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
# auth server sends neither in development. Production is opt-in, chosen by the
# thing doing the deploying.
#
# Port 80 and /health are what the generated compose file expects of the web
# container, in both targets.
# Port 8080 and /health are what the generated compose file expects of the web
# container, in both targets. 8080 rather than 80 so the runtime target, which
# runs as the unprivileged node user, needs no privileged port.

FROM node:24-slim AS deps
WORKDIR /app
Expand All @@ -28,20 +29,23 @@ RUN npm run build
FROM node:24-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=80
ENV PORT=8080
ENV HOSTNAME=0.0.0.0

COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
# node owns /app so the server can write its cache under .next at runtime.
RUN chown node:node /app
COPY --from=build --chown=node:node /app/.next/standalone ./
COPY --from=build --chown=node:node /app/.next/static ./.next/static
COPY --from=build --chown=node:node /app/public ./public

EXPOSE 80
HEALTHCHECK CMD node -e "fetch('http://localhost/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
USER node
EXPOSE 8080
HEALTHCHECK CMD node -e "fetch('http://localhost:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]

# Last on purpose. See the note at the top of this file before moving it.
FROM deps AS dev
COPY . .
EXPOSE 80
HEALTHCHECK CMD node -e "fetch('http://localhost/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["npx", "next", "dev", "--port", "80", "--hostname", "0.0.0.0"]
EXPOSE 8080
HEALTHCHECK CMD node -e "fetch('http://localhost:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["npx", "next", "dev", "--port", "8080", "--hostname", "0.0.0.0"]
7 changes: 4 additions & 3 deletions templates/fullstack/nextjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,17 @@ The tests use Vitest and need no auth server or network.

## Deploying

The `Dockerfile` has two targets, both serving on port 80 with a `/health`
The `Dockerfile` has two targets, both serving on port 8080 with a `/health`
route:

| Command | Target | Runs |
| --------------------------------- | ------------------- | ----------------------------------------------------------------------- |
| `docker build .` | `dev` (the default) | `next dev`, for the local stack: source bind-mounted, codes in the logs |
| `docker build --target runtime .` | `runtime` | the standalone production server (`output: "standalone"`) |

Deploy the `runtime` target. Set the environment variables above at runtime;
the image runs on the Node.js runtime, which the cookie signing requires.
Deploy the `runtime` target, which runs as the unprivileged `node` user. Set the
environment variables above at runtime; the image runs on the Node.js runtime,
which the cookie signing requires.

## License

Expand Down
14 changes: 10 additions & 4 deletions templates/web/angular/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
FROM deps AS builder
RUN npm run build

FROM nginx:alpine AS runtime
# Unprivileged nginx runs as user nginx (uid 101) and listens on 8080, so the
# container needs neither root nor a privileged port.
FROM nginxinc/nginx-unprivileged:alpine AS runtime
USER root
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist/web/browser /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/health || exit 1
# The entrypoint writes config.js into the web root at start.
RUN chmod +x /entrypoint.sh \
&& chown -R nginx:nginx /usr/share/nginx/html
USER nginx
EXPOSE 8080
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1
CMD ["/entrypoint.sh"]
4 changes: 2 additions & 2 deletions templates/web/angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ your project's API URL (`{{apiUrl}}` in [template.json](template.json)).

## Docker

The `Dockerfile`'s default target serves the production build with nginx on port 80, writing
The `Dockerfile`'s default target serves the production build with unprivileged nginx on port 8080, writing
`API_URL` into `config.js` at startup, so one image runs against any API:

```bash
docker build -t my-web .
docker run -p 5173:80 -e API_URL=http://localhost:3000 my-web
docker run -p 5173:8080 -e API_URL=http://localhost:3000 my-web
```

Its `dev` target runs the development server instead.
Expand Down
2 changes: 1 addition & 1 deletion templates/web/angular/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;

root /usr/share/nginx/html;
index index.html;
Expand Down
17 changes: 13 additions & 4 deletions templates/web/react-oauth/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ COPY . .
RUN npm run build


FROM nginx:alpine
# Unprivileged nginx runs as user nginx (uid 101) and listens on 8080, so the
# container needs neither root nor a privileged port.
FROM nginxinc/nginx-unprivileged:alpine

USER root

COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 80
# The entrypoint writes config.js into the web root at start.
RUN chmod +x /entrypoint.sh \
&& chown -R nginx:nginx /usr/share/nginx/html

USER nginx

EXPOSE 8080

HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1

CMD ["/entrypoint.sh"]
2 changes: 1 addition & 1 deletion templates/web/react-oauth/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;

root /usr/share/nginx/html;
index index.html;
Expand Down
17 changes: 13 additions & 4 deletions templates/web/react-vite/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,25 @@ COPY . .
RUN npm run build


FROM nginx:alpine
# Unprivileged nginx runs as user nginx (uid 101) and listens on 8080, so the
# container needs neither root nor a privileged port.
FROM nginxinc/nginx-unprivileged:alpine

USER root

COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 80
# The entrypoint writes config.js into the web root at start.
RUN chmod +x /entrypoint.sh \
&& chown -R nginx:nginx /usr/share/nginx/html

USER nginx

EXPOSE 8080

HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1

CMD ["/entrypoint.sh"]
4 changes: 2 additions & 2 deletions templates/web/react-vite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ npm run test:coverage # vitest with a v8 coverage report

## Docker

The production image builds the Vite app and serves it with nginx. Runtime config is written to
The production image builds the Vite app and serves it with unprivileged nginx on port 8080. Runtime config is written to
`/usr/share/nginx/html/config.js` from the container `API_URL` environment variable.

```bash
docker build -t seamless-auth-react-starter .
docker run --rm -p 8080:80 -e API_URL=http://localhost:3000/ seamless-auth-react-starter
docker run --rm -p 8080:8080 -e API_URL=http://localhost:3000/ seamless-auth-react-starter
```

## License
Expand Down
2 changes: 1 addition & 1 deletion templates/web/react-vite/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;

root /usr/share/nginx/html;
index index.html;
Expand Down
14 changes: 10 additions & 4 deletions templates/web/svelte/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173", "--strict
FROM deps AS builder
RUN npm run build

FROM nginx:alpine AS runtime
# Unprivileged nginx runs as user nginx (uid 101) and listens on 8080, so the
# container needs neither root nor a privileged port.
FROM nginxinc/nginx-unprivileged:alpine AS runtime
USER root
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/health || exit 1
# The entrypoint writes config.js into the web root at start.
RUN chmod +x /entrypoint.sh \
&& chown -R nginx:nginx /usr/share/nginx/html
USER nginx
EXPOSE 8080
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1
CMD ["/entrypoint.sh"]
4 changes: 2 additions & 2 deletions templates/web/svelte/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ your project's API URL (`{{apiUrl}}` in [template.json](template.json)).

## Docker

The `Dockerfile`'s default target serves the production build with nginx on port 80, writing
The `Dockerfile`'s default target serves the production build with unprivileged nginx on port 8080, writing
`API_URL` into `config.js` at startup, so one image runs against any API:

```bash
docker build -t my-web .
docker run -p 5173:80 -e API_URL=http://localhost:3000 my-web
docker run -p 5173:8080 -e API_URL=http://localhost:3000 my-web
```

Its `dev` target runs the development server instead.
Expand Down
2 changes: 1 addition & 1 deletion templates/web/svelte/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;

root /usr/share/nginx/html;
index index.html;
Expand Down
14 changes: 10 additions & 4 deletions templates/web/vue/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173", "--strict
FROM deps AS builder
RUN npm run build

FROM nginx:alpine AS runtime
# Unprivileged nginx runs as user nginx (uid 101) and listens on 8080, so the
# container needs neither root nor a privileged port.
FROM nginxinc/nginx-unprivileged:alpine AS runtime
USER root
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/health || exit 1
# The entrypoint writes config.js into the web root at start.
RUN chmod +x /entrypoint.sh \
&& chown -R nginx:nginx /usr/share/nginx/html
USER nginx
EXPOSE 8080
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/health || exit 1
CMD ["/entrypoint.sh"]
4 changes: 2 additions & 2 deletions templates/web/vue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ your project's API URL (`{{apiUrl}}` in [template.json](template.json)).

## Docker

The `Dockerfile`'s default target serves the production build with nginx on port 80, writing
The `Dockerfile`'s default target serves the production build with unprivileged nginx on port 8080, writing
`API_URL` into `config.js` at startup, so one image runs against any API:

```bash
docker build -t my-web .
docker run -p 5173:80 -e API_URL=http://localhost:3000 my-web
docker run -p 5173:8080 -e API_URL=http://localhost:3000 my-web
```

Its `dev` target runs the development server instead.
Expand Down
2 changes: 1 addition & 1 deletion templates/web/vue/nginx.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server {
listen 80;
listen 8080;

root /usr/share/nginx/html;
index index.html;
Expand Down
Loading