Skip to content

Commit 36ce7f2

Browse files
Added Dockerfile (#156)
* Added `Deockerfile` * sentry credentials passing fix * Update README.md
1 parent 8b1ec90 commit 36ce7f2

6 files changed

Lines changed: 211 additions & 2 deletions

File tree

.dockerignore

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# dependencies
2+
node_modules
3+
.pnp
4+
.pnp.*
5+
6+
# build output
7+
.next
8+
out
9+
build
10+
dist
11+
12+
# env files (provide at runtime, not build time)
13+
.env
14+
.env.local
15+
.env.*.local
16+
17+
# version control
18+
.git
19+
.gitignore
20+
21+
# editor / tooling
22+
.claude
23+
.conductor
24+
.ruby-lsp
25+
.vscode
26+
.idea
27+
.DS_Store
28+
29+
# CI / docs / tests (not needed in image)
30+
.github
31+
docs
32+
README.md
33+
LICENSE
34+
coverage
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
vitest.config.ts
38+
39+
# logs
40+
npm-debug.log*
41+
yarn-debug.log*
42+
yarn-error.log*
43+
.pnpm-debug.log*
44+
45+
# vercel
46+
.vercel
47+
48+
# docker
49+
Dockerfile
50+
.dockerignore

Dockerfile

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# syntax=docker/dockerfile:1.7
2+
3+
# Multi-stage build for Next.js 16 (standalone output) on Node.js 22 (Alpine).
4+
#
5+
# Build (Spree env required: pages are prerendered against the Spree API at build time):
6+
# docker build \
7+
# --build-arg SPREE_API_URL=https://your-spree.example.com \
8+
# --build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
9+
# -t storefront .
10+
#
11+
# Run:
12+
# docker run -p 3001:3001 --env-file .env.local storefront
13+
#
14+
# Optional Sentry source map upload at build time (skipped when SENTRY_DSN is unset).
15+
# SENTRY_AUTH_TOKEN is read via a BuildKit secret so it never lands in image
16+
# layers, history, or shared builder caches.
17+
# SENTRY_AUTH_TOKEN=... docker build \
18+
# --build-arg SPREE_API_URL=... \
19+
# --build-arg SPREE_PUBLISHABLE_KEY=... \
20+
# --build-arg SENTRY_DSN=... \
21+
# --build-arg SENTRY_ORG=... \
22+
# --build-arg SENTRY_PROJECT=... \
23+
# --secret id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
24+
# -t storefront .
25+
26+
ARG NODE_VERSION=22-alpine
27+
28+
29+
# ---- deps: install production+dev dependencies for the build ----
30+
FROM node:${NODE_VERSION} AS deps
31+
WORKDIR /app
32+
33+
# libc6-compat keeps a few native modules happy on Alpine (musl).
34+
RUN apk add --no-cache libc6-compat
35+
36+
COPY package.json package-lock.json ./
37+
RUN --mount=type=cache,target=/root/.npm \
38+
npm ci --include=dev
39+
40+
41+
# ---- builder: compile the Next.js app ----
42+
FROM node:${NODE_VERSION} AS builder
43+
WORKDIR /app
44+
45+
ENV NEXT_TELEMETRY_DISABLED=1
46+
ENV NODE_ENV=production
47+
48+
# Spree API config — required at build time because the app prerenders pages
49+
# that fetch from Spree (categories, products, etc.).
50+
ARG SPREE_API_URL
51+
ARG SPREE_PUBLISHABLE_KEY
52+
ENV SPREE_API_URL=$SPREE_API_URL \
53+
SPREE_PUBLISHABLE_KEY=$SPREE_PUBLISHABLE_KEY
54+
55+
# Optional Sentry release/source-map upload. When SENTRY_DSN is empty,
56+
# next.config.ts skips withSentryConfig entirely, so the build still works.
57+
# SENTRY_AUTH_TOKEN is intentionally not declared as ARG/ENV — it's mounted
58+
# only for the build step via --mount=type=secret below, so it never persists
59+
# in image layers or build cache.
60+
ARG SENTRY_DSN=""
61+
ARG SENTRY_ORG=""
62+
ARG SENTRY_PROJECT=""
63+
ENV SENTRY_DSN=$SENTRY_DSN \
64+
SENTRY_ORG=$SENTRY_ORG \
65+
SENTRY_PROJECT=$SENTRY_PROJECT
66+
67+
COPY --from=deps /app/node_modules ./node_modules
68+
COPY . .
69+
70+
RUN --mount=type=secret,id=sentry_auth_token,required=false \
71+
SENTRY_AUTH_TOKEN="$(cat /run/secrets/sentry_auth_token 2>/dev/null || true)" \
72+
npm run build
73+
74+
75+
# ---- runner: minimal runtime image ----
76+
FROM node:${NODE_VERSION} AS runner
77+
WORKDIR /app
78+
79+
ENV NODE_ENV=production
80+
ENV NEXT_TELEMETRY_DISABLED=1
81+
ENV PORT=3001
82+
ENV HOSTNAME=0.0.0.0
83+
84+
RUN addgroup --system --gid 1001 nodejs \
85+
&& adduser --system --uid 1001 nextjs
86+
87+
# Static assets and the standalone server bundle.
88+
# The standalone output ships its own minimal node_modules.
89+
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
90+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
91+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
92+
93+
USER nextjs
94+
95+
EXPOSE 3001
96+
97+
CMD ["node", "server.js"]

README.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@ Spree Backend → Webhook POST → /api/webhooks/spree → render email → send
367367

368368
The webhook route handler (`src/app/api/webhooks/spree/route.ts`) uses `createWebhookHandler` from `src/lib/spree/webhooks` — signature verification and event routing are handled automatically.
369369

370-
## Deploy on Vercel
370+
## Deployment
371+
372+
### Vercel
371373

372374
The easiest way to deploy is using [Vercel](https://vercel.com/new):
373375

@@ -380,6 +382,59 @@ The easiest way to deploy is using [Vercel](https://vercel.com/new):
380382
- `SENTRY_DSN`, `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` (optional — for error tracking with readable stack traces)
381383
4. Deploy
382384

385+
### Docker
386+
387+
A multi-stage `Dockerfile` is included at the repo root. It uses Next.js standalone output to produce a small (~240 MB) image based on `node:22-alpine`, runs as a non-root user, and exposes port `3001`.
388+
389+
> **Note:** `SPREE_API_URL` and `SPREE_PUBLISHABLE_KEY` are required at **build time** because the storefront prerenders pages against the Spree API. Point them at a Spree instance reachable from wherever you run `docker build` (hosted Spree, tunnel, or `host.docker.internal` for a local backend on Docker Desktop).
390+
391+
**Build:**
392+
393+
```bash
394+
docker build \
395+
--build-arg SPREE_API_URL=https://your-spree.example.com \
396+
--build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
397+
-t spree-storefront .
398+
```
399+
400+
**Run:**
401+
402+
```bash
403+
docker run -p 3001:3001 --env-file .env.local spree-storefront
404+
```
405+
406+
**Optional — Sentry source map upload at build time:**
407+
408+
`SENTRY_AUTH_TOKEN` is mounted via a BuildKit secret so it never lands in image layers or the build cache. Other Sentry vars are passed as regular build args.
409+
410+
```bash
411+
SENTRY_AUTH_TOKEN=... docker build \
412+
--build-arg SPREE_API_URL=... \
413+
--build-arg SPREE_PUBLISHABLE_KEY=... \
414+
--build-arg SENTRY_DSN=... \
415+
--build-arg SENTRY_ORG=... \
416+
--build-arg SENTRY_PROJECT=... \
417+
--secret id=sentry_auth_token,env=SENTRY_AUTH_TOKEN \
418+
-t spree-storefront .
419+
```
420+
421+
**Building against a local Spree backend** (Docker Desktop on macOS/Windows):
422+
423+
```bash
424+
docker build \
425+
--add-host=host.docker.internal:host-gateway \
426+
--build-arg SPREE_API_URL=http://host.docker.internal:3000 \
427+
--build-arg SPREE_PUBLISHABLE_KEY=your_publishable_key \
428+
-t spree-storefront .
429+
430+
docker run -p 3001:3001 \
431+
--add-host=host.docker.internal:host-gateway \
432+
--env-file .env.local \
433+
spree-storefront
434+
```
435+
436+
The same env vars listed under [Vercel](#vercel) apply to runtime configuration.
437+
383438
## License
384439

385440
MIT

next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import createNextIntlPlugin from "next-intl/plugin";
55
const withNextIntl = createNextIntlPlugin();
66

77
const nextConfig: NextConfig = {
8+
output: "standalone",
89
allowedDevOrigins: ["shop.lvh.me", "*.trycloudflare.com", "192.168.33.13"],
910
env: {
1011
NEXT_PUBLIC_SENTRY_DSN: process.env.SENTRY_DSN || "",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"use client";
2+
3+
export function CurrentYear() {
4+
return <>{new Date().getFullYear()}</>;
5+
}

src/components/layout/Footer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Link from "next/link";
33
import { getTranslations } from "next-intl/server";
44
import { POLICY_LINKS } from "@/lib/constants/policies";
55
import { getStoreDescription, getStoreName } from "@/lib/store";
6+
import { CurrentYear } from "./CurrentYear";
67

78
const storeName = getStoreName();
89
const storeDescription = getStoreDescription();
@@ -149,7 +150,7 @@ export async function Footer({
149150

150151
<div className="mt-8 pt-8 border-t border-neutral-800 text-xs text-neutral-400 text-center">
151152
<p>
152-
&copy; {new Date().getFullYear()} {storeName}. {t("poweredBy")}{" "}
153+
&copy; <CurrentYear /> {storeName}. {t("poweredBy")}{" "}
153154
<Link
154155
href="https://spreecommerce.org"
155156
target="_blank"

0 commit comments

Comments
 (0)