diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0d4e0b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.next +node_modules +npm-debug.log* +.env* +Dockerfile +.dockerignore +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..abbc474 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Production image. Designed for a hardened runtime: read-only root filesystem, +# a tmpfs at /tmp, no Linux capabilities, and a reverse proxy in front. + +FROM node:22-bookworm-slim AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --no-audit --no-fund + +FROM node:22-bookworm-slim AS build +WORKDIR /app +ENV NEXT_TELEMETRY_DISABLED=1 +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN mkdir -p public && npm run build + +FROM node:22-bookworm-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 +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 +# The root filesystem is read-only at runtime, so Next's cache goes to the /tmp tmpfs. +RUN rm -rf .next/cache && ln -s /tmp/next-cache .next/cache +USER node +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \ + CMD node -e "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 3955941..1ec027a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,16 @@ npm run dev # http://localhost:3000 | `npm run build` | Production build. Every page is statically prerendered. | | `npm run format` | Runs Prettier, which also sorts Tailwind classes | +## Deploying + +Live at **https://playground.jun-liu.net**. + +Merging to `main` deploys automatically: the server builds the `Dockerfile` and +swaps the running container. If a build fails, the running version stays up. + +The image uses Next's `standalone` output and runs as the unprivileged `node` +user on a read-only filesystem. + ## Structure ``` @@ -63,4 +73,5 @@ src/ - [ ] Toast stack with swipe-to-dismiss - [ ] Playwright visual regression and axe checks in CI - [ ] Case-study write-ups for each component: the problem, the options, and the decision -- [ ] Deploy (Vercel) and link from a portfolio +- [x] Deploy +- [ ] Link from a portfolio diff --git a/next.config.ts b/next.config.ts index a0cf0c7..8f8106d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Self-contained server bundle for the Docker image (see Dockerfile). + output: "standalone", cacheComponents: true, partialPrefetching: true, turbopack: {