Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9498aa82b4 |
@@ -27,6 +27,11 @@ dist
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
docker/
|
||||
|
||||
# Cloudflare
|
||||
cloudflare/
|
||||
wrangler.jsonc
|
||||
|
||||
# Development files
|
||||
.github
|
||||
|
||||
@@ -48,3 +48,4 @@ next-env.d.ts
|
||||
|
||||
# Sentry Config File
|
||||
.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",
|
||||
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",
|
||||
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
|
||||
|
||||
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
|
||||
treeshake: {
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
removeDebugLogging: true,
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+944
-109
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -32,6 +32,7 @@
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
"@aws-sdk/client-s3": "^3.948.0",
|
||||
"@cloudflare/containers": "^0.1.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
@@ -111,7 +112,8 @@
|
||||
"tsx": "^4.21.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^2.1.8",
|
||||
"wrangler": "^4.76.0"
|
||||
},
|
||||
"engines": {
|
||||
"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