diff --git a/Dockerfile b/Dockerfile index 606e1a2..4441b91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,60 @@ -FROM node:22-alpine AS builder +# Stage 1: Compile libvips with HEIC support +FROM node:22-bullseye-slim AS libvips-builder + +# Install build tools and dependencies for libvips and libheif +# These are Debian/Ubuntu package names +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + git \ + python3 \ + curl \ + meson \ + ninja-build \ + libglib2.0-dev \ + libexpat1-dev \ + libheif-dev \ + liblcms2-dev \ + libjpeg-dev \ + libpng-dev \ + libwebp-dev \ + libtiff-dev \ + libexif-dev \ + libgif-dev \ + # Dependencies for libheif codecs (libde265, x265) + libde265-dev \ + libx265-dev \ + # Clean up apt cache + && rm -rf /var/lib/apt/lists/* + +# Define libvips version to build (check sharp's package.json for compatible versions) +# Sharp 0.34.x supports libvips 8.15+ (8.16.2 is latest stable at time of writing) +ARG LIBVIPS_VERSION=8.16.2 + +# Build libvips from source +WORKDIR /tmp +RUN git clone --branch v${LIBVIPS_VERSION} --depth 1 https://github.com/libvips/libvips.git \ + && cd libvips \ + && meson setup build --prefix=/usr --buildtype=release \ + && ninja -C build \ + && ninja -C build install \ + && ldconfig \ + && cd / \ + && rm -rf /tmp/libvips + +# Stage 2: Build the Next.js app, linking sharp to the custom libvips +FROM node:22-bullseye-slim AS builder WORKDIR /app -# Install build dependencies for sharp and libvips with HEIF support -# See: https://sharp.pixelplumbing.com/install#alpine -RUN apk add --no-cache \ - build-base \ - pkgconf \ - # libvips runtime dependencies - libjpeg-turbo-dev \ - libpng-dev \ - libwebp-dev \ - tiff-dev \ - libexif-dev \ - lcms2-dev \ - glib-dev \ - # libvips itself and its HEIF support - vips-dev \ - libheif-dev \ - # Codecs for HEIF - libde265-dev \ - x265-dev +# Set environment variables for sharp to find libvips +ENV LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH +ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig:$PKG_CONFIG_PATH + +# Copy custom-built libvips libraries from libvips-builder stage +COPY --from=libvips-builder /usr/lib /usr/lib +COPY --from=libvips-builder /usr/bin /usr/bin +COPY --from=libvips-builder /usr/share /usr/share COPY package*.json ./ # Tell sharp to use the system-wide libvips we just installed with HEIF support @@ -30,8 +64,8 @@ RUN npm ci COPY . . RUN npm run build -# ---- Runner ---- -FROM node:22-alpine AS runner +# Stage 3: Final production image +FROM node:22-bullseye-slim AS runner WORKDIR /app @@ -39,27 +73,24 @@ ENV NODE_ENV=production ENV PORT=3000 ENV HOSTNAME="0.0.0.0" +# Create a non-root user RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs +USER nextjs -# Standalone output +# Copy runtime dependencies and Next.js standalone output COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public -# Sharp native binaries are no longer copied directly, -# as sharp is built against system libvips and is part of node_modules copied with the standalone app. -# If sharp is needed outside standalone, node_modules would need to be copied. -# COPY --from=builder /app/node_modules/sharp ./node_modules/sharp -# COPY --from=builder /app/node_modules/@img ./node_modules/@img +# Copy custom-built libvips libraries into the runtime image +COPY --from=libvips-builder /usr/lib /usr/lib +COPY --from=libvips-builder /usr/bin /usr/bin +COPY --from=libvips-builder /usr/share /usr/share +# Ensure data directories exist and are owned by nextjs user RUN mkdir -p /app/data/uploads/photos /app/data/uploads/videos /app/data/uploads/music \ && chown -R nextjs:nodejs /app/data -# Entrypoint fixes data dir permissions at runtime (volume mount overrides) -RUN apk add --no-cache su-exec \ - && printf '#!/bin/sh\nmkdir -p /app/data/uploads/photos /app/data/uploads/videos /app/data/uploads/music\nchown -R nextjs:nodejs /app/data 2>/dev/null || true\nexec su-exec nextjs node server.js\n' > /app/entrypoint.sh \ - && chmod +x /app/entrypoint.sh - EXPOSE 3000 -CMD ["/app/entrypoint.sh"] +CMD ["node", "server.js"] diff --git a/src/app/api/files/[...path]/route.ts b/src/app/api/files/[...path]/route.ts index c480c59..e63bcb1 100644 --- a/src/app/api/files/[...path]/route.ts +++ b/src/app/api/files/[...path]/route.ts @@ -55,7 +55,7 @@ export async function GET( contentType = 'image/jpeg' } else { try { - const converted = await sharp(fs.readFileSync(filePath)).jpeg({ quality: 90 }).toBuffer() + const converted = await sharp(fs.readFileSync(filePath), { failOn: 'none' }).jpeg({ quality: 90 }).toBuffer() fs.writeFileSync(jpegPath, converted) fileToSendPath = jpegPath ext = '.jpg' @@ -83,7 +83,7 @@ export async function GET( fileToSendPath = resizedPath } else { try { - let transformer = sharp(fs.readFileSync(fileToSendPath)) + let transformer = sharp(fs.readFileSync(fileToSendPath), { failOn: 'none' }) .resize(targetWidth, null, { withoutEnlargement: true }) if (ext === '.jpg' || ext === '.jpeg') { diff --git a/src/components/TimelineSection.tsx b/src/components/TimelineSection.tsx index 7f6b4c5..a95bc28 100644 --- a/src/components/TimelineSection.tsx +++ b/src/components/TimelineSection.tsx @@ -75,16 +75,16 @@ export default function TimelineSection({ entries }: TimelineSectionProps) { transition={{ duration: 0.6, delay: 0.1 }} className={`relative flex items-start ${ isLeft - ? 'pl-10 sm:pl-0 sm:pr-[50%]' - : 'pl-10 sm:pl-[50%]' + ? 'pl-10 sm:pl-0 sm:pr-[52%]' + : 'pl-10 sm:pl-[52%]' }`} > {/* Content Card */} setSelectedEntry(entry)} - whileHover={{ scale: 1.02, y: -2 }} - whileTap={{ scale: 0.98 }} - className={`group cursor-pointer text-left w-full bg-white/80 backdrop-blur-sm rounded-xl shadow-md border border-warm-border p-4 sm:p-5 hover:shadow-lg transition-all ${ + whileHover={{ scale: 1.01, y: -1 }} + whileTap={{ scale: 0.99 }} + className={`group cursor-pointer text-left w-full bg-white/80 backdrop-blur-sm rounded-xl shadow-md border border-warm-border p-3.5 sm:p-4.5 hover:shadow-lg transition-all ${ isLeft ? 'sm:mr-auto' : 'sm:ml-auto' } ${isSpecial ? 'ring-2 ring-warm-gold/30' : ''}`} style={{ minWidth: 0 }} @@ -93,24 +93,25 @@ export default function TimelineSection({ entries }: TimelineSectionProps) { {photos.length > 0 && (
{photos.slice(0, 2).map((filename, i) => ( - +
+ +
))}
)} {/* Date */} -
+
{formatDate(entry.year, entry.month, entry.day)}
{/* Title */} -

+

{entry.title}