chore(cloudflare): update Dockerfile and .gitignore for Cloudflare deployment

Entire-Checkpoint: 3684d8febba1
This commit is contained in:
Fatih Kadir Akın
2026-03-25 10:29:02 +03:00
parent f724bb7951
commit 9498aa82b4
9 changed files with 1256 additions and 118 deletions
+5
View File
@@ -27,6 +27,11 @@ dist
# Docker
docker-compose*.yml
Dockerfile*
docker/
# Cloudflare
cloudflare/
wrangler.jsonc
# Development files
.github
+1
View File
@@ -48,3 +48,4 @@ next-env.d.ts
# Sentry Config File
.env.sentry-build-plugin
.env.production.cloudflare
+68
View File
@@ -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"]
+102
View File
@@ -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.
+73
View File
@@ -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
View File
@@ -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,
},
},
+944 -109
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -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"
+59
View File
@@ -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"]
}
]
}