نشر تطبيق Next.js على AWS باستخدام SST Ion: دليل شامل للحوسبة السحابية بدون خوادم

المقدمة
نشر تطبيق Next.js على AWS كان تقليدياً يعني ربط CloudFront وLambda@Edge وS3 وكومة من ملفات YAML في Terraform أو CloudFormation. يعمل الأمر — لكنه بطيء وهش ومؤلم عند التعديل.
SST Ion (الإصدار 3) يغير قواعد اللعبة. هو إطار عمل مفتوح المصدر يتيح لك تعريف بنيتك التحتية بالكامل على AWS بلغة TypeScript، بجانب كود التطبيق مباشرة. ملف واحد، لغة واحدة، بدون YAML. ينشر تطبيق Next.js الخاص بك على CloudFront + S3 + Lambda باستخدام OpenNext تحت الغطاء، مما يمنحك إعداداً جاهزاً للإنتاج بدون خوادم بأمر واحد.
في هذا الدليل ستتعلم كيفية:
- تهيئة SST في مشروع Next.js
- النشر على AWS مع CloudFront وS3 وLambda
- إضافة رفع ملفات عبر S3 باستخدام روابط موقعة مسبقاً
- دمج DynamoDB لتخزين البيانات بدون خوادم
- استخدام التطوير المباشر على Lambda للحصول على ملاحظات فورية
- إعداد النطاقات المخصصة ومراحل الإنتاج
- إدارة بيئات متعددة (تطوير، اختبار، إنتاج)
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت
- AWS CLI مُعدّ مع بيانات الاعتماد (
aws configure) - حساب AWS بصلاحيات المسؤول (أو على الأقل صلاحيات IAM وS3 وCloudFront وLambda وDynamoDB)
- معرفة أساسية بـ Next.js App Router وTypeScript
- محرر أكواد (يُنصح بـ VS Code)
ينشر SST موارد حقيقية على AWS قد تترتب عليها تكاليف. الموارد في هذا الدليل تبقى ضمن الطبقة المجانية لـ AWS في معظم الحالات، لكن راقب دائماً لوحة الفواتير.
ما الذي ستبنيه
بنهاية هذا الدليل، سيكون لديك:
- تطبيق Next.js منشور على AWS عبر CloudFront (شبكة توصيل محتوى عالمية)
- رفع ملفات مدعوم بـ S3 مع روابط موقعة مسبقاً
- جدول DynamoDB لتخزين البيانات
- بيئة تطوير Lambda مباشرة مع إعادة تحميل فورية
- بيئات اختبار وإنتاج منفصلة
- نطاق مخصص مع شهادة SSL تلقائية
الخطوة 1: إنشاء مشروع Next.js
ابدأ بإنشاء تطبيق Next.js جديد:
npx create-next-app@latest sst-nextjs-app
cd sst-nextjs-appاختر الخيارات التالية عند السؤال:
- TypeScript: نعم
- ESLint: نعم
- Tailwind CSS: نعم
- مجلد
src/: نعم - App Router: نعم
- اختصار الاستيراد: @/*
الخطوة 2: تهيئة SST
ثبّت SST في المشروع:
npx sst@latest initعند السؤال، اختر aws كمزود. يُنشئ هذا ملف sst.config.ts في جذر المشروع:
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "sst-nextjs-app",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
new sst.aws.Nextjs("MyWeb");
},
});لنفصّل ما يحدث هنا:
app()يُعدّ اسم التطبيق وسياسة الحذف والمزود الرئيسيremoval: "retain"يحتفظ بالموارد عند حذف مرحلة الإنتاج (شبكة أمان)protectيمنع الحذف العرضي لموارد الإنتاجasync run()يُعرّف البنية التحتية — حالياً موقع Next.js فقطsst.aws.Nextjsينشر تطبيقك باستخدام OpenNext (CloudFront + S3 + Lambda)
ينشئ SST أيضاً مجلد .sst/ للأنواع والحالة. أضفه إلى .gitignore:
echo ".sst" >> .gitignoreالخطوة 3: النشر على AWS (النشر الأول)
انشر التطبيق على مرحلة التطوير:
npx sst deploy --stage devيستغرق النشر الأول من 3 إلى 5 دقائق لتوفير CloudFront وS3 وLambda. عمليات النشر اللاحقة أسرع بكثير. عند الانتهاء، يعرض SST رابط CloudFront:
✓ Complete
MyWeb: https://d1234abcd.cloudfront.net
افتح الرابط — تطبيقك يعمل الآن على AWS.
كل مرحلة تحصل على مجموعة معزولة خاصة من موارد AWS. يمكنك إنشاء أي عدد من المراحل: dev، staging، production، pr-123، إلخ.
الخطوة 4: إضافة رفع الملفات عبر S3
لنضف حاوية S3 لرفع الملفات ونربطها بتطبيق Next.js.
4.1 تحديث إعدادات SST
عدّل sst.config.ts لإضافة حاوية S3 عامة:
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "sst-nextjs-app",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
const bucket = new sst.aws.Bucket("Uploads", {
access: "public",
});
new sst.aws.Nextjs("MyWeb", {
link: [bucket],
});
return {
bucket: bucket.name,
};
},
});خاصية link هي سحر SST. تمنح وظائف خادم Next.js صلاحيات IAM للحاوية وتحقن اسم الحاوية كمتغير بيئة — بدون سياسات IAM يدوية أو ملفات .env.
4.2 تثبيت AWS SDK وSST SDK
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner sst4.3 إنشاء مسار API للرفع
أنشئ Server Action يولّد رابطاً موقعاً مسبقاً للرفع المباشر إلى S3:
// src/app/api/upload/route.ts
import { Resource } from "sst";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NextResponse } from "next/server";
const s3 = new S3Client({});
export async function POST(request: Request) {
const { filename, contentType } = await request.json();
const key = `uploads/${Date.now()}-${filename}`;
const command = new PutObjectCommand({
Bucket: Resource.Uploads.name,
Key: key,
ContentType: contentType,
});
const presignedUrl = await getSignedUrl(s3, command, {
expiresIn: 3600,
});
return NextResponse.json({
presignedUrl,
key,
publicUrl: `https://${Resource.Uploads.name}.s3.amazonaws.com/${key}`,
});
}لاحظ Resource.Uploads.name — يحقن SST اسم الحاوية المرتبطة تلقائياً. بدون قيم ثابتة، بدون متغيرات بيئة للإدارة.
4.4 إنشاء واجهة الرفع
// src/app/upload/page.tsx
"use client";
import { useState } from "react";
export default function UploadPage() {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
async function handleUpload() {
if (!file) return;
setUploading(true);
try {
const res = await fetch("/api/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
const { presignedUrl, publicUrl } = await res.json();
await fetch(presignedUrl, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
setUploadedUrl(publicUrl);
} catch (error) {
console.error("فشل الرفع:", error);
} finally {
setUploading(false);
}
}
return (
<div className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">رفع الملفات</h1>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="mb-4 block w-full"
/>
<button
onClick={handleUpload}
disabled={!file || uploading}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{uploading ? "جارٍ الرفع..." : "رفع إلى S3"}
</button>
{uploadedUrl && (
<div className="mt-4 p-4 bg-green-50 rounded">
<p className="text-sm text-green-800">تم الرفع بنجاح!</p>
<a
href={uploadedUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline text-sm break-all"
>
{uploadedUrl}
</a>
</div>
)}
</div>
);
}الخطوة 5: إضافة DynamoDB لتخزين البيانات
5.1 تعريف الجدول في إعدادات SST
حدّث sst.config.ts لإضافة جدول DynamoDB:
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "sst-nextjs-app",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
const bucket = new sst.aws.Bucket("Uploads", {
access: "public",
});
const table = new sst.aws.Dynamo("Notes", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
});
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
});
return {
bucket: bucket.name,
table: table.name,
};
},
});5.2 تثبيت عميل DynamoDB
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb5.3 إنشاء API الملاحظات
// src/app/api/notes/route.ts
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
PutCommand,
QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import { NextResponse } from "next/server";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("userId") || "anonymous";
const result = await client.send(
new QueryCommand({
TableName: Resource.Notes.name,
KeyConditionExpression: "userId = :userId",
ExpressionAttributeValues: { ":userId": userId },
})
);
return NextResponse.json({ notes: result.Items || [] });
}
export async function POST(request: Request) {
const { userId = "anonymous", title, content } = await request.json();
const note = {
userId,
noteId: `note_${Date.now()}`,
title,
content,
createdAt: new Date().toISOString(),
};
await client.send(
new PutCommand({
TableName: Resource.Notes.name,
Item: note,
})
);
return NextResponse.json({ note });
}5.4 إنشاء صفحة الملاحظات
// src/app/notes/page.tsx
"use client";
import { useEffect, useState } from "react";
interface Note {
noteId: string;
title: string;
content: string;
createdAt: string;
}
export default function NotesPage() {
const [notes, setNotes] = useState<Note[]>([]);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
async function loadNotes() {
const res = await fetch("/api/notes?userId=anonymous");
const data = await res.json();
setNotes(data.notes);
}
async function createNote() {
if (!title || !content) return;
await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content }),
});
setTitle("");
setContent("");
loadNotes();
}
useEffect(() => {
loadNotes();
}, []);
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-2xl font-bold mb-6">ملاحظات بدون خوادم</h1>
<div className="mb-8 space-y-3">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="عنوان الملاحظة"
className="w-full border rounded px-3 py-2"
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="محتوى الملاحظة"
className="w-full border rounded px-3 py-2 h-24"
/>
<button
onClick={createNote}
className="bg-blue-600 text-white px-4 py-2 rounded"
>
إضافة ملاحظة
</button>
</div>
<div className="space-y-4">
{notes.map((note) => (
<div key={note.noteId} className="border rounded p-4">
<h2 className="font-semibold">{note.title}</h2>
<p className="text-gray-600 mt-1">{note.content}</p>
<p className="text-xs text-gray-400 mt-2">{note.createdAt}</p>
</div>
))}
</div>
</div>
);
}الخطوة 6: التطوير المباشر على Lambda
هنا يتألق SST حقاً. بدلاً من إعادة النشر في كل مرة تُغيّر كود الخادم، يوجّه sst dev استدعاءات Lambda إلى جهازك المحلي في الوقت الفعلي.
ابدأ بيئة التطوير:
npx sst devيقوم SST بثلاثة أشياء في وقت واحد:
- نشر البنية التحتية على AWS (S3، DynamoDB، CloudFront)
- توجيه طلبات Lambda إلى جهازك المحلي (إعادة تحميل فورية)
- تشغيل خادم تطوير Next.js على
localhost:3000
غيّر مسار API، احفظ الملف، وأعد التحميل — التغيير فوري. بدون إعادة نشر، بدون انتظار. هذا ممكن لأن SST يستبدل دالة Lambda بوكيل يُعيد توجيه الاستدعاءات إلى جهازك عبر اتصال WebSocket.
يتطلب التطوير المباشر على Lambda أن تكون بيانات AWS الخاصة بك مُعدّة. ينشئ SST اتصال WebSocket خفيف عبر IoT للتوجيه — كودك يعمل محلياً لكنه يصل إلى موارد AWS الحقيقية.
الخطوة 7: إعداد النطاق المخصص
7.1 باستخدام Route 53
إذا كان نطاقك مُداراً عبر Route 53، أضف خيار domain لمكون Nextjs:
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
domain: {
name: "app.yourdomain.com",
dns: sst.aws.dns(),
},
});ينشئ SST تلقائياً سجلات Route 53 ويوفر شهادة SSL عبر ACM.
7.2 باستخدام Cloudflare DNS
للنطاقات المُدارة عبر Cloudflare:
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
domain: {
name: "app.yourdomain.com",
dns: sst.cloudflare.dns(),
},
});7.3 باستخدام DNS خارجي
لمزودين آخرين، يعرض SST سجلات DNS المطلوبة وينتظرك لإعدادها:
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
domain: {
name: "app.yourdomain.com",
dns: false,
},
});الخطوة 8: الإعدادات حسب البيئة
تتيح مراحل SST إنشاء بيئات معزولة. استخدم متغير $app.stage لإعداد الموارد بشكل مختلف لكل مرحلة:
async run() {
const isProd = $app.stage === "production";
const bucket = new sst.aws.Bucket("Uploads", {
access: "public",
});
const table = new sst.aws.Dynamo("Notes", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
pointInTimeRecovery: isProd,
});
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
domain: isProd
? { name: "app.yourdomain.com", dns: sst.aws.dns() }
: undefined,
server: {
memory: isProd ? "1024 MB" : "512 MB",
},
});
}النشر على مراحل مختلفة:
# التطوير
npx sst deploy --stage dev
# الاختبار
npx sst deploy --stage staging
# الإنتاج
npx sst deploy --stage productionكل مرحلة تُنشئ موارد معزولة تماماً — حاويات S3 منفصلة، جداول DynamoDB، توزيعات CloudFront، ودوال Lambda.
الخطوة 9: إضافة مهمة مجدولة
يُسهّل SST إضافة المهام المجدولة. لنضف مهمة يومية لتنظيف الملفات القديمة:
// أضف إلى sst.config.ts داخل async run()
new sst.aws.Cron("CleanupOldUploads", {
schedule: "rate(1 day)",
job: {
handler: "src/functions/cleanup.handler",
link: [bucket],
},
});أنشئ المعالج:
// src/functions/cleanup.ts
import { Resource } from "sst";
import {
S3Client,
ListObjectsV2Command,
DeleteObjectsCommand,
} from "@aws-sdk/client-s3";
const s3 = new S3Client({});
const DAYS_TO_KEEP = 30;
export async function handler() {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - DAYS_TO_KEEP);
const objects = await s3.send(
new ListObjectsV2Command({
Bucket: Resource.Uploads.name,
Prefix: "uploads/",
})
);
const toDelete = (objects.Contents || [])
.filter((obj) => obj.LastModified && obj.LastModified < cutoff)
.map((obj) => ({ Key: obj.Key! }));
if (toDelete.length > 0) {
await s3.send(
new DeleteObjectsCommand({
Bucket: Resource.Uploads.name,
Delete: { Objects: toDelete },
})
);
console.log(`تم حذف ${toDelete.length} ملفات قديمة`);
}
return { deleted: toDelete.length };
}الخطوة 10: النشر للإنتاج
10.1 إعدادات SST النهائية
هذا هو ملف sst.config.ts الكامل مع جميع الموارد:
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "sst-nextjs-app",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
const isProd = $app.stage === "production";
const bucket = new sst.aws.Bucket("Uploads", {
access: "public",
});
const table = new sst.aws.Dynamo("Notes", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
pointInTimeRecovery: isProd,
});
new sst.aws.Nextjs("MyWeb", {
link: [bucket, table],
domain: isProd
? { name: "app.yourdomain.com", dns: sst.aws.dns() }
: undefined,
server: {
memory: isProd ? "1024 MB" : "512 MB",
architecture: "arm64",
},
});
new sst.aws.Cron("CleanupOldUploads", {
schedule: "rate(1 day)",
job: {
handler: "src/functions/cleanup.handler",
link: [bucket],
},
});
return {
bucket: bucket.name,
table: table.name,
};
},
});10.2 النشر للإنتاج
npx sst deploy --stage production10.3 مراقبة النشر
يعرض SST جميع تفاصيل الموارد بعد النشر. يمكنك أيضاً عرضها في وحدة تحكم AWS:
- CloudFront — توزيع CDN والنطاق
- S3 — حاوية الرفع والأصول الثابتة
- Lambda — دوال الخادم
- DynamoDB — جدول البيانات
10.4 حذف مرحلة
لهدم مرحلة غير إنتاجية وحذف جميع مواردها:
npx sst remove --stage devمراحل الإنتاج مع protect: true لا يمكن حذفها عرضياً. يجب أولاً تعيين protect إلى false في الإعدادات قبل الحذف.
استكشاف الأخطاء
خطأ "Cannot find module 'sst'" في Next.js
تأكد من تثبيت حزمة sst:
npm install sstالنشر الأول بطيء
يوفر النشر الأولي توزيع CloudFront، مما يستغرق من 3 إلى 5 دقائق. عمليات النشر اللاحقة أسرع بكثير (عادة أقل من 60 ثانية).
أخطاء "Access Denied"
تأكد من أن بيانات AWS لديك تملك صلاحيات كافية. يحتاج SST للوصول إلى IAM وS3 وCloudFront وLambda وDynamoDB وCloudWatch وSSM على الأقل. يُنصح باستخدام دور المسؤول للتطوير.
التطوير المباشر لا يتصل
تأكد من صلاحية بيانات AWS وأن المنفذ 443 الصادر غير محظور بجدار الحماية. يستخدم SST خدمة AWS IoT Core لاتصال WebSocket.
SST مقابل خيارات النشر الأخرى
| الميزة | SST Ion | Vercel | AWS CDK | Terraform |
|---|---|---|---|---|
| اللغة | TypeScript | غير متاح (واجهة) | TypeScript | HCL |
| التطوير المباشر | نعم (أقل من ثانية) | لا | لا | لا |
| ربط الموارد | تلقائي | متغيرات بيئة يدوية | يدوي | يدوي |
| التكلفة | أسعار AWS فقط | لكل مقعد + استخدام | أسعار AWS | أسعار AWS |
| دعم Next.js | كامل (OpenNext) | أصلي | يدوي | يدوي |
| تعدد المزودين | أكثر من 150 مزود | Vercel فقط | AWS فقط | أكثر من 150 |
الخطوات التالية
الآن بعد أن لديك تطبيق Next.js متكامل على AWS، فكّر في:
- إضافة المصادقة مع
sst.aws.Cognitoأو دمج NextAuth.js - إعداد خط أنابيب CI/CD مع GitHub Actions يشغّل
npx sst deploy --stage production - إضافة بوابة API مع
sst.aws.ApiGatewayV2لواجهات برمجة مستقلة - استكشاف وحدة تحكم SST على
console.sst.devللوحة بصرية لمواردك - إضافة طابور مع
sst.aws.Queueللمعالجة في الخلفية
الخلاصة
يحوّل SST Ion نشر AWS من صداع DevOps إلى تجربة صديقة للمطورين. بتعريف البنية التحتية بـ TypeScript بجانب كود التطبيق، تحصل على أمان الأنواع وربط الموارد والتطوير المباشر — كل ذلك أثناء النشر على حساب AWS الخاص بك بدون رسوم إضافية.
النقاط الرئيسية:
- البنية التحتية ككود بـ TypeScript — بدون YAML، بدون ملفات إعدادات منفصلة
- ربط الموارد يلغي سياسات IAM اليدوية ومتغيرات البيئة
- التطوير المباشر على Lambda يوفر ملاحظات فورية بدون إعادة نشر
- المراحل تمنحك بيئات معزولة مجاناً
- OpenNext ينشر Next.js على AWS بدعم كامل للميزات
يتيح لك SST امتلاك بنيتك التحتية بدون تعقيدات AWS التقليدية. ابدأ بـ npx sst@latest init وانشر تطبيقك الأول في دقائق.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء وظائف متينة وسير عمل مبني على الأحداث مع Inngest و Next.js
تعلم كيفية بناء سير عمل موثوق ومبني على الأحداث باستخدام Inngest و Next.js. يغطي هذا الدليل الوظائف المتينة والخطوات المتسلسلة وأنماط التوزيع وإعادة المحاولة والجدولة والنشر في بيئة الإنتاج.

Neon Serverless Postgres مع Next.js App Router: بناء تطبيق كامل مع تفريع قواعد البيانات
تعلّم كيفية بناء تطبيق Next.js كامل مدعوم بقاعدة بيانات Neon Serverless Postgres. يغطي هذا الدليل التطبيقي برنامج التشغيل بدون خادم، تفريع قواعد البيانات لعمليات النشر التجريبية، تجميع الاتصالات، وأنماط الإنتاج الجاهزة.

بناء مهام خلفية للإنتاج باستخدام Trigger.dev v3 و Next.js
تعلم كيفية بناء مهام خلفية موثوقة ومهام مجدولة وسير عمل متعدد الخطوات باستخدام Trigger.dev v3 مع Next.js. يغطي هذا الدرس إنشاء المهام ومعالجة الأخطاء وإعادة المحاولة والمهام المجدولة والنشر في بيئة الإنتاج.