diff --git a/.dockerignore b/.dockerignore index 0d7c2da..f0c533b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ +.env +.env*.local node_modules/ .next/ .git/ diff --git a/Dockerfile b/Dockerfile index 0c263bc..f26d63a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,8 @@ RUN npm ci COPY --chown=node:node . . # Use the node user from the image (instead of the root user) -# USER node +RUN chown node:node /usr/src/app +USER node # For container development, the following command runs forever, so we can inspect the container CMD ["npm", "run", ".dev"] diff --git a/src/__tests__/docker-security.test.ts b/src/__tests__/docker-security.test.ts new file mode 100644 index 0000000..5ebce9b --- /dev/null +++ b/src/__tests__/docker-security.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +describe("Docker build context security", () => { + const read = (file: string) => readFileSync(join(process.cwd(), file), "utf8"); + + it("excludes local environment secrets from image build contexts", () => { + const ignored = read(".dockerignore") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")); + + expect(ignored).toContain(".env"); + expect(ignored).toContain(".env*.local"); + }); + + it("runs the development image as the unprivileged node user", () => { + const developmentStage = read("Dockerfile").split("FROM node:lts-alpine AS build")[0]; + expect(developmentStage).toMatch(/^USER node$/m); + }); +});