Skip to content

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:


Installation and Setup

Step 1: Create Your Project Directory

First, create a new directory for your Fork Recipes deployment project:

Terminal window
mkdir fork-recipes-klutch
cd fork-recipes-klutch
git init

Step 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 directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
# Install production dependencies only
COPY package*.json ./
RUN npm ci --only=production
# Copy built application from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
# Create directories for uploads and data
RUN mkdir -p /app/data /app/uploads && \
chown -R node:node /app/data /app/uploads
# Switch to non-root user
USER node
# Expose port 3000
EXPOSE 3000
# Health check
HEALTHCHECK --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 application
CMD ["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:

Terminal window
mkdir -p src/routes src/controllers src/models src/middleware public/uploads

Create 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 connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Static files
app.use('/uploads', express.static('public/uploads'));
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// API routes
app.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 middleware
app.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 server
app.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 table
CREATE 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 table
CREATE 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 table
CREATE 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 table
CREATE 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 performance
CREATE 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 timestamp
CREATE 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_at
CREATE 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:

Terminal window
# Application Settings
NODE_ENV=production
PORT=3000
APP_URL=https://example-app.klutch.sh
# Database Configuration (PostgreSQL - recommended)
DATABASE_URL=postgresql://username:password@postgres-app.klutch.sh:8000/fork_recipes
# Security
JWT_SECRET=your_jwt_secret_key_here
SESSION_SECRET=your_session_secret_here
# File Upload Settings
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gif
# Email Configuration (optional - for password resets)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_FROM=Fork Recipes <noreply@example.com>
# API Keys (optional - for recipe import)
SPOONACULAR_API_KEY=your_spoonacular_key
EDAMAM_APP_ID=your_edamam_app_id
EDAMAM_APP_KEY=your_edamam_app_key
# Feature Flags
ENABLE_REGISTRATION=true
ENABLE_PUBLIC_RECIPES=false
ENABLE_RECIPE_IMPORT=true

Step 7: Create .gitignore

Create a .gitignore file to exclude sensitive and unnecessary files:

# Dependencies
node_modules/
package-lock.json
yarn.lock
# Environment files
.env
*.env.local
.env.production
# Build output
dist/
build/
.next/
out/
# Uploads and data
public/uploads/
data/
*.db
*.sqlite
# Logs
logs/
*.log
npm-debug.log*
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Testing
coverage/
.nyc_output/
# Temporary files
tmp/
temp/

Step 8: Test Locally (Optional)

Before deploying to Klutch.sh, you can test your Fork Recipes setup locally:

Terminal window
# Install dependencies
npm install
# Build the application
npm run build
# Build Docker image
docker build -t fork-recipes-local .
# Run with environment variables
docker 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 container
docker stop fork-recipes-test && docker rm fork-recipes-test

Step 9: Initialize Git Repository

Commit your files to Git:

Terminal window
# Add files
git add Dockerfile package.json tsconfig.json src/ schema.sql .env.example .gitignore
git commit -m "Initial Fork Recipes setup for Klutch.sh deployment"
# Create GitHub repository and push
git remote add origin https://github.com/yourusername/fork-recipes-klutch.git
git branch -M master
git push -u origin master

Deploying to Klutch.sh

Step 1: Push to GitHub

  1. Create a new repository on GitHub.
  2. Push your local repository:
Terminal window
git remote add origin https://github.com/yourusername/fork-recipes-klutch.git
git branch -M master
git push -u origin master

Step 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:

Terminal window
# Connect to PostgreSQL
psql postgresql://user:password@postgres-app.klutch.sh:8000/fork_recipes
# Run schema
\i schema.sql

Step 3: Create a New App on Klutch.sh

  1. Log in to your Klutch.sh dashboard.
  2. Click New App and select your GitHub repository.
  3. Klutch.sh will automatically detect your Dockerfile and prepare for deployment.
  4. Select HTTP traffic since Fork Recipes is a web application.
  5. 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:

Terminal window
# Application
NODE_ENV=production
PORT=3000
APP_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_here
SESSION_SECRET=your_generated_session_secret_here

Optional Configuration:

Terminal window
# File Uploads
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gif
# Email (for password resets)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your_app_password
SMTP_FROM=Fork Recipes <noreply@yourdomain.com>
# Features
ENABLE_REGISTRATION=true
ENABLE_PUBLIC_RECIPES=false
ENABLE_RECIPE_IMPORT=true

Mark sensitive variables like DATABASE_URL, JWT_SECRET, SESSION_SECRET, and SMTP_PASSWORD as secret in the dashboard.

Generate Secure Secrets:

Terminal window
# Generate JWT secret
openssl rand -base64 32
# Generate session secret
openssl rand -base64 32

Step 5: Attach Persistent Volumes

Fork Recipes requires persistent storage for uploaded recipe images and data:

  1. In the Klutch.sh dashboard, navigate to Volumes.
  2. 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

  1. Click Deploy in the Klutch.sh dashboard.
  2. Klutch.sh will build your Docker image and deploy the application.
  3. Monitor the build logs for any errors.
  4. Once deployed, your Fork Recipes instance will be accessible at https://your-app.klutch.sh.

Step 7: Complete Initial Setup

  1. Access your deployed Fork Recipes instance at https://your-app.klutch.sh.
  2. If registration is enabled, create your first user account.
  3. Start adding recipes manually or import them from URLs.
  4. 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:

Terminal window
postgresql://username:password@host:port/database_name

Example:

Terminal window
DATABASE_URL=postgresql://fork_user:secure_password@postgres-app.klutch.sh:8000/fork_recipes

Authentication & Security

Fork Recipes uses JWT (JSON Web Tokens) for authentication:

Terminal window
# JWT Configuration
JWT_SECRET=your_secret_key_here
JWT_EXPIRATION=7d
# Session Configuration
SESSION_SECRET=your_session_secret_here
SESSION_MAX_AGE=604800000
# Password Requirements
MIN_PASSWORD_LENGTH=8
REQUIRE_STRONG_PASSWORD=true

Security 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:

Terminal window
# Upload Settings
MAX_FILE_SIZE=10485760
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/webp,image/gif
UPLOAD_PATH=/app/uploads
# Image Processing
IMAGE_QUALITY=85
MAX_IMAGE_WIDTH=2048
MAX_IMAGE_HEIGHT=2048
GENERATE_THUMBNAILS=true
THUMBNAIL_SIZE=300

Email Configuration

Set up email for password resets and notifications:

Terminal window
# SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your_app_password
SMTP_FROM=Fork Recipes <noreply@yourdomain.com>
# Email Features
ENABLE_EMAIL_VERIFICATION=false
ENABLE_PASSWORD_RESET=true

Recommended 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:

Terminal window
# Spoonacular API (recipe data)
SPOONACULAR_API_KEY=your_api_key_here
SPOONACULAR_BASE_URL=https://api.spoonacular.com
# Edamam API (nutrition data)
EDAMAM_APP_ID=your_app_id
EDAMAM_APP_KEY=your_app_key
EDAMAM_BASE_URL=https://api.edamam.com
# Recipe Import Settings
ENABLE_RECIPE_IMPORT=true
IMPORT_RATE_LIMIT=10

Feature Flags

Control application features via environment variables:

Terminal window
# Registration & Access
ENABLE_REGISTRATION=true
ENABLE_PUBLIC_RECIPES=false
REQUIRE_EMAIL_VERIFICATION=false
# Recipe Features
ENABLE_RECIPE_IMPORT=true
ENABLE_RECIPE_SHARING=true
ENABLE_RECIPE_COMMENTS=false
# Meal Planning
ENABLE_MEAL_PLANNING=true
ENABLE_SHOPPING_LISTS=true
# Social Features
ENABLE_USER_PROFILES=true
ENABLE_RECIPE_RATING=true

Sample 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 requests
import 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 usage
search_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 usage
upload_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
end
rescue StandardError => e
puts "Error creating meal plan: #{e.message}"
nil
end
# Example usage
create_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 usage
getShoppingList('2024-01-15', '2024-01-21');
?>

Production Best Practices

Security Hardening

  1. Use Strong Secrets: Generate cryptographically secure secrets for JWT and sessions.
Terminal window
# Generate secure secrets
openssl rand -base64 32
openssl rand -hex 32
  1. Enable HTTPS Only: Klutch.sh provides automatic HTTPS. Ensure your APP_URL uses https://.
  2. 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);
  1. 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()
});
  1. Sanitize File Uploads: Validate file types and sanitize filenames.
  2. Use Prepared Statements: Always use parameterized queries to prevent SQL injection.
  3. Enable CORS Properly: Configure CORS to allow only trusted domains.

Performance Optimization

  1. 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);
  1. 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);
}
  1. 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);
});
  1. Enable Compression: Use gzip compression for API responses:
import compression from 'compression';
app.use(compression());
  1. 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

  1. Database Backups: Regularly backup your PostgreSQL database:
Terminal window
# Automated daily backup
pg_dump postgresql://user:pass@postgres-app.klutch.sh:8000/fork_recipes > backup-$(date +%Y%m%d).sql
# Compressed backup
pg_dump postgresql://user:pass@postgres-app.klutch.sh:8000/fork_recipes | gzip > backup-$(date +%Y%m%d).sql.gz
  1. Volume Backups: Backup the /app/uploads volume containing recipe images.
  2. Automated Backups: Set up automated daily backups and store them offsite (S3, Backblaze B2).
  3. Test Restores: Periodically test backup restoration to ensure data integrity.
  4. Backup Retention: Keep daily backups for 7 days, weekly for 4 weeks, monthly for 12 months.

Monitoring and Logging

  1. 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 });
  1. Error Tracking: Use error tracking services (Sentry, Rollbar) for production errors.
  2. Health Checks: Monitor application health via the /health endpoint.
  3. Database Monitoring: Track database performance, query times, and connection pool usage.
  4. Resource Monitoring: Monitor CPU, memory, and disk usage through Klutch.sh dashboard.

Scaling Recommendations

  1. Vertical Scaling: Start with 1-2 GB RAM, scale to 4-8 GB for high traffic (1000+ users).
  2. Storage Scaling: Monitor upload volume growth and expand storage as needed.
  3. Database Optimization: Use connection pooling and read replicas for high read loads.
  4. CDN for Images: Use a CDN (Cloudflare, CloudFront) for serving recipe images.
  5. 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_URL environment 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_URL format: 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_SIZE environment variable is set appropriately
  • Verify file type is in ALLOWED_FILE_TYPES list
  • Check file permissions on upload directory
  • Review logs for specific upload errors

JWT Token Errors

Problem: Authentication fails with JWT errors.

Solution:

  • Ensure JWT_SECRET is set and consistent across deployments
  • Check token expiration settings
  • Verify JWT_SECRET hasn’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_FROM address 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


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.