From b7fdae8967a3e24a8e9ba64f8769505d08b97a0c Mon Sep 17 00:00:00 2001 From: Ali Taghavi Date: Tue, 19 May 2026 01:08:17 +0330 Subject: [PATCH] Add Iran offline deploy: Mac bundle + prebuilt Docker image. Fix pnpm workspace error during prisma generate, and add bundle-for-iran.sh so the app is built on Mac (full internet) and the server only copies artifacts. Co-authored-by: Cursor --- .gitignore | 2 + Dockerfile | 4 +- Dockerfile.prebuilt | 26 ++++ docker-compose.prebuilt.yml | 50 +++++++ package.json | 3 +- pnpm-workspace.yaml | 3 - prisma/migrations/0_init/migration.sql | 174 +++++++++++++++++++++++++ public/robots.txt | 11 ++ scripts/bundle-for-iran.sh | 64 +++++++++ scripts/server-deploy.sh | 61 ++++++--- 10 files changed, 374 insertions(+), 24 deletions(-) create mode 100644 Dockerfile.prebuilt create mode 100644 docker-compose.prebuilt.yml delete mode 100644 pnpm-workspace.yaml create mode 100644 public/robots.txt create mode 100755 scripts/bundle-for-iran.sh diff --git a/.gitignore b/.gitignore index 787dd333..6376606d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ # production /build +/iran-bundle +iran-bundle.tar.gz # misc .DS_Store diff --git a/Dockerfile b/Dockerfile index da4d23db..f1017494 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,10 @@ COPY . . ENV PRISMA_QUERY_ENGINE_LIBRARY=/app/prisma-binaries/libquery_engine-debian-openssl-3.0.x.so.node ENV PRISMA_SCHEMA_ENGINE_BINARY=/app/prisma-binaries/schema-engine-debian-openssl-3.0.x -RUN pnpm prisma generate +RUN ./node_modules/.bin/prisma generate # Placeholder only — real DATABASE_URL is injected at runtime via docker-compose ENV DATABASE_URL="mysql://build:build@localhost:3306/build" -RUN pnpm build +RUN ./node_modules/.bin/next build && ./node_modules/.bin/next-sitemap # ── runner ──────────────────────────────────────────────────────────────────── FROM docker.arvancloud.ir/library/node:20-slim AS runner diff --git a/Dockerfile.prebuilt b/Dockerfile.prebuilt new file mode 100644 index 00000000..8b70aea3 --- /dev/null +++ b/Dockerfile.prebuilt @@ -0,0 +1,26 @@ +# Server-side image — NO npm/pnpm/network. Requires iran-bundle/ from scripts/bundle-for-iran.sh (run on Mac). +FROM docker.arvancloud.ir/library/node:20-slim AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 +ENV PRISMA_QUERY_ENGINE_LIBRARY=/app/prisma-binaries/libquery_engine-debian-openssl-3.0.x.so.node + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY iran-bundle/standalone ./ +COPY iran-bundle/static ./.next/static +COPY iran-bundle/public ./public +COPY iran-bundle/prisma-binaries ./prisma-binaries +COPY iran-bundle/node_modules/.prisma ./node_modules/.prisma +COPY iran-bundle/node_modules/@prisma ./node_modules/@prisma + +RUN mkdir -p /app/public/uploads && chown -R nextjs:nodejs /app/public/uploads + +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/docker-compose.prebuilt.yml b/docker-compose.prebuilt.yml new file mode 100644 index 00000000..2f281634 --- /dev/null +++ b/docker-compose.prebuilt.yml @@ -0,0 +1,50 @@ +services: + db: + image: docker.arvancloud.ir/library/mariadb:11 + container_name: biztaghavi_db + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MARIADB_DATABASE: ${DB_NAME:-biztaghavi} + MARIADB_USER: ${DB_USER:-biztaghavi} + MARIADB_PASSWORD: ${DB_PASSWORD} + volumes: + - db-data:/var/lib/mysql + networks: + - app-internal + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 20 + + next-app: + build: + context: . + dockerfile: Dockerfile.prebuilt + container_name: biztaghavi_app + restart: unless-stopped + depends_on: + db: + condition: service_healthy + env_file: .env + environment: + NODE_ENV: production + DATABASE_URL: mysql://${DB_USER:-biztaghavi}:${DB_PASSWORD}@db:3306/${DB_NAME:-biztaghavi} + ports: + - "127.0.0.1:3009:3000" + volumes: + - uploads:/app/public/uploads + networks: + - app-internal + - nodecloud-net + +volumes: + db-data: + uploads: + +networks: + app-internal: + driver: bridge + nodecloud-net: + external: true diff --git a/package.json b/package.json index e5368ae2..45cf01a9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "@tiptap/extension-character-count": "^2.11.0" }, "pnpm": { - "onlyBuiltDependencies": ["@parcel/watcher", "@swc/core"] + "onlyBuiltDependencies": ["@parcel/watcher", "@swc/core"], + "ignoredBuiltDependencies": ["sharp", "unrs-resolver"] }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 581a9d5b..00000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -ignoredBuiltDependencies: - - sharp - - unrs-resolver diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql index e69de29b..1ce8fce9 100644 --- a/prisma/migrations/0_init/migration.sql +++ b/prisma/migrations/0_init/migration.sql @@ -0,0 +1,174 @@ +-- CreateTable +CREATE TABLE `Post` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(500) NOT NULL, + `slug` VARCHAR(500) NOT NULL, + `locale` VARCHAR(5) NOT NULL DEFAULT 'fa', + `category` VARCHAR(100) NOT NULL, + `excerpt` TEXT NULL, + `body` LONGTEXT NULL, + `coverImage` VARCHAR(1000) NULL, + `featured` BOOLEAN NOT NULL DEFAULT false, + `publishedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `seoTitle` VARCHAR(255) NULL, + `seoDesc` VARCHAR(500) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Post_slug_key`(`slug`), + INDEX `Post_locale_featured_idx`(`locale`, `featured`), + INDEX `Post_locale_category_idx`(`locale`, `category`), + INDEX `Post_publishedAt_idx`(`publishedAt` DESC), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Project` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(500) NOT NULL, + `slug` VARCHAR(500) NOT NULL, + `locale` VARCHAR(5) NOT NULL DEFAULT 'fa', + `projectType` VARCHAR(100) NOT NULL, + `description` TEXT NULL, + `body` LONGTEXT NULL, + `coverImage` VARCHAR(1000) NULL, + `gallery` TEXT NULL, + `techStack` TEXT NULL, + `toolsUsed` TEXT NULL, + `liveUrl` VARCHAR(500) NULL, + `githubUrl` VARCHAR(500) NULL, + `featured` BOOLEAN NOT NULL DEFAULT false, + `seoTitle` VARCHAR(255) NULL, + `seoDesc` VARCHAR(500) NULL, + `sortOrder` INTEGER NOT NULL DEFAULT 0, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Project_slug_key`(`slug`), + INDEX `Project_locale_projectType_idx`(`locale`, `projectType`), + INDEX `Project_locale_featured_idx`(`locale`, `featured`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Product` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(500) NOT NULL, + `slug` VARCHAR(500) NOT NULL, + `locale` VARCHAR(5) NOT NULL DEFAULT 'fa', + `description` TEXT NULL, + `price` DOUBLE NULL, + `currency` VARCHAR(10) NOT NULL DEFAULT 'IRR', + `coverImage` VARCHAR(1000) NULL, + `purchaseUrl` VARCHAR(500) NULL, + `productType` VARCHAR(50) NOT NULL DEFAULT 'digital', + `featured` BOOLEAN NOT NULL DEFAULT false, + `seoTitle` VARCHAR(255) NULL, + `seoDesc` VARCHAR(500) NULL, + `sortOrder` INTEGER NOT NULL DEFAULT 0, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `Product_slug_key`(`slug`), + INDEX `Product_locale_idx`(`locale`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `Tag` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(100) NOT NULL, + `slug` VARCHAR(100) NOT NULL, + + UNIQUE INDEX `Tag_slug_key`(`slug`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `PostTag` ( + `postId` VARCHAR(191) NOT NULL, + `tagId` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`postId`, `tagId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `TimelineEvent` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(500) NOT NULL, + `date` DATETIME(3) NOT NULL, + `description` TEXT NULL, + `icon` VARCHAR(10) NULL, + `category` VARCHAR(50) NOT NULL DEFAULT 'career', + `sortOrder` INTEGER NOT NULL DEFAULT 0, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `TimelineEvent_date_idx`(`date` DESC), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `UsesItem` ( + `id` VARCHAR(191) NOT NULL, + `title` VARCHAR(255) NOT NULL, + `description` TEXT NULL, + `category` VARCHAR(50) NOT NULL, + `url` VARCHAR(500) NULL, + `image` VARCHAR(1000) NULL, + `sortOrder` INTEGER NOT NULL DEFAULT 0, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `UsesItem_category_idx`(`category`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `SiteSettings` ( + `id` VARCHAR(191) NOT NULL DEFAULT 'main', + `siteTitle` VARCHAR(255) NOT NULL DEFAULT 'Ali Taghavi', + `description` TEXT NULL, + `currentStatus` TEXT NULL, + `telegramChannel` VARCHAR(255) NULL, + `socialGithub` VARCHAR(255) NULL, + `socialLinkedin` VARCHAR(255) NULL, + `socialTwitter` VARCHAR(255) NULL, + `socialTelegram` VARCHAR(255) NULL, + `socialInstagram` VARCHAR(255) NULL, + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `AdminUser` ( + `id` VARCHAR(191) NOT NULL, + `username` VARCHAR(100) NOT NULL, + `password` VARCHAR(255) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + UNIQUE INDEX `AdminUser_username_key`(`username`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `ContactMessage` ( + `id` VARCHAR(191) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) NOT NULL, + `subject` VARCHAR(500) NULL, + `message` LONGTEXT NOT NULL, + `isRead` BOOLEAN NOT NULL DEFAULT false, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `ContactMessage_isRead_idx`(`isRead`), + INDEX `ContactMessage_createdAt_idx`(`createdAt` DESC), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `PostTag` ADD CONSTRAINT `PostTag_postId_fkey` FOREIGN KEY (`postId`) REFERENCES `Post`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `PostTag` ADD CONSTRAINT `PostTag_tagId_fkey` FOREIGN KEY (`tagId`) REFERENCES `Tag`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..8b13f572 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,11 @@ +# * +User-agent: * +Allow: / + +# * +User-agent: * +Disallow: /studio +Disallow: /api + +# Host +Host: https://biztaghavi.com diff --git a/scripts/bundle-for-iran.sh b/scripts/bundle-for-iran.sh new file mode 100755 index 00000000..d0dba5c6 --- /dev/null +++ b/scripts/bundle-for-iran.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Run on your Mac (unrestricted internet). Produces iran-bundle.tar.gz for the Iran server. +# The server never talks to npmjs / Prisma CDN during docker build. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +BUNDLE_DIR="$ROOT/iran-bundle" +ARCHIVE="$ROOT/iran-bundle.tar.gz" + +if ! command -v docker >/dev/null 2>&1; then + echo "ERROR: Docker is required." + exit 1 +fi + +if [[ ! -f prisma-binaries/libquery_engine-debian-openssl-3.0.x.so.node ]]; then + echo "ERROR: prisma-binaries/ missing. Run: bash scripts/fetch-prisma-binaries.sh" + exit 1 +fi + +echo "==> Building linux/amd64 app inside Docker (this uses your Mac's internet)..." +rm -rf "$BUNDLE_DIR" "$ARCHIVE" + +docker run --rm --platform linux/amd64 \ + -v "$ROOT:/app" -w /app \ + -e PRISMA_QUERY_ENGINE_LIBRARY=/app/prisma-binaries/libquery_engine-debian-openssl-3.0.x.so.node \ + -e PRISMA_SCHEMA_ENGINE_BINARY=/app/prisma-binaries/schema-engine-debian-openssl-3.0.x \ + -e PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 \ + -e PRISMA_SKIP_POSTINSTALL_GENERATE=1 \ + -e NEXT_TELEMETRY_DISABLED=1 \ + -e NEXT_SWC_DOWNLOAD_DISABLED=1 \ + -e DATABASE_URL="mysql://build:build@127.0.0.1:3306/build" \ + node:20-slim bash -c ' + set -euo pipefail + apt-get update -qq + apt-get install -y -qq openssl ca-certificates >/dev/null + corepack enable + corepack prepare pnpm@9 --activate + rm -rf node_modules .next iran-bundle + pnpm install --frozen-lockfile + pnpm exec prisma generate + pnpm run build + mkdir -p iran-bundle/standalone iran-bundle/node_modules + cp -a .next/standalone/. iran-bundle/standalone/ + cp -a .next/static iran-bundle/static + cp -a public iran-bundle/public + cp -a prisma-binaries iran-bundle/prisma-binaries + cp -a node_modules/.prisma iran-bundle/node_modules/.prisma + cp -a node_modules/@prisma iran-bundle/node_modules/@prisma + ' + +echo "==> Packing archive..." +tar -czf "$ARCHIVE" -C "$ROOT" iran-bundle +SIZE="$(du -h "$ARCHIVE" | awk '{print $1}')" +echo "" +echo "Created: $ARCHIVE ($SIZE)" +echo "" +echo "Upload to server:" +echo " scp \"$ARCHIVE\" root@193.105.234.35:/srv/nodecloud/apps/biztaghavisite/" +echo "" +echo "On server:" +echo " cd /srv/nodecloud/apps/biztaghavisite" +echo " git pull origin main" +echo " bash scripts/server-deploy.sh" diff --git a/scripts/server-deploy.sh b/scripts/server-deploy.sh index 43f59962..b25f9e92 100755 --- a/scripts/server-deploy.sh +++ b/scripts/server-deploy.sh @@ -3,6 +3,8 @@ set -euo pipefail APP_DIR="/srv/nodecloud/apps/biztaghavisite" PORT=3009 +COMPOSE_FILE="docker-compose.yml" +DOCKERFILE_NOTE="online build (Liara npm mirror)" cd "$APP_DIR" @@ -17,39 +19,62 @@ if [[ ! -f .env ]]; then exit 1 fi -echo "==> Build image (10–20 min on first run; Liara mirror may be slow)" -docker compose build --no-cache +# Offline bundle from Mac (recommended for Iran) +if [[ -f iran-bundle.tar.gz ]]; then + COMPOSE_FILE="docker-compose.prebuilt.yml" + DOCKERFILE_NOTE="prebuilt bundle (no npm on server)" + echo "==> Extract iran-bundle.tar.gz" + rm -rf iran-bundle + mkdir -p iran-bundle + tar -xzf iran-bundle.tar.gz -C . + if [[ ! -f iran-bundle/standalone/server.js ]]; then + echo "ERROR: iran-bundle/standalone/server.js missing — re-run bundle-for-iran.sh on Mac" + exit 1 + fi +else + echo "WARN: iran-bundle.tar.gz not found — building on server (needs Liara mirror)." + echo " Recommended: run scripts/bundle-for-iran.sh on Mac and scp iran-bundle.tar.gz here." +fi + +echo "==> Build image ($DOCKERFILE_NOTE)" +docker compose -f "$COMPOSE_FILE" build --no-cache echo "==> Start MariaDB + app" -docker compose up -d +docker compose -f "$COMPOSE_FILE" up -d echo "==> Wait for MariaDB" -for i in $(seq 1 30); do - if docker compose exec -T db healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; then +for _ in $(seq 1 30); do + if docker compose -f "$COMPOSE_FILE" exec -T db healthcheck.sh --connect --innodb_initialized >/dev/null 2>&1; then echo " MariaDB healthy" break fi sleep 2 done -echo "==> Apply database schema (idempotent)" -set -a -# shellcheck disable=SC1091 -source .env -set +a -docker compose exec -T db mariadb -u "${DB_USER:-biztaghavi}" -p"${DB_PASSWORD}" "${DB_NAME:-biztaghavi}" \ - < prisma/migrations/0_init/migration.sql || true +if [[ -f prisma/migrations/0_init/migration.sql ]] && [[ -s prisma/migrations/0_init/migration.sql ]]; then + echo "==> Apply database schema" + set -a + # shellcheck disable=SC1091 + source .env + set +a + docker compose -f "$COMPOSE_FILE" exec -T db mariadb \ + -u "${DB_USER:-biztaghavi}" -p"${DB_PASSWORD}" "${DB_NAME:-biztaghavi}" \ + < prisma/migrations/0_init/migration.sql || true +else + echo "WARN: prisma/migrations/0_init/migration.sql missing or empty — skip SQL apply" +fi echo "==> Health check" sleep 3 -curl -sf "http://127.0.0.1:${PORT}/" -o /dev/null && echo "OK: app responds on :${PORT}" || { +if curl -sf "http://127.0.0.1:${PORT}/" -o /dev/null; then + echo "OK: app responds on :${PORT}" +else echo "FAIL: app not responding" - docker compose logs --tail=80 next-app + docker compose -f "$COMPOSE_FILE" logs --tail=80 next-app exit 1 -} +fi curl -sI -H "Host: biztaghavi.com" "http://127.0.0.1/" | head -5 -docker compose ps +docker compose -f "$COMPOSE_FILE" ps echo "" -echo "Done. Test https://biztaghavi.com — purge ArvanCloud cache if needed." -echo "Create admin once: curl -X POST https://biztaghavi.com/api/admin/setup ..." +echo "Done. Test https://biztaghavi.com (purge ArvanCloud cache if needed)."