feat(FN-1226): add production Docker workflow

- Add a root .dockerignore to reduce build context and exclude local runtime state
- Add a multi-stage Dockerfile that builds the workspace, runs as non-root, and includes health checks
- Document Docker build/run usage, env vars, persistence, and quick-start commands in README and docs/docker.md
- Add Docker configuration tests in packages/cli/src/docker.test.ts to validate required image and docs expectations
This commit is contained in:
gsxdsm
2026-04-08 06:55:35 -07:00
parent ad54b0f6f9
commit 872369faed
5 changed files with 261 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
const workspaceRoot = resolve(import.meta.dirname, "../../..");
const dockerfilePath = resolve(workspaceRoot, "Dockerfile");
const dockerignorePath = resolve(workspaceRoot, ".dockerignore");
const dockerDocsPath = resolve(workspaceRoot, "docs", "docker.md");
describe("Docker configuration", () => {
it("has a Dockerfile with required production instructions", () => {
expect(existsSync(dockerfilePath)).toBe(true);
const dockerfile = readFileSync(dockerfilePath, "utf8");
expect(dockerfile).toContain("FROM node:22");
expect(dockerfile).toContain("ENTRYPOINT");
expect(dockerfile).toContain("USER node");
expect(dockerfile).toContain("HEALTHCHECK");
expect(dockerfile).toContain("EXPOSE 4040");
expect(dockerfile).toContain("CMD");
});
it("uses a multi-stage Docker build", () => {
const dockerfile = readFileSync(dockerfilePath, "utf8");
const fromInstructions = dockerfile.match(/^FROM\s+/gm) ?? [];
expect(fromInstructions.length).toBeGreaterThanOrEqual(2);
});
it("installs git and uses deterministic pnpm installs", () => {
const dockerfile = readFileSync(dockerfilePath, "utf8");
expect(dockerfile).toMatch(/apt-get[^\n]*install[^\n]*git/);
expect(dockerfile).toContain("pnpm install --frozen-lockfile");
});
it("has a .dockerignore with required exclusions", () => {
expect(existsSync(dockerignorePath)).toBe(true);
const dockerignore = readFileSync(dockerignorePath, "utf8");
expect(dockerignore).toContain("node_modules/");
expect(dockerignore).toContain(".git/");
expect(dockerignore).toContain("dist/");
expect(dockerignore).toContain(".fusion/");
});
it("does not exclude package manifests needed for install", () => {
const dockerignore = readFileSync(dockerignorePath, "utf8");
const dockerignoreLines = dockerignore
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#"));
expect(dockerignoreLines).not.toContain("package.json");
expect(dockerignoreLines).not.toContain("pnpm-lock.yaml");
});
it("has docker documentation for build, run, and environment variables", () => {
expect(existsSync(dockerDocsPath)).toBe(true);
const docs = readFileSync(dockerDocsPath, "utf8").toLowerCase();
expect(docs).toContain("build");
expect(docs).toContain("run");
expect(docs).toContain("environment variables");
});
});