Skip to content

Commit

Permalink
refactor: various improvements for Docker build process
Browse files Browse the repository at this point in the history
  • Loading branch information
spencerlepine committed Nov 9, 2024
1 parent db763f9 commit da0d6d5
Show file tree
Hide file tree
Showing 13 changed files with 88 additions and 69 deletions.
11 changes: 7 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
pnpm-debug.log
.next
.env*
docker
.git
.git
.gitignore
README.md
*.swp
.vscode
35 changes: 17 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@ Full-stack e-commerce store for developer laptop stickers. Automated with dropsh

https://github.com/user-attachments/assets/d32962b3-6aa8-401c-ab43-440fff3e31cc

<img width="800px" style="margin:auto" src="./.github/swagsticker.com-system-diagram.png" alt="SwagSticker.com system diagram">

## 🎯 Project Overview

<!-- TODO_README -->

- designed autonomous and scalable e-commerce store
- enabled both guest checkout and no-password login for seamless checkout experience (JSON-Web-Token (JWT))
- designed an accessible, responsive, and performant UI with Next.js and TailwindCSS
- utilized open-source SDKs to integrate third-party APIs
- reduced initial load time to 1.2secs
- generated product images with OpenCV python script
- optimized load times and SEO with server-side rendering
- avoided complex database setup with JSON product catalog and GraphQL for easy migration to headless CMS
- secured checkout payments with stripe forms and bot detection
- load test, >95% success rate with <300ms response for up to XXX users peak traffic
- Developed an autonomous, full-stack **e-commerce** store using **Next.js** and **TypeScript**, supporting automated dropshipping via **Printify SDK**
- Implemented **passwordless authentication** using JSON Web Tokens (JWT) for a secure, seamless checkout experience
- Integrated **Stripe SDK** with embedded payment forms and Webhooks for secure, automated payment processing, including bot detection for fraud prevention
- Enhanced page load speed and SEO with **server-side rendering**, achieving sub-2.5s initial load times
- Scaled to handle 1,500+ monthly active users, supporting up to 50 requests per second during peak traffic
- Conducted load tests to validate system reliability, achieving a >95% success rate with **P90 response times** between under 300 ms

## 🛠️ Built With

Expand All @@ -40,6 +32,10 @@ https://github.com/user-attachments/assets/d32962b3-6aa8-401c-ab43-440fff3e31cc

![Account Page Feature](./.github/feature-view-orders.png) _View orders, download receipts, and track shipping status._

## 🏗️ System Diagram

<img width="800px" style="margin:auto" src="./.github/swagsticker.com-system-diagram.png" alt="SwagSticker.com system diagram">

## 💻 Local Development

#### Prerequisites
Expand Down Expand Up @@ -70,23 +66,26 @@ npm run dev
# visit http://locahost:3000
```

## 🐋 Docker

#### Local Docker Container

```sh
stripe login
stripe listen --forward-to localhost:3000/api/v1/webhook/checkout
# *open separate terminal*

cp .env.template .env.development
docker-compose -f ./docker/docker-compose.dev.yml --env-file .env.development up --build
cp .env.template .env.production
docker-compose -f ./docker/docker-compose.dev.yml --env-file .env.production up --build
# visit http://locahost:3000
```

#### Production Build
#### Production Docker Build

```sh
cp .env.template .env.production
npm run build
docker-compose -f ./docker/docker-compose.prod.yml --env-file .env.production up --build
# visit http://localhost
```

## License
Expand Down
28 changes: 10 additions & 18 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
FROM node:20.16-alpine3.19 AS base

# 1. Install dependencies and build
FROM --platform=$BUILDPLATFORM base AS builder
# enable pnpm - https://pnpm.io/docker
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

# source: https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine
RUN apk add --no-cache libc6-compat

# 1. Install dependencies and build
FROM --platform=$BUILDPLATFORM base AS builder

WORKDIR /app

# Install dependencies based on the preferred package manager
# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
# RUN \
# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
# elif [ -f package-lock.json ]; then npm ci; \
# elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
# else echo "Lockfile not found." && exit 1; \
# fi

# Install dependencies with pnpm (with cache)
RUN npm install -g pnpm
# Copy lockfile and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile

# Copy source code and environment files
COPY . .
COPY .env.production .env.development ./

RUN pnpm build

# 2. Production image, copy all the files and run
Expand All @@ -40,7 +32,7 @@ ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME="0.0.0.0"

COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone /app

# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", ".next/standalone/server.js"]
CMD ["node", "./server.js"]
5 changes: 4 additions & 1 deletion docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ services:
context: ../
dockerfile: docker/Dockerfile
ports:
- "3000:3000"
- "3000:3000"
env_file:
- ../.env.production
restart: always
8 changes: 6 additions & 2 deletions next.config.mjs → next.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
productionBrowserSourceMaps: false,
trailingSlash: false,
experimental: {
optimizePackageImports: ['react-icons'],
},
swcMinify: true,
output: 'standalone', // for Docker
trailingSlash: false,
};

export default nextConfig;
module.exports = nextConfig
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"react-icons": "^5.3.0",
"sharp": "0.32.6",
"stripe": "^16.9.0",
"tailwindcss-animate": "^1.0.7",
"use-shopping-cart": "^3.2.0",
"winston": "^3.15.0"
},
Expand Down
12 changes: 0 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export default async function AccountPage() {
const { email, error } = verifyJwt(token);
if (error) return notFound();

console.log(email);
const orders = await getOrders(email as string);

return (
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/v1/auth/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const POST = async () => {

response.cookies.set('authToken', '', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
secure: true,
maxAge: 0,
path: '/',
});
Expand Down
22 changes: 22 additions & 0 deletions src/app/api/v1/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextResponse } from 'next/server';
import logger from '@/lib/logger';
import { checkPrintifyStatus } from '@/lib/printify';
import { checkStripeStatus } from '@/lib/stripe';

export async function GET() {
try {
const printifyStatus = await checkPrintifyStatus();
const stripeStatus = await checkStripeStatus();
const status = printifyStatus === "operational" && stripeStatus === "operational" ? "operational" : "degraded";

if (status === "degraded") {
logger.error('[Status] report:', { status, printifyStatus, stripeStatus });
}

return NextResponse.json({ status });
} catch (error) {
logger.error('[Status] Error checking status', { error });
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}

2 changes: 2 additions & 0 deletions src/app/order-confirmation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import OrderConfirmation from '@/components/OrderConfirmation';
import { stripe } from '@/lib/stripe';
import { notFound } from 'next/navigation';

export const dynamic = 'force-dynamic'; // Dynamic server-side rendering

async function getSession(sessionId: string) {
try {
const session = await stripe.checkout.sessions.retrieve(sessionId!);
Expand Down
20 changes: 9 additions & 11 deletions src/lib/printify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@ const PRINTIFY_VARIANT_IDS = {
[STICKER_SIZES.FOUR_BY_FOUR_IN]: 95745,
};

// export const formatCartItemsForPrintify = (cartItems: CartItem[]): PrintifyLineItem[] => {
// return cartItems.map(cartItem => ({
// print_provider_id: PRINTIFY_PRINT_PROVIDER_ID,
// blueprint_id: PRINTIFY_BLUEPRINT_ID,
// variant_id: PRINTIFY_VARIANT_IDS[cartItem.product_data.size],
// print_areas: {
// front: retrieveStickerPNGFileUrl(cartItem.product_data.productId),
// },
// quantity: cartItem.quantity,
// }));
// };
export async function checkPrintifyStatus() {
try {
await printify.shops.list();
return "operational";
} catch (error) {
logger.error('[Status] Printify status check failed', { error });
return "degraded";
}
}

export const formatCartItemsForPrintify = (lineItems: StripeType.LineItem[]): PrintifyLineItem[] => {
return lineItems.map(item => ({
Expand Down
10 changes: 10 additions & 0 deletions src/lib/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
},
});

export async function checkStripeStatus() {
try {
await stripe.accounts.list({ limit: 1 });
return "operational";
} catch (error) {
logger.error('[Status] Stripe status check failed', { error });
return "degraded";
}
}

export const formatCartItemsForStripe = (cartItems: CartItem[]): Stripe.Checkout.SessionCreateParams.LineItem[] => {
return cartItems.map(cartItem => {
const lineItem = {
Expand Down

0 comments on commit da0d6d5

Please sign in to comment.