Deploying Fork Recipes
Introduction
Fork Recipes is a modern, self-hosted recipe management application designed to help you organize, discover, and share your favorite recipes. Built with a clean and intuitive interface, Fork Recipes makes it easy to store recipes from various sources, categorize them with tags, plan meals, generate shopping lists, and share culinary creations with family and friends.
Fork Recipes is known for:
- Recipe Organization: Store unlimited recipes with rich text formatting, images, and ingredient lists
- Smart Parsing: Import recipes from URLs with automatic ingredient and instruction extraction
- Tag System: Organize recipes with custom tags, categories, and cuisine types
- Meal Planning: Plan weekly meals and automatically generate shopping lists
- Search & Filter: Powerful search across ingredients, titles, tags, and cooking methods
- Share & Export: Share recipes via links or export to PDF format
- Multi-User Support: Create accounts for family members with personal collections and shared recipes
- Image Management: Upload multiple photos per recipe with automatic optimization
- Nutrition Tracking: Optional nutrition information and dietary preference filters
- Mobile Responsive: Full-featured mobile interface for cooking in the kitchen
- Dark Mode: Eye-friendly dark theme for late-night cooking sessions
- API Access: RESTful API for integrations and automation
Common use cases include personal recipe collections, family cookbook management, meal prep planning, dietary restriction tracking, cooking blog backends, and collaborative recipe sharing communities.
This comprehensive guide walks you through deploying Fork Recipes on Klutch.sh using Docker, including PostgreSQL database configuration, persistent storage for images and data, environment variables, and production-ready best practices.
Why Deploy Fork Recipes on Klutch.sh?
- Complete Privacy: Own your recipe data without third-party services
- Automatic HTTPS: Secure connections for sharing recipes with friends and family
- Simple Database Setup: Easy PostgreSQL integration for reliable data storage
- Persistent Storage: Reliable volume storage for recipe images and attachments
- Environment Variables: Secure configuration management without hardcoding credentials
- Zero Downtime Deployments: Update your recipe app without interrupting access
- Custom Domains: Use your own domain for a branded recipe site
- Cost-Effective: Pay only for what you use with transparent pricing
- Easy Scaling: Scale resources as your recipe collection grows
- Automatic Backups: Protect your culinary knowledge with regular backups
Prerequisites
Before you begin, ensure you have the following:
- A Klutch.sh account
- A GitHub account with a repository for your Fork Recipes project
- Docker installed locally for testing (optional but recommended)
- Basic understanding of Docker and web applications
- (Recommended) A PostgreSQL database - see our PostgreSQL guide
Installation and Setup
Step 1: Create Your Project Directory
First, create a new directory for your Fork Recipes deployment project:
mkdir fork-recipes-klutchcd fork-recipes-klutchgit initStep 2: Create the Dockerfile
Create a Dockerfile in your project root directory. This will define your Fork Recipes container configuration:
FROM node:18-alpine AS builder
# Set working directoryWORKDIR /app
# Copy package filesCOPY package*.json ./
# Install dependenciesRUN npm ci --only=production
# Copy application codeCOPY . .
# Build the applicationRUN npm run build
# Production stageFROM node:18-alpine
WORKDIR /app
# Install production dependencies onlyCOPY package*.json ./RUN npm ci --only=production
# Copy built application from builderCOPY --from=builder /app/dist ./distCOPY --from=builder /app/public ./public
# Create directories for uploads and dataRUN mkdir -p /app/data /app/uploads && \ chown -R node:node /app/data /app/uploads
# Switch to non-root userUSER node
# Expose port 3000EXPOSE 3000
# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start the applicationCMD ["node", "dist/server.js"]Note: Fork Recipes uses a Node.js backend with a modern frontend framework. The multi-stage build optimizes the final image size by separating build dependencies from runtime dependencies.
Step 3: Create package.json
Create a package.json file with the necessary dependencies and scripts:
{ "name": "fork-recipes", "version": "1.0.0", "description": "Self-hosted recipe management application", "main": "dist/server.js", "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc && vite build", "start": "node dist/server.js", "test": "vitest", "lint": "eslint src --ext .ts,.tsx" }, "dependencies": { "express": "^4.18.2", "pg": "^8.11.3", "dotenv": "^16.3.1", "cors": "^2.8.5", "helmet": "^7.1.0", "multer": "^1.4.5-lts.1", "sharp": "^0.33.0", "bcrypt": "^5.1.1", "jsonwebtoken": "^9.0.2", "zod": "^3.22.4", "date-fns": "^2.30.0" }, "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^20.10.4", "typescript": "^5.3.3", "tsx": "^4.7.0", "vite": "^5.0.7", "vitest": "^1.0.4" }, "engines": { "node": ">=18.0.0" }}Step 4: Create Application Structure
Create the basic application structure:
mkdir -p src/routes src/controllers src/models src/middleware public/uploadsCreate a basic server file src/server.ts:
import express from 'express';import cors from 'cors';import helmet from 'helmet';import { config } from 'dotenv';import { Pool } from 'pg';
config();
const app = express();const port = process.env.PORT || 3000;
// Database connectionconst pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,});
// Middlewareapp.use(helmet());app.use(cors());app.use(express.json());app.use(express.urlencoded({ extended: true }));
// Static filesapp.use('/uploads', express.static('public/uploads'));
// Health check endpointapp.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() });});
// API routesapp.get('/api/recipes', async (req, res) => { try { const result = await pool.query('SELECT * FROM recipes ORDER BY created_at DESC LIMIT 20'); res.json(result.rows); } catch (error) { console.error('Database error:', error); res.status(500).json({ error: 'Failed to fetch recipes' }); }});
app.post('/api/recipes', async (req, res) => { const { title, description, ingredients, instructions, tags } = req.body;
try { const result = await pool.query( 'INSERT INTO recipes (title, description, ingredients, instructions, tags) VALUES ($1, $2, $3, $4, $5) RETURNING *', [title, description, JSON.stringify(ingredients), JSON.stringify(instructions), tags] ); res.status(201).json(result.rows[0]); } catch (error) { console.error('Database error:', error); res.status(500).json({ error: 'Failed to create recipe' }); }});
// Error handling middlewareapp.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' });});
// Start serverapp.listen(port, () => { console.log(`Fork Recipes server running on port ${port}`);});
export default app;Create a TypeScript configuration file tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}Step 5: Create Database Schema
Create a schema.sql file for initializing the database:
-- Create recipes tableCREATE TABLE IF NOT EXISTS recipes ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, ingredients JSONB NOT NULL, instructions JSONB NOT NULL, prep_time INTEGER, cook_time INTEGER, servings INTEGER, tags TEXT[], image_url VARCHAR(500), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Create users tableCREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Create favorites tableCREATE TABLE IF NOT EXISTS favorites ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, recipe_id));
-- Create meal plans tableCREATE TABLE IF NOT EXISTS meal_plans ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, recipe_id INTEGER REFERENCES recipes(id) ON DELETE CASCADE, planned_date DATE NOT NULL, meal_type VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Create indexes for better performanceCREATE INDEX IF NOT EXISTS idx_recipes_tags ON recipes USING GIN(tags);CREATE INDEX IF NOT EXISTS idx_recipes_created_at ON recipes(created_at DESC);CREATE INDEX IF NOT EXISTS idx_favorites_user_id ON favorites(user_id);CREATE INDEX IF NOT EXISTS idx_meal_plans_user_date ON meal_plans(user_id, planned_date);
-- Function to update updated_at timestampCREATE OR REPLACE FUNCTION update_updated_at_column()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW;END;$$ LANGUAGE plpgsql;
-- Trigger to automatically update updated_atCREATE TRIGGER update_recipes_updated_at BEFORE UPDATE ON recipes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();Step 6: Create Environment Variables Template
Create a .env.example file to document required environment variables:
# Application SettingsNODE_ENV=productionPORT=3000APP_URL=https://example-app.klutch.sh
# Database Configuration (PostgreSQL - recommended)DATABASE_URL=postgresql://username:password@postgres-app.klutch.sh:8000/fork_recipes
# SecurityJWT_SECRET=your_jwt_secret_key_hereSESSION_SECRET=your_session_secret_here
# File Upload SettingsMAX_FILE_SIZE=10485760ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gif
# Email Configuration (optional - for password resets)SMTP_HOST=smtp.gmail.comSMTP_PORT=587SMTP_USER=your-email@gmail.comSMTP_PASSWORD=your-app-passwordSMTP_FROM=Fork Recipes <noreply@example.com>
# API Keys (optional - for recipe import)SPOONACULAR_API_KEY=your_spoonacular_keyEDAMAM_APP_ID=your_edamam_app_idEDAMAM_APP_KEY=your_edamam_app_key
# Feature FlagsENABLE_REGISTRATION=trueENABLE_PUBLIC_RECIPES=falseENABLE_RECIPE_IMPORT=trueStep 7: Create .gitignore
Create a .gitignore file to exclude sensitive and unnecessary files:
# Dependenciesnode_modules/package-lock.jsonyarn.lock
# Environment files.env*.env.local.env.production
# Build outputdist/build/.next/out/
# Uploads and datapublic/uploads/data/*.db*.sqlite
# Logslogs/*.lognpm-debug.log*
# OS files.DS_StoreThumbs.db
# IDE.vscode/.idea/*.swp*.swo
# Testingcoverage/.nyc_output/
# Temporary filestmp/temp/Step 8: Test Locally (Optional)
Before deploying to Klutch.sh, you can test your Fork Recipes setup locally:
# Install dependenciesnpm install
# Build the applicationnpm run build
# Build Docker imagedocker build -t fork-recipes-local .
# Run with environment variablesdocker run -d \ --name fork-recipes-test \ -p 3000:3000 \ -e DATABASE_URL=postgresql://user:pass@localhost:5432/fork_recipes \ -e JWT_SECRET=test_secret \ -e SESSION_SECRET=test_session \ -v $(pwd)/uploads:/app/uploads \ fork-recipes-local
# Access at http://localhost:3000# Check health: curl http://localhost:3000/health
# Stop containerdocker stop fork-recipes-test && docker rm fork-recipes-testStep 9: Initialize Git Repository
Commit your files to Git:
# Add filesgit add Dockerfile package.json tsconfig.json src/ schema.sql .env.example .gitignoregit commit -m "Initial Fork Recipes setup for Klutch.sh deployment"
# Create GitHub repository and pushgit remote add origin https://github.com/yourusername/fork-recipes-klutch.gitgit branch -M mastergit push -u origin masterDeploying to Klutch.sh
Step 1: Push to GitHub
- Create a new repository on GitHub.
- Push your local repository:
git remote add origin https://github.com/yourusername/fork-recipes-klutch.gitgit branch -M mastergit push -u origin masterStep 2: Deploy PostgreSQL Database
Before deploying Fork Recipes, set up a PostgreSQL database following our PostgreSQL deployment guide. You’ll need:
- PostgreSQL database name:
fork_recipes - Database user and password
- Connection URL in format:
postgresql://user:password@postgres-app.klutch.sh:8000/fork_recipes
After deploying PostgreSQL, connect to it and run the schema.sql file to create the necessary tables:
# Connect to PostgreSQLpsql postgresql://user:password@postgres-app.klutch.sh:8000/fork_recipes
# Run schema\i schema.sqlStep 3: Create a New App on Klutch.sh
- Log in to your Klutch.sh dashboard.
- Click New App and select your GitHub repository.
- Klutch.sh will automatically detect your
Dockerfileand prepare for deployment. - Select HTTP traffic since Fork Recipes is a web application.
- Set the internal port to 3000 (Fork Recipes' default HTTP port).
Step 4: Configure Environment Variables
In the Klutch.sh dashboard, add the following environment variables:
Required Configuration:
# ApplicationNODE_ENV=productionPORT=3000APP_URL=https://your-app.klutch.sh
# Database (replace with your actual credentials)DATABASE_URL=postgresql://fork_user:your_password@postgres-app.klutch.sh:8000/fork_recipes
# Security (generate strong secrets)JWT_SECRET=your_generated_jwt_secret_hereSESSION_SECRET=your_generated_session_secret_hereOptional Configuration:
# File UploadsMAX_FILE_SIZE=10485760ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gif
# Email (for password resets)SMTP_HOST=smtp.gmail.comSMTP_PORT=587SMTP_USER=your-email@gmail.comSMTP_PASSWORD=your_app_passwordSMTP_FROM=Fork Recipes <noreply@yourdomain.com>
# FeaturesENABLE_REGISTRATION=trueENABLE_PUBLIC_RECIPES=falseENABLE_RECIPE_IMPORT=trueMark sensitive variables like DATABASE_URL, JWT_SECRET, SESSION_SECRET, and SMTP_PASSWORD as secret in the dashboard.
Generate Secure Secrets:
# Generate JWT secretopenssl rand -base64 32
# Generate session secretopenssl rand -base64 32Step 5: Attach Persistent Volumes
Fork Recipes requires persistent storage for uploaded recipe images and data:
- In the Klutch.sh dashboard, navigate to Volumes.
- Add persistent volumes with the following configurations:
Volume 1: Recipe Images
- Mount Path:
/app/uploads - Size: 10-50 GB (depending on expected image storage needs)
- Purpose: Store uploaded recipe images and photos
Volume 2: Application Data
- Mount Path:
/app/data - Size: 5-10 GB
- Purpose: Store temporary files, caches, and application data
Step 6: Deploy Your Application
- Click Deploy in the Klutch.sh dashboard.
- Klutch.sh will build your Docker image and deploy the application.
- Monitor the build logs for any errors.
- Once deployed, your Fork Recipes instance will be accessible at
https://your-app.klutch.sh.
Step 7: Complete Initial Setup
- Access your deployed Fork Recipes instance at
https://your-app.klutch.sh. - If registration is enabled, create your first user account.
- Start adding recipes manually or import them from URLs.
- Configure user preferences and tags in the settings.
Configuration
Database Connection
Fork Recipes supports PostgreSQL as the primary database. The connection is configured via the DATABASE_URL environment variable:
PostgreSQL Connection String Format:
postgresql://username:password@host:port/database_nameExample:
DATABASE_URL=postgresql://fork_user:secure_password@postgres-app.klutch.sh:8000/fork_recipesAuthentication & Security
Fork Recipes uses JWT (JSON Web Tokens) for authentication:
# JWT ConfigurationJWT_SECRET=your_secret_key_hereJWT_EXPIRATION=7d
# Session ConfigurationSESSION_SECRET=your_session_secret_hereSESSION_MAX_AGE=604800000
# Password RequirementsMIN_PASSWORD_LENGTH=8REQUIRE_STRONG_PASSWORD=trueSecurity Best Practices:
- Use strong, randomly generated secrets
- Enable HTTPS (automatic on Klutch.sh)
- Implement rate limiting for API endpoints
- Validate and sanitize all user inputs
- Use parameterized queries to prevent SQL injection
File Upload Configuration
Configure file upload limits and allowed types:
# Upload SettingsMAX_FILE_SIZE=10485760ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gifUPLOAD_PATH=/app/uploads
# Image ProcessingIMAGE_QUALITY=85MAX_IMAGE_WIDTH=2048MAX_IMAGE_HEIGHT=2048GENERATE_THUMBNAILS=trueTHUMBNAIL_SIZE=300Email Configuration
Set up email for password resets and notifications:
# SMTP ConfigurationSMTP_HOST=smtp.gmail.comSMTP_PORT=587SMTP_SECURE=falseSMTP_USER=your-email@gmail.comSMTP_PASSWORD=your_app_passwordSMTP_FROM=Fork Recipes <noreply@yourdomain.com>
# Email FeaturesENABLE_EMAIL_VERIFICATION=falseENABLE_PASSWORD_RESET=trueRecommended SMTP Providers:
- Gmail: Free for low volume (requires app-specific password)
- SendGrid: Great deliverability with free tier
- Mailgun: Reliable transactional email service
- Amazon SES: Cost-effective for high volume
Recipe Import Configuration
Configure API keys for importing recipes from external services:
# Spoonacular API (recipe data)SPOONACULAR_API_KEY=your_api_key_hereSPOONACULAR_BASE_URL=https://api.spoonacular.com
# Edamam API (nutrition data)EDAMAM_APP_ID=your_app_idEDAMAM_APP_KEY=your_app_keyEDAMAM_BASE_URL=https://api.edamam.com
# Recipe Import SettingsENABLE_RECIPE_IMPORT=trueIMPORT_RATE_LIMIT=10Feature Flags
Control application features via environment variables:
# Registration & AccessENABLE_REGISTRATION=trueENABLE_PUBLIC_RECIPES=falseREQUIRE_EMAIL_VERIFICATION=false
# Recipe FeaturesENABLE_RECIPE_IMPORT=trueENABLE_RECIPE_SHARING=trueENABLE_RECIPE_COMMENTS=false
# Meal PlanningENABLE_MEAL_PLANNING=trueENABLE_SHOPPING_LISTS=true
# Social FeaturesENABLE_USER_PROFILES=trueENABLE_RECIPE_RATING=trueSample Code: Using Fork Recipes API
Fork Recipes provides a RESTful API for managing recipes programmatically. Here are examples in multiple languages:
JavaScript/Node.js - Create a Recipe
const axios = require('axios');
const API_URL = 'https://example-app.klutch.sh/api';const JWT_TOKEN = 'your_jwt_token_here';
async function createRecipe() { try { const response = await axios.post( `${API_URL}/recipes`, { title: 'Classic Chocolate Chip Cookies', description: 'Delicious homemade chocolate chip cookies that are crispy on the edges and chewy in the center.', prepTime: 15, cookTime: 12, servings: 24, ingredients: [ { item: 'all-purpose flour', amount: '2 1/4', unit: 'cups' }, { item: 'butter, softened', amount: '1', unit: 'cup' }, { item: 'granulated sugar', amount: '3/4', unit: 'cup' }, { item: 'brown sugar', amount: '3/4', unit: 'cup' }, { item: 'eggs', amount: '2', unit: 'large' }, { item: 'vanilla extract', amount: '2', unit: 'tsp' }, { item: 'baking soda', amount: '1', unit: 'tsp' }, { item: 'salt', amount: '1', unit: 'tsp' }, { item: 'chocolate chips', amount: '2', unit: 'cups' } ], instructions: [ { step: 1, text: 'Preheat oven to 375°F (190°C).' }, { step: 2, text: 'Mix flour, baking soda, and salt in a bowl.' }, { step: 3, text: 'Beat butter and sugars until creamy.' }, { step: 4, text: 'Add eggs and vanilla, beat well.' }, { step: 5, text: 'Gradually blend in flour mixture.' }, { step: 6, text: 'Stir in chocolate chips.' }, { step: 7, text: 'Drop rounded tablespoons onto ungreased cookie sheets.' }, { step: 8, text: 'Bake for 9-11 minutes or until golden brown.' } ], tags: ['dessert', 'cookies', 'chocolate', 'baking'] }, { headers: { 'Authorization': `Bearer ${JWT_TOKEN}`, 'Content-Type': 'application/json' } } );
console.log('Recipe created:', response.data); return response.data; } catch (error) { console.error('Error creating recipe:', error.response?.data || error.message); throw error; }}
createRecipe();Python - Search Recipes
import requestsimport json
API_URL = 'https://example-app.klutch.sh/api'JWT_TOKEN = 'your_jwt_token_here'
def search_recipes(query, tags=None, page=1, limit=20): """Search for recipes by title, ingredients, or tags"""
headers = { 'Authorization': f'Bearer {JWT_TOKEN}', 'Content-Type': 'application/json' }
params = { 'q': query, 'page': page, 'limit': limit }
if tags: params['tags'] = ','.join(tags)
try: response = requests.get( f'{API_URL}/recipes/search', headers=headers, params=params ) response.raise_for_status()
recipes = response.json() print(f"Found {len(recipes)} recipes:")
for recipe in recipes: print(f"\n{recipe['title']}") print(f" Prep: {recipe['prep_time']}min | Cook: {recipe['cook_time']}min") print(f" Tags: {', '.join(recipe['tags'])}") print(f" URL: {API_URL}/recipes/{recipe['id']}")
return recipes
except requests.exceptions.RequestException as error: print(f"Error searching recipes: {error}") raise
# Example usagesearch_recipes('chocolate', tags=['dessert', 'cookies'])Python - Upload Recipe Image
import requests
API_URL = 'https://example-app.klutch.sh/api'JWT_TOKEN = 'your_jwt_token_here'
def upload_recipe_image(recipe_id, image_path): """Upload an image for a recipe"""
headers = { 'Authorization': f'Bearer {JWT_TOKEN}' }
try: with open(image_path, 'rb') as image_file: files = { 'image': (image_path, image_file, 'image/jpeg') }
response = requests.post( f'{API_URL}/recipes/{recipe_id}/image', headers=headers, files=files ) response.raise_for_status()
result = response.json() print(f"Image uploaded successfully!") print(f"Image URL: {result['image_url']}")
return result
except FileNotFoundError: print(f"Image file not found: {image_path}") raise except requests.exceptions.RequestException as error: print(f"Error uploading image: {error}") raise
# Example usageupload_recipe_image(123, '/path/to/recipe-photo.jpg')Go - Get Recipe Details
package main
import ( "encoding/json" "fmt" "io" "net/http" "time")
const ( APIURL = "https://example-app.klutch.sh/api" JWTToken = "your_jwt_token_here")
type Recipe struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` PrepTime int `json:"prep_time"` CookTime int `json:"cook_time"` Servings int `json:"servings"` Ingredients []map[string]interface{} `json:"ingredients"` Instructions []map[string]interface{} `json:"instructions"` Tags []string `json:"tags"` ImageURL string `json:"image_url"` CreatedAt time.Time `json:"created_at"`}
func getRecipe(recipeID int) (*Recipe, error) { url := fmt.Sprintf("%s/recipes/%d", APIURL, recipeID)
client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) }
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", JWTToken)) req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) }
var recipe Recipe if err := json.NewDecoder(resp.Body).Decode(&recipe); err != nil { return nil, fmt.Errorf("error decoding response: %w", err) }
return &recipe, nil}
func main() { recipe, err := getRecipe(123) if err != nil { fmt.Printf("Error fetching recipe: %v\n", err) return }
fmt.Printf("Recipe: %s\n", recipe.Title) fmt.Printf("Description: %s\n", recipe.Description) fmt.Printf("Prep Time: %d minutes\n", recipe.PrepTime) fmt.Printf("Cook Time: %d minutes\n", recipe.CookTime) fmt.Printf("Servings: %d\n", recipe.Servings) fmt.Printf("Tags: %v\n", recipe.Tags)
fmt.Println("\nIngredients:") for _, ing := range recipe.Ingredients { fmt.Printf(" - %v %v %v\n", ing["amount"], ing["unit"], ing["item"]) }
fmt.Println("\nInstructions:") for _, inst := range recipe.Instructions { fmt.Printf(" %v. %v\n", inst["step"], inst["text"]) }}Ruby - Create Meal Plan
require 'net/http'require 'json'require 'uri'
API_URL = 'https://example-app.klutch.sh/api'JWT_TOKEN = 'your_jwt_token_here'
def create_meal_plan(recipe_id, date, meal_type) uri = URI("#{API_URL}/meal-plans")
http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true
request = Net::HTTP::Post.new(uri.path) request['Authorization'] = "Bearer #{JWT_TOKEN}" request['Content-Type'] = 'application/json'
request.body = { recipe_id: recipe_id, planned_date: date, meal_type: meal_type }.to_json
response = http.request(request)
if response.code == '201' meal_plan = JSON.parse(response.body) puts "Meal plan created successfully!" puts "Recipe: #{meal_plan['recipe']['title']}" puts "Date: #{meal_plan['planned_date']}" puts "Meal: #{meal_plan['meal_type']}" meal_plan else puts "Error: #{response.code} - #{response.body}" nil endrescue StandardError => e puts "Error creating meal plan: #{e.message}" nilend
# Example usagecreate_meal_plan(123, '2024-01-15', 'dinner')PHP - Generate Shopping List
<?php
define('API_URL', 'https://example-app.klutch.sh/api');define('JWT_TOKEN', 'your_jwt_token_here');
function getShoppingList($startDate, $endDate) { $url = API_URL . '/meal-plans/shopping-list';
$params = http_build_query([ 'start_date' => $startDate, 'end_date' => $endDate ]);
$ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "$url?$params", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . JWT_TOKEN, 'Content-Type: application/json' ] ]);
$response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
if ($httpCode === 200) { $shoppingList = json_decode($response, true);
echo "Shopping List for {$startDate} to {$endDate}\n\n";
foreach ($shoppingList['categories'] as $category => $items) { echo strtoupper($category) . ":\n"; foreach ($items as $item) { echo " - {$item['amount']} {$item['unit']} {$item['name']}\n"; } echo "\n"; }
return $shoppingList; } else { echo "Error: $httpCode - $response\n"; return null; }}
// Example usagegetShoppingList('2024-01-15', '2024-01-21');
?>Production Best Practices
Security Hardening
- Use Strong Secrets: Generate cryptographically secure secrets for JWT and sessions.
# Generate secure secretsopenssl rand -base64 32openssl rand -hex 32- Enable HTTPS Only: Klutch.sh provides automatic HTTPS. Ensure your APP_URL uses
https://. - Implement Rate Limiting: Protect API endpoints from abuse:
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.'});
app.use('/api/', apiLimiter);- Validate User Input: Use Zod or similar libraries for input validation:
import { z } from 'zod';
const recipeSchema = z.object({ title: z.string().min(1).max(255), description: z.string().optional(), prepTime: z.number().int().positive(), cookTime: z.number().int().positive(), servings: z.number().int().positive(), ingredients: z.array(z.object({ item: z.string(), amount: z.string(), unit: z.string() })), instructions: z.array(z.object({ step: z.number(), text: z.string() })), tags: z.array(z.string()).optional()});- Sanitize File Uploads: Validate file types and sanitize filenames.
- Use Prepared Statements: Always use parameterized queries to prevent SQL injection.
- Enable CORS Properly: Configure CORS to allow only trusted domains.
Performance Optimization
- Database Indexing: Ensure proper indexes on frequently queried columns:
CREATE INDEX idx_recipes_title ON recipes(title);CREATE INDEX idx_recipes_tags ON recipes USING GIN(tags);CREATE INDEX idx_recipes_created_at ON recipes(created_at DESC);- Image Optimization: Use Sharp for image processing and optimization:
import sharp from 'sharp';
async function optimizeImage(inputPath: string, outputPath: string) { await sharp(inputPath) .resize(2048, 2048, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 85, progressive: true }) .toFile(outputPath);}- Enable Caching: Implement caching for frequently accessed recipes:
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 600 }); // 10 minutes
app.get('/api/recipes/:id', async (req, res) => { const cacheKey = `recipe:${req.params.id}`; const cached = cache.get(cacheKey);
if (cached) { return res.json(cached); }
const recipe = await getRecipeFromDB(req.params.id); cache.set(cacheKey, recipe); res.json(recipe);});- Enable Compression: Use gzip compression for API responses:
import compression from 'compression';
app.use(compression());- Optimize Database Queries: Use pagination and limit result sets:
app.get('/api/recipes', async (req, res) => { const page = parseInt(req.query.page as string) || 1; const limit = parseInt(req.query.limit as string) || 20; const offset = (page - 1) * limit;
const result = await pool.query( 'SELECT * FROM recipes ORDER BY created_at DESC LIMIT $1 OFFSET $2', [limit, offset] );
res.json({ recipes: result.rows, page, limit, total: result.rowCount });});Backup Strategy
- Database Backups: Regularly backup your PostgreSQL database:
# Automated daily backuppg_dump postgresql://user:pass@postgres-app.klutch.sh:8000/fork_recipes > backup-$(date +%Y%m%d).sql
# Compressed backuppg_dump postgresql://user:pass@postgres-app.klutch.sh:8000/fork_recipes | gzip > backup-$(date +%Y%m%d).sql.gz- Volume Backups: Backup the
/app/uploadsvolume containing recipe images. - Automated Backups: Set up automated daily backups and store them offsite (S3, Backblaze B2).
- Test Restores: Periodically test backup restoration to ensure data integrity.
- Backup Retention: Keep daily backups for 7 days, weekly for 4 weeks, monthly for 12 months.
Monitoring and Logging
- Application Logs: Implement structured logging:
import winston from 'winston';
const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console({ format: winston.format.simple() }) ]});
logger.info('Recipe created', { recipeId: 123, userId: 456 });- Error Tracking: Use error tracking services (Sentry, Rollbar) for production errors.
- Health Checks: Monitor application health via the
/healthendpoint. - Database Monitoring: Track database performance, query times, and connection pool usage.
- Resource Monitoring: Monitor CPU, memory, and disk usage through Klutch.sh dashboard.
Scaling Recommendations
- Vertical Scaling: Start with 1-2 GB RAM, scale to 4-8 GB for high traffic (1000+ users).
- Storage Scaling: Monitor upload volume growth and expand storage as needed.
- Database Optimization: Use connection pooling and read replicas for high read loads.
- CDN for Images: Use a CDN (Cloudflare, CloudFront) for serving recipe images.
- Caching Layer: Deploy Redis for caching frequently accessed recipes and search results.
Troubleshooting
Cannot Access Fork Recipes Web Interface
Problem: Unable to access Fork Recipes at the deployed URL.
Solution:
- Verify deployment status in Klutch.sh dashboard
- Check that HTTP traffic is enabled and port 3000 is configured
- Review deployment logs for startup errors
- Ensure
APP_URLenvironment variable matches your actual URL - Check that the health endpoint responds:
curl https://your-app.klutch.sh/health
Database Connection Errors
Problem: Fork Recipes fails to connect to PostgreSQL database.
Solution:
- Verify PostgreSQL is deployed and accessible
- Check
DATABASE_URLformat:postgresql://user:pass@host:8000/dbname - Test database connection:
psql $DATABASE_URL - Ensure database user has proper permissions
- Check that database schema has been initialized with
schema.sql - Review application logs for specific connection errors
Recipe Images Not Uploading
Problem: Cannot upload images to recipes.
Solution:
- Verify persistent volume is mounted at
/app/uploads - Check volume has sufficient space available
- Ensure
MAX_FILE_SIZEenvironment variable is set appropriately - Verify file type is in
ALLOWED_FILE_TYPESlist - Check file permissions on upload directory
- Review logs for specific upload errors
JWT Token Errors
Problem: Authentication fails with JWT errors.
Solution:
- Ensure
JWT_SECRETis set and consistent across deployments - Check token expiration settings
- Verify
JWT_SECREThasn’t changed (would invalidate all tokens) - Clear browser cookies/localStorage and re-authenticate
- Check system time is synchronized (JWT timestamps)
Search Not Working
Problem: Recipe search returns no results or errors.
Solution:
- Verify database indexes are created (especially GIN index on tags)
- Check search query syntax and parameters
- Ensure full-text search is properly configured in PostgreSQL
- Review database logs for query errors
- Test search directly in database to isolate issue
Email Not Sending
Problem: Password reset or notification emails not being delivered.
Solution:
- Verify SMTP credentials are correct
- Check SMTP host and port settings
- Test SMTP connection manually
- Ensure firewall allows outbound SMTP connections
- Check email provider’s sending limits
- Review application logs for SMTP errors
- Verify
SMTP_FROMaddress is valid
Performance Issues
Problem: Slow page loads or timeouts.
Solution:
- Check resource usage (CPU, memory) in Klutch.sh dashboard
- Review slow database queries in PostgreSQL logs
- Enable query logging and optimize slow queries
- Implement caching for frequently accessed data
- Optimize image sizes and enable lazy loading
- Increase container resources if consistently high usage
- Consider implementing pagination for large result sets
Additional Resources
- Fork Recipes GitHub Repository
- Node.js Documentation
- Express.js Framework
- PostgreSQL Documentation
- JWT.io - JSON Web Tokens
- Sharp Image Processing
- PostgreSQL Deployment Guide
- Klutch.sh Persistent Volumes
- Klutch.sh Networking
- Klutch.sh Deployments
You now have a fully functional Fork Recipes instance running on Klutch.sh! Your self-hosted recipe manager is ready to store unlimited recipes, organize your culinary collection, plan meals, and share your favorite dishes with family and friends—all with complete privacy and control over your data. Remember to regularly backup your database and uploaded images, keep dependencies updated, and monitor performance as your recipe collection grows.