xiaohongshu
xiaohongshu
composable-svelte-deployment
Production deployment patterns for Composable Svelte SSR applications. Use when deploying to Fly.io, Docker, or any cloud platform. Covers multi-stage Docker builds, Fly.io configuration, security hardening, performance optimization, and integration with Composable Rust backends.
Full skill instructions
This skill covers production deployment of Composable Svelte SSR applications, with focus on Fly.io and Docker-based deployments.
Stack: Fastify + Composable Svelte SSR (NOT SvelteKit)
┌─────────────────────────────────────────────────┐
│ Fly.io │
├─────────────────────────────────────────────────┤
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Composable │ │ Composable │ │
│ │ Svelte SSR │◄──►│ Rust Backend │ │
│ │ (Fastify) │ │ (Axum/Actix) │ │
│ └──────────────────┘ └──────────────────┘ │
│ Docker Container Docker Container │
│ Internal Network: .internal (6PN) │
└─────────────────────────────────────────────────┘
Why NOT SvelteKit?
@composable-svelte/core/ssrGoal: <150MB final image, production-only dependencies
# Stage 1: Dependencies (Production Only)
FROM node:20-alpine AS deps
WORKDIR /app
RUN npm install -g pnpm@9
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
# Stage 2: Builder (Development Dependencies + Build)
FROM node:20-alpine AS builder
WORKDIR /app
RUN npm install -g pnpm@9
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
RUN find dist -name "*.map" -type f -delete # Remove source maps
# Stage 3: Production Runtime (Minimal)
FROM node:20-alpine AS runner
RUN apk add --no-cache dumb-init
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
# Copy ONLY production dependencies and built artifacts
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./package.json
USER nodejs
ENV NODE_ENV=production PORT=3000
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server/index.js"]
Key Patterns:
nodejs:nodejs (UID 1001)find dist -name "*.map" -deleteCommon Mistakes:
Monorepo (like this repo):
# Copy workspace structure
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/core/package.json ./packages/core/
COPY examples/ssr-server/package.json ./examples/ssr-server/
# Install workspace dependencies
RUN pnpm install --frozen-lockfile
# Build both packages
WORKDIR /app/packages/core
RUN pnpm run build
WORKDIR /app/examples/ssr-server
RUN pnpm run build
# Copy ALL workspace artifacts
COPY --from=builder /app/packages/core/dist ./packages/core/dist
COPY --from=builder /app/packages/core/package.json ./packages/core/package.json
COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
Standalone (user's app):
# Simple install
COPY package.json package-lock.json ./
RUN npm ci
# Simple build
COPY . .
RUN npm run build
# Simple copy
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
Required: Vite must be configured for dual builds (client + server)
// vite.config.ts
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
build: {
// Client build (default)
outDir: 'dist/client',
rollupOptions: {
input: 'src/client/index.ts'
}
},
ssr: {
// Don't externalize workspace packages
noExternal: ['@composable-svelte/core']
}
});
package.json scripts:
{
"scripts": {
"build": "vite build && vite build --ssr",
"build:client": "vite build",
"build:server": "vite build --ssr src/server/index.ts --outDir dist/server",
"start": "NODE_ENV=production node dist/server/index.js"
}
}
Why: SSR requires TWO builds:
app = "my-composable-app"
primary_region = "sjc" # Choose region closest to users
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
PORT = "3000"
# Internal Fly network for backend communication
COMPOSABLE_RUST_BACKEND_URL = "http://my-rust-backend.internal:8080"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0 # Scale to zero (change to 1+ for production)
[http_service.concurrency]
type = "connections"
hard_limit = 250
soft_limit = 200
[[vm]]
memory = '512mb' # Minimum for Node.js SSR
cpu_kind = 'shared'
cpus = 1
# Health checks (Apps V2 syntax)
[[http_service.checks]]
interval = "30s"
timeout = "5s"
grace_period = "10s"
method = "GET"
path = "/health"
Critical Patterns:
[[http_service.checks]] NOT [[services.http_checks]].internal domain for Rust backend (no internet egress)fly secrets set KEY=value (NOT in fly.toml)Common Mistakes:
NEVER commit secrets to fly.toml:
# ✅ CORRECT: Use Fly secrets CLI
fly secrets set \
SESSION_SECRET=$(openssl rand -hex 32) \
JWT_SECRET=$(openssl rand -hex 32) \
BACKEND_API_KEY=your-backend-api-key-here
# Verify secrets are set (values hidden)
fly secrets list
# ❌ WRONG: Environment variables in fly.toml
[env]
SESSION_SECRET = "abc123" # NEVER DO THIS
Validate secrets at startup:
// src/server/index.ts
if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET.length < 32) {
throw new Error('SESSION_SECRET must be at least 32 characters');
}
Use built-in middleware:
import { fastifySecurityHeaders } from '@composable-svelte/core/ssr';
fastifySecurityHeaders(app, {
contentSecurityPolicy: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Remove 'unsafe-inline' if possible
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://your-backend.fly.dev",
"frame-ancestors 'none'",
"base-uri 'self'"
].join('; '),
frameOptions: 'DENY',
referrerPolicy: 'strict-origin-when-cross-origin',
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
});
Test headers:
curl -I https://your-app.fly.dev
# Should see:
# Content-Security-Policy: default-src 'self'; ...
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=31536000
Per-IP rate limiting:
import { fastifyRateLimit } from '@composable-svelte/core/ssr';
fastifyRateLimit(app, {
max: 100, // 100 requests
windowMs: 60000, // per minute
message: 'Too many requests from this IP'
});
Per-route rate limiting:
app.get('/api/expensive', {
config: {
rateLimit: {
max: 10, // 10 requests
timeWindow: 60000 // per minute
}
}
}, async (request, reply) => {
// Expensive operation
});
Strict CORS for Rust backend:
import cors from '@fastify/cors';
app.register(cors, {
origin: [
'https://your-app.fly.dev',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE']
});
Non-root user (REQUIRED):
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
Read-only filesystem (optional):
# fly.toml
[[vm]]
read_only = true
[[mounts]]
source = "logs"
destination = "/app/logs" # Only writable directory
In-memory cache (single instance):
const cache = new Map<string, { html: string; timestamp: number }>();
const CACHE_TTL = 60000; // 1 minute
app.get('*', async (request, reply) => {
const cacheKey = request.url;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return reply.type('text/html').send(cached.html);
}
const html = await renderToHTML(/* ... */);
cache.set(cacheKey, { html, timestamp: Date.now() });
return reply.type('text/html').send(html);
});
Redis cache (multi-instance):
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedHTML(key: string) {
return await redis.get(key);
}
async function setCachedHTML(key: string, html: string, ttl: number) {
await redis.setex(key, ttl, html);
}
Enable Brotli compression:
import compress from '@fastify/compress';
app.register(compress, {
global: true,
threshold: 1024, // Min size to compress (1KB)
encodings: ['gzip', 'deflate', 'br'] // Brotli for best compression
});
Code splitting (automatic with Vite):
// Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart.svelte'));
{#if showChart}
<Suspense fallback={<Spinner />}>
<HeavyChart data={chartData} />
</Suspense>
{/if}
Tree shaking (use named imports):
// ✅ GOOD: Named imports
import { createStore, Effect } from '@composable-svelte/core';
// ❌ BAD: Wildcard imports
import * as Core from '@composable-svelte/core';
# 1. Build Docker image
docker build -t my-composable-app .
# 2. Check image size (should be <150MB)
docker images my-composable-app
# 3. Run locally
docker run -p 3000:3000 \
-e COMPOSABLE_RUST_BACKEND_URL=http://localhost:8080 \
my-composable-app
# 4. Test health check
curl http://localhost:3000/health
# 1. Login
fly auth login
# 2. Create app (first time)
fly launch
# 3. Set secrets
fly secrets set \
SESSION_SECRET=$(openssl rand -hex 32) \
JWT_SECRET=$(openssl rand -hex 32)
# 4. Deploy
fly deploy
# 5. Check status
fly status
# 6. Open in browser
fly open
Horizontal scaling:
# Scale to 2 instances
fly scale count 2
# Scale to multiple regions
fly scale count 2 --region sjc # San Jose
fly scale count 1 --region lhr # London
Vertical scaling:
# Increase memory
fly scale memory 1024
# Increase CPU
fly scale vm shared-cpu-2x
Autoscaling (paid):
[auto_scaling]
min_instances = 1
max_instances = 10
[[auto_scaling.metrics]]
type = "requests"
target = 500 # Scale when avg requests > 500/sec
# List releases
fly releases
# Rollback to previous release
fly releases rollback
# Rollback to specific version
fly releases rollback v3
Key Pattern: Use .internal domain for Composable Rust backend
// fly.toml
[env]
COMPOSABLE_RUST_BACKEND_URL = "http://my-rust-backend.internal:8080"
// Fastify server
export const backendAPI = createAPIClient({
baseURL: process.env.COMPOSABLE_RUST_BACKEND_URL,
timeout: 30000
});
Benefits:
CORS on Rust backend:
let cors = CorsLayer::new()
.allow_origin("https://my-composable-app.fly.dev".parse::<HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST])
.allow_headers([AUTHORIZATION, CONTENT_TYPE]);
Required for Fly.io:
app.get('/health', async () => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
};
});
# Stream logs
fly logs
# Filter errors only
fly logs | grep ERROR
# Tail last 100 lines
fly logs --tail 100
# Open console in running instance
fly ssh console
# Check processes
ps aux
# Check memory
free -m
# Exit
exit
# Check logs
fly logs
# Common issues:
# - Missing environment variables
# - Port mismatch (must use PORT env var)
# - Build failed (check Dockerfile)
# Test locally first
docker run -p 3000:3000 my-composable-app
curl http://localhost:3000/health
# Check health check path in fly.toml matches your endpoint
# Verify internal network
fly ssh console
wget http://my-rust-backend.internal:8080/health
# If fails, check:
# - Backend is running
# - Backend app name is correct (.internal must match)
# - Firewall rules (Fly apps can communicate by default)
# Increase memory
fly scale memory 1024
# Or optimize Node.js
NODE_OPTIONS="--max-old-space-size=512"
fly secrets (not env vars)pnpm audit)curl -I)| Metric | Target | Excellent |
|---|---|---|
| Docker image size | <150MB | <100MB |
| Initial JS bundle | <100KB (gzipped) | <70KB |
| Time to First Byte (TTFB) | <200ms | <100ms |
| First Contentful Paint (FCP) | <1.8s | <1.0s |
| Largest Contentful Paint (LCP) | <2.5s | <1.5s |
| Time to Interactive (TTI) | <3.8s | <2.5s |
| Cumulative Layout Shift (CLS) | <0.1 | <0.05 |
plans/production-deployment/packages/core/src/lib/ssr/plans/production-deployment/SECURITY.mdplans/production-deployment/OPTIMIZATION.mdxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience