#!/bin/bash # Deploy pipeline: doormile-next (Next.js standalone Node server) -> build/ -> Node Docker image. # Standalone (not static export) is required because the contact form's Server # Action (src/actions/sendEmail.ts, SMTP via nodemailer) needs a live Node # process at runtime - it cannot run under static/nginx serving. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_REPO="${SOURCE_REPO:-$SCRIPT_DIR/../doormile-next}" DEPLOY_REPO="$SCRIPT_DIR" STANDALONE_DIR="$SOURCE_REPO/.next/standalone" BUILD_DIR="$DEPLOY_REPO/build" echo "Source repository: $SOURCE_REPO" echo "Deploy repository: $DEPLOY_REPO" if [ ! -d "$SOURCE_REPO" ]; then echo "ERROR: source repository not found at $SOURCE_REPO" >&2 exit 1 fi if [ ! -f "$SOURCE_REPO/package.json" ] || [ ! -f "$SOURCE_REPO/next.config.ts" ]; then echo "ERROR: $SOURCE_REPO does not look like the Next.js source repo (missing package.json or next.config.ts)" >&2 exit 1 fi if ! grep -Eq 'output:\s*"standalone"' "$SOURCE_REPO/next.config.ts"; then echo "ERROR: $SOURCE_REPO/next.config.ts does not have output: \"standalone\"." >&2 echo "This pipeline runs the app as a Node server (required by the SMTP Server Action) - a non-standalone build cannot be deployed here." >&2 exit 1 fi SOURCE_BRANCH="$(git -C "$SOURCE_REPO" branch --show-current)" SOURCE_HEAD="$(git -C "$SOURCE_REPO" rev-parse HEAD)" DEPLOY_BRANCH="$(git -C "$DEPLOY_REPO" branch --show-current)" DEPLOY_HEAD_BEFORE="$(git -C "$DEPLOY_REPO" rev-parse HEAD)" echo "Source branch/HEAD: $SOURCE_BRANCH @ $SOURCE_HEAD" echo "Deploy branch/HEAD (before sync): $DEPLOY_BRANCH @ $DEPLOY_HEAD_BEFORE" echo "Cleaning previous build artifacts in source repo (.next)..." rm -rf "$SOURCE_REPO/.next" echo "Building Next.js application..." (cd "$SOURCE_REPO" && npm run build) if [ ! -f "$STANDALONE_DIR/server.js" ]; then echo "ERROR: $STANDALONE_DIR/server.js not found after build. Standalone output was not generated." >&2 exit 1 fi echo "Standalone server verified at $STANDALONE_DIR/server.js" echo "Replacing deploy build/ with the fresh standalone artifact..." rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" # server.js, node_modules, package.json, and the traced .next/server tree rsync -a "$STANDALONE_DIR"/ "$BUILD_DIR"/ # static assets and public/ are deliberately excluded from standalone output # by Next.js and must be copied manually mkdir -p "$BUILD_DIR/.next" rsync -a --delete "$SOURCE_REPO/.next/static"/ "$BUILD_DIR/.next/static"/ rsync -a --delete "$SOURCE_REPO/public"/ "$BUILD_DIR/public"/ if [ ! -f "$BUILD_DIR/server.js" ] || [ ! -d "$BUILD_DIR/.next/static" ] || [ ! -d "$BUILD_DIR/public" ]; then echo "ERROR: $BUILD_DIR is missing required standalone artifacts (server.js, .next/static, public)." >&2 exit 1 fi echo "Artifact verified: build/server.js, build/.next/static, build/public all present." # `sharp` ships platform-specific native binaries as optional dependencies. # Since this build runs on the host machine (macOS), only the host's binary # (e.g. darwin-arm64) gets installed/traced - it will NOT load inside the # Linux (node:20-alpine, musl libc) container and crashes the server at # runtime (surfaces as a 502 after an otherwise-successful docker build). # `npm pack` can fetch another platform's prebuilt tarball without an # install-time OS/arch check, so pull the linux-musl builds directly and # drop them into node_modules alongside the host's. if [ -d "$BUILD_DIR/node_modules/sharp" ]; then echo echo "Injecting linux-musl sharp binaries (container runs node:20-alpine)..." SHARP_TMP="$(mktemp -d)" trap 'rm -rf "$SHARP_TMP"' EXIT SHARP_VERSION="$(node -p "require('$BUILD_DIR/node_modules/sharp/package.json').version")" LIBVIPS_VERSION="$(node -p "require('$BUILD_DIR/node_modules/sharp/package.json').optionalDependencies['@img/sharp-libvips-linuxmusl-x64']")" SHARP_PKGS=( "@img/sharp-linuxmusl-x64@${SHARP_VERSION}" "@img/sharp-libvips-linuxmusl-x64@${LIBVIPS_VERSION}" "@img/sharp-linuxmusl-arm64@${SHARP_VERSION}" "@img/sharp-libvips-linuxmusl-arm64@${LIBVIPS_VERSION}" ) (cd "$SHARP_TMP" && npm pack "${SHARP_PKGS[@]}" >/dev/null) for spec in "${SHARP_PKGS[@]}"; do name="${spec%@*}" version="${spec##*@}" tarball_name="$(echo "${name#@}" | tr '/' '-')-${version}.tgz" dest="$BUILD_DIR/node_modules/$name" mkdir -p "$dest" tar xzf "$SHARP_TMP/$tarball_name" -C "$dest" --strip-components=1 done # The host's own platform binary is dead weight in the Linux container. find "$BUILD_DIR/node_modules/@img" -maxdepth 1 -type d \( -name 'sharp-darwin-*' -o -name 'sharp-libvips-darwin-*' -o -name 'sharp-win32-*' -o -name 'sharp-libvips-win32-*' \) -exec rm -rf {} + rm -rf "$SHARP_TMP" trap - EXIT echo "Linux-musl sharp binaries (x64 + arm64) installed; host platform binary removed." fi echo echo "Source: $SOURCE_REPO ($SOURCE_BRANCH @ $SOURCE_HEAD)" echo "Deploy: $DEPLOY_REPO ($DEPLOY_BRANCH @ $DEPLOY_HEAD_BEFORE)" echo "Synced: $STANDALONE_DIR (+ .next/static, public) -> $BUILD_DIR" echo echo "Deploy repo status:" git -C "$DEPLOY_REPO" status --short echo if command -v docker >/dev/null 2>&1; then echo "Building Docker image..." (cd "$DEPLOY_REPO" && docker build -t doormile-app .) else echo "WARNING: docker not found on this machine - skipping 'docker build'." >&2 echo "Run 'docker build -t doormile-app .' from $DEPLOY_REPO once docker is available." >&2 fi echo echo "Done. This script did NOT commit, push, or trigger Dokploy." echo "Review the diff above, then: git add -A && git commit && git push, and redeploy doormilesite in Dokploy." echo "Dokploy must route to internal container port 80." echo "Required SMTP environment variables in Dokploy: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, CONTACT_RECEIVER"