feat: add Docker deployment for self-hosted infrastructure

- Add Dockerfile for standalone Next.js deployment
- Add docker-compose.yml with Traefik labels for cryptocommonsgather.ing
- Fix Stripe initialization to happen at runtime (not build time)
- Update Next.js to 16.0.7 (security fix)
- Add output: 'standalone' to next.config.mjs
- Add .gitignore and .dockerignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-06 15:57:46 +01:00
parent a3cf6995b2
commit 30185941bc
10 changed files with 4901 additions and 18 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.git
.gitignore
.next
.env
.env.local
.env.production.local
.env.development.local

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# dependencies
node_modules/
.pnp
.pnp.js
# build output
.next/
out/
build/
# env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# misc
.DS_Store
*.pem
# typescript
*.tsbuildinfo
next-env.d.ts

43
Dockerfile Normal file
View File

@ -0,0 +1,43 @@
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
RUN corepack enable pnpm && pnpm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@ -1,12 +1,16 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
})
export async function POST(request: NextRequest) {
try {
const stripeSecretKey = process.env.STRIPE_SECRET_KEY
if (!stripeSecretKey) {
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 })
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
})
const formData = await request.formData()
const paymentMethod = formData.get("paymentMethod") as string
const registrationDataStr = formData.get("registrationData") as string

View File

@ -1,14 +1,19 @@
import { type NextRequest, NextResponse } from "next/server"
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
})
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
export async function POST(request: NextRequest) {
try {
const stripeSecretKey = process.env.STRIPE_SECRET_KEY
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
if (!stripeSecretKey || !webhookSecret) {
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 })
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
})
const body = await request.text()
const signature = request.headers.get("stripe-signature")!

21
docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
services:
ccg-website:
build: .
container_name: ccg-website
restart: unless-stopped
environment:
- NODE_ENV=production
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}
labels:
- "traefik.enable=true"
- "traefik.http.routers.ccg.rule=Host(`cryptocommonsgather.ing`) || Host(`www.cryptocommonsgather.ing`)"
- "traefik.http.routers.ccg.entrypoints=web,websecure"
- "traefik.http.services.ccg.loadbalancer.server.port=3000"
networks:
- traefik-public
networks:
traefik-public:
external: true

View File

@ -1,12 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
}
export default nextConfig

4755
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@
"embla-carousel-react": "8.5.1",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "16.0.3",
"next": "^16.0.7",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-day-picker": "9.8.0",
@ -71,4 +71,4 @@
"tw-animate-css": "1.3.3",
"typescript": "^5"
}
}
}

View File

@ -1,6 +1,10 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}