Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9498aa82b4 |
@@ -27,6 +27,11 @@ dist
|
|||||||
# Docker
|
# Docker
|
||||||
docker-compose*.yml
|
docker-compose*.yml
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
|
docker/
|
||||||
|
|
||||||
|
# Cloudflare
|
||||||
|
cloudflare/
|
||||||
|
wrangler.jsonc
|
||||||
|
|
||||||
# Development files
|
# Development files
|
||||||
.github
|
.github
|
||||||
|
|||||||
@@ -48,3 +48,4 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.env.sentry-build-plugin
|
.env.sentry-build-plugin
|
||||||
|
.env.production.cloudflare
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Production Dockerfile for Cloudflare Containers
|
||||||
|
# Multi-stage build producing a lean standalone Next.js image
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 1: Install dependencies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM node:24-alpine AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat openssl
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY prisma ./prisma/
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 2: Build the application
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM node:24-alpine AS builder
|
||||||
|
RUN apk add --no-cache libc6-compat openssl
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
|
# Dummy URL for prisma generate (only needs schema, not a real connection)
|
||||||
|
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
|
||||||
|
# Skip Sentry source map upload during container build
|
||||||
|
ENV SENTRY_AUTH_TOKEN=""
|
||||||
|
|
||||||
|
RUN npx prisma generate
|
||||||
|
# Use webpack instead of Turbopack to stay within Docker memory limits
|
||||||
|
RUN npx next build --webpack
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 3: Production runner
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
RUN apk add --no-cache libc6-compat openssl
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
|
||||||
|
# YAML prompt files read at runtime via readFileSync(process.cwd() + path)
|
||||||
|
COPY --from=builder /app/src/lib/ai/*.prompt.yml ./src/lib/ai/
|
||||||
|
COPY --from=builder /app/src/app/api/prompt-builder/chat/*.prompt.yml ./src/app/api/prompt-builder/chat/
|
||||||
|
|
||||||
|
# Next.js standalone output includes a minimal server and all dependencies
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Deploying prompts.chat to Cloudflare Containers
|
||||||
|
|
||||||
|
This guide covers deploying the Next.js application to [Cloudflare Containers](https://developers.cloudflare.com/containers/).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [Docker](https://docs.docker.com/desktop/) running locally
|
||||||
|
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) (`npm i -g wrangler`)
|
||||||
|
- A Cloudflare account on the [Workers Paid plan](https://workers.cloudflare.com/pricing/)
|
||||||
|
- An external PostgreSQL database (e.g. Neon, Supabase, or self-hosted)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @cloudflare/containers
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure secrets
|
||||||
|
|
||||||
|
Set required environment variables as Wrangler secrets. These are injected into the container at runtime:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
wrangler secret put DATABASE_URL
|
||||||
|
wrangler secret put AUTH_SECRET
|
||||||
|
|
||||||
|
# OAuth (if using these providers)
|
||||||
|
wrangler secret put AUTH_GITHUB_ID
|
||||||
|
wrangler secret put AUTH_GITHUB_SECRET
|
||||||
|
wrangler secret put AUTH_GOOGLE_ID
|
||||||
|
wrangler secret put AUTH_GOOGLE_SECRET
|
||||||
|
|
||||||
|
# Optional
|
||||||
|
wrangler secret put OPENAI_API_KEY
|
||||||
|
wrangler secret put SENTRY_DSN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
On the first deploy, Wrangler will:
|
||||||
|
1. Build the Docker image locally using `Dockerfile.cloudflare`
|
||||||
|
2. Push the image to Cloudflare's managed registry
|
||||||
|
3. Deploy the Worker that routes traffic to the container
|
||||||
|
|
||||||
|
The first deploy takes a few minutes for image push and container provisioning. Subsequent deploys are faster due to layer caching.
|
||||||
|
|
||||||
|
### 4. Check status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler containers list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
User Request → Cloudflare Network → Worker (router) → Container (Next.js on Node.js)
|
||||||
|
↓
|
||||||
|
PostgreSQL DB
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Worker** (`cloudflare/worker.js`): Thin routing layer that forwards all requests to the container
|
||||||
|
- **Container** (`Dockerfile.cloudflare`): Multi-stage Node.js image running the Next.js standalone server
|
||||||
|
- The container runs the full Node.js runtime, so all dependencies (Prisma, next-auth, next-intl, sharp) work without modification
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings in `wrangler.jsonc`:
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `max_instances` | 3 | Maximum concurrent container instances |
|
||||||
|
| `instance_type` | standard-1 | 2 vCPU, 2 GiB memory |
|
||||||
|
| `sleepAfter` | 5m | Container sleeps after 5 minutes of inactivity |
|
||||||
|
|
||||||
|
Adjust `max_instances` and `instance_type` based on your traffic. See [instance types](https://developers.cloudflare.com/containers/platform-details/#instance-types) for all options.
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
Local development remains unchanged:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
To test the Docker image locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.cloudflare -t prompts-chat .
|
||||||
|
docker run -p 3000:3000 --env-file .env prompts-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Cold starts**: Containers take 2-3 seconds to wake from sleep. The `sleepAfter` setting in `cloudflare/worker.js` controls when idle containers go to sleep.
|
||||||
|
- **Database**: Use an external PostgreSQL provider. Cloudflare Hyperdrive can be added for edge connection pooling if needed.
|
||||||
|
- **Containers Beta**: Cloudflare Containers is currently in beta. See [beta info](https://developers.cloudflare.com/containers/beta-info/) for limitations.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Container } from "@cloudflare/containers";
|
||||||
|
import { env } from "cloudflare:workers";
|
||||||
|
|
||||||
|
const SECRET_KEYS = [
|
||||||
|
"NEXTAUTH_SECRET",
|
||||||
|
"GITHUB_CLIENT_ID",
|
||||||
|
"GITHUB_CLIENT_SECRET",
|
||||||
|
"GOOGLE_CLIENT_ID",
|
||||||
|
"GOOGLE_CLIENT_SECRET",
|
||||||
|
"AUTH_APPLE_ID",
|
||||||
|
"AUTH_APPLE_SECRET",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
"SENTRY_AUTH_TOKEN",
|
||||||
|
"DO_SPACES_ACCESS_KEY_ID",
|
||||||
|
"DO_SPACES_SECRET_ACCESS_KEY",
|
||||||
|
"HF_API_TOKEN",
|
||||||
|
"FAL_API_KEY",
|
||||||
|
"WIRO_API_KEY",
|
||||||
|
"CRON_SECRET",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSecretEnvVars() {
|
||||||
|
const vars = {};
|
||||||
|
for (const key of SECRET_KEYS) {
|
||||||
|
if (env[key]) vars[key] = env[key];
|
||||||
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PromptsChatContainer extends Container {
|
||||||
|
defaultPort = 3000;
|
||||||
|
sleepAfter = "5m";
|
||||||
|
envVars = {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
DATABASE_URL: env.DATABASE_URL || "",
|
||||||
|
NEXTAUTH_URL: env.NEXTAUTH_URL,
|
||||||
|
OPENAI_BASE_URL: env.OPENAI_BASE_URL,
|
||||||
|
OPENAI_EMBEDDING_MODEL: env.OPENAI_EMBEDDING_MODEL,
|
||||||
|
OPENAI_GENERATIVE_MODEL: env.OPENAI_GENERATIVE_MODEL,
|
||||||
|
GOOGLE_ANALYTICS_ID: env.GOOGLE_ANALYTICS_ID,
|
||||||
|
ENABLED_STORAGE: env.ENABLED_STORAGE,
|
||||||
|
DO_SPACES_BUCKET: env.DO_SPACES_BUCKET,
|
||||||
|
DO_SPACES_REGION: env.DO_SPACES_REGION,
|
||||||
|
FAL_IMAGE_MODELS: env.FAL_IMAGE_MODELS,
|
||||||
|
FAL_VIDEO_MODELS: env.FAL_VIDEO_MODELS,
|
||||||
|
FAL_AUDIO_MODELS: env.FAL_AUDIO_MODELS,
|
||||||
|
WIRO_IMAGE_MODELS: env.WIRO_IMAGE_MODELS,
|
||||||
|
WIRO_VIDEO_MODELS: env.WIRO_VIDEO_MODELS,
|
||||||
|
EZOIC_SITE_DOMAIN: env.EZOIC_SITE_DOMAIN,
|
||||||
|
NEXT_PUBLIC_EZOIC_ENABLED: env.NEXT_PUBLIC_EZOIC_ENABLED,
|
||||||
|
GOOGLE_ADSENSE_ACCOUNT: env.GOOGLE_ADSENSE_ACCOUNT,
|
||||||
|
...getSecretEnvVars(),
|
||||||
|
};
|
||||||
|
|
||||||
|
onStart() {
|
||||||
|
console.log("prompts.chat container started");
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop() {
|
||||||
|
console.log("prompts.chat container stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error) {
|
||||||
|
console.error("prompts.chat container error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env) {
|
||||||
|
const container = env.PROMPTS_CHAT.getByName("main");
|
||||||
|
return container.fetch(request);
|
||||||
|
},
|
||||||
|
};
|
||||||
+1
-8
@@ -55,7 +55,7 @@ const nextConfig: NextConfig = {
|
|||||||
destination: "/embed",
|
destination: "/embed",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
// Redirect book PDF downloads to GitHub raw to save Vercel edge bandwidth
|
// Redirect book PDF downloads to GitHub raw to save bandwidth
|
||||||
{
|
{
|
||||||
source: "/book-pdf/:filename",
|
source: "/book-pdf/:filename",
|
||||||
destination: "https://raw.githubusercontent.com/f/prompts.chat/refs/heads/main/public/book-pdf/:filename",
|
destination: "https://raw.githubusercontent.com/f/prompts.chat/refs/heads/main/public/book-pdf/:filename",
|
||||||
@@ -88,15 +88,8 @@ export default withSentryConfig(withMDX(withNextIntl(nextConfig)), {
|
|||||||
// See: https://github.com/f/prompts.chat/issues/1085
|
// See: https://github.com/f/prompts.chat/issues/1085
|
||||||
|
|
||||||
webpack: {
|
webpack: {
|
||||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
|
||||||
// See the following for more information:
|
|
||||||
// https://docs.sentry.io/product/crons/
|
|
||||||
// https://vercel.com/docs/cron-jobs
|
|
||||||
automaticVercelMonitors: true,
|
|
||||||
|
|
||||||
// Tree-shaking options for reducing bundle size
|
// Tree-shaking options for reducing bundle size
|
||||||
treeshake: {
|
treeshake: {
|
||||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
|
||||||
removeDebugLogging: true,
|
removeDebugLogging: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+944
-109
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -32,6 +32,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
"@aws-sdk/client-s3": "^3.948.0",
|
"@aws-sdk/client-s3": "^3.948.0",
|
||||||
|
"@cloudflare/containers": "^0.1.1",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -111,7 +112,8 @@
|
|||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^2.1.8",
|
||||||
|
"wrangler": "^4.76.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "24.x"
|
"node": "24.x"
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "prompts-chat",
|
||||||
|
"main": "cloudflare/worker.js",
|
||||||
|
"compatibility_date": "2026-03-22",
|
||||||
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
|
|
||||||
|
"hyperdrive": [
|
||||||
|
{
|
||||||
|
"binding": "HYPERDRIVE",
|
||||||
|
"id": "2c6ef99cbde84baaac5edacc418eee72"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"vars": {
|
||||||
|
"NEXTAUTH_URL": "https://prompts.chat",
|
||||||
|
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||||
|
"OPENAI_EMBEDDING_MODEL": "text-embedding-3-large",
|
||||||
|
"OPENAI_GENERATIVE_MODEL": "gpt-4o",
|
||||||
|
"GOOGLE_ANALYTICS_ID": "G-MSNHFWTE77",
|
||||||
|
"ENABLED_STORAGE": "do-spaces",
|
||||||
|
"DO_SPACES_BUCKET": "prompts-chat-space",
|
||||||
|
"DO_SPACES_REGION": "fra1",
|
||||||
|
"FAL_IMAGE_MODELS": "fal-ai/flux-pro/v1.1-ultra,fal-ai/nano-banana-pro",
|
||||||
|
"FAL_VIDEO_MODELS": "fal-ai/veo3.1/fast",
|
||||||
|
"FAL_AUDIO_MODELS": "sonauto/v2/text-to-music,cassetteai/music-generator",
|
||||||
|
"WIRO_IMAGE_MODELS": "google/nano-banana-pro,google/nano-banana",
|
||||||
|
"WIRO_VIDEO_MODELS": "google/veo3.1-fast",
|
||||||
|
"EZOIC_SITE_DOMAIN": "prompts.chat",
|
||||||
|
"NEXT_PUBLIC_EZOIC_ENABLED": "true",
|
||||||
|
"GOOGLE_ADSENSE_ACCOUNT": "ca-pub-6945602608405209"
|
||||||
|
},
|
||||||
|
|
||||||
|
"containers": [
|
||||||
|
{
|
||||||
|
"class_name": "PromptsChatContainer",
|
||||||
|
"image": "./Dockerfile.cloudflare",
|
||||||
|
"max_instances": 3,
|
||||||
|
// standard-1: 2 vCPU, 2 GiB memory
|
||||||
|
"instance_type": "standard-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "PROMPTS_CHAT",
|
||||||
|
"class_name": "PromptsChatContainer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_sqlite_classes": ["PromptsChatContainer"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user