docker compose fixed for persistence

This commit is contained in:
taogaetz 2025-09-08 19:54:02 -04:00
parent 208c8be6c4
commit 3d043d9ade
9 changed files with 308 additions and 20 deletions

103
DATABASE_MANAGEMENT.md Normal file
View File

@ -0,0 +1,103 @@
# ChefBible Database Management
Easy backup and restore for your recipes using the `db-manager.sh` script.
## Quick Start
### 1. Setup (One-time)
```bash
# Create the data directory on your server
sudo mkdir -p /home/chefbible/data
sudo chown 1001:1001 /home/chefbible/data
sudo chmod 755 /home/chefbible/data
```
### 2. Before Updating Your App
```bash
# Create a backup
./db-manager.sh backup
```
### 3. After Updating (if data is lost)
```bash
# Restore from the latest backup
./db-manager.sh restore
```
## Available Commands
| Command | Description | Example |
|---------|-------------|---------|
| `backup` | Create a backup of the database | `./db-manager.sh backup` |
| `restore` | Restore from the most recent backup | `./db-manager.sh restore` |
| `list` | List all available backups | `./db-manager.sh list` |
| `status` | Show current database status | `./db-manager.sh status` |
| `help` | Show help message | `./db-manager.sh help` |
## Typical Workflow
### Before App Updates:
```bash
# 1. Check current status
./db-manager.sh status
# 2. Create backup
./db-manager.sh backup
# 3. Update your app in Portainer
# (Your data should be preserved, but backup is safety net)
```
### If Data Gets Lost:
```bash
# 1. Check available backups
./db-manager.sh list
# 2. Restore from latest backup
./db-manager.sh restore
# 3. Restart your container in Portainer
```
## Backup Storage
- **Location**: `/home/chefbible/backups/`
- **Format**: `chefbible_backup_YYYYMMDD_HHMMSS.db`
- **Retention**: Keeps last 5 backups automatically
- **Size**: Typically 1-10MB per backup
## Troubleshooting
### Permission Issues
```bash
# If you get permission errors, run with sudo:
sudo ./db-manager.sh backup
sudo ./db-manager.sh restore
```
### Container Not Found
```bash
# If container name is different, edit the script:
# Change CONTAINER_NAME="chefbible" to your actual container name
```
### Database Path Issues
```bash
# Check if the database path exists:
ls -la /home/chefbible/data/
```
## Safety Features
**Automatic cleanup** - Keeps only last 5 backups
**Size reporting** - Shows backup sizes
**Status checking** - Verifies database and container state
**Error handling** - Clear error messages and suggestions
**Color output** - Easy to read status messages
## Pro Tips
1. **Always backup before major updates**
2. **Test restore process** on a test environment first
3. **Keep backups in a separate location** for extra safety
4. **Monitor backup sizes** - large backups might indicate issues

169
db-manager.sh Executable file
View File

@ -0,0 +1,169 @@
#!/bin/bash
# ChefBible Database Manager
# Easy backup and restore for your recipes
BACKUP_DIR="/home/chefbible/backups"
DB_PATH="/home/chefbible/data/database.db"
CONTAINER_NAME="chefbible"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
show_help() {
echo -e "${BLUE}ChefBible Database Manager${NC}"
echo ""
echo "Usage: $0 [command]"
echo ""
echo "Commands:"
echo " backup - Create a backup of the database"
echo " restore - Restore from the most recent backup"
echo " list - List all available backups"
echo " status - Show current database status"
echo " help - Show this help message"
echo ""
echo "Examples:"
echo " $0 backup # Create backup"
echo " $0 restore # Restore from latest backup"
echo " $0 restore backup_20240101 # Restore from specific backup"
}
backup_database() {
echo -e "${BLUE}🔄 Creating backup of ChefBible database...${NC}"
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Check if database exists
if [ -f "$DB_PATH" ]; then
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/chefbible_backup_$TIMESTAMP.db"
# Create backup
cp "$DB_PATH" "$BACKUP_FILE"
echo -e "${GREEN}✅ Backup created: $(basename "$BACKUP_FILE")${NC}"
# Show backup size
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo -e "${GREEN}📊 Backup size: $BACKUP_SIZE${NC}"
# Keep only last 5 backups
ls -t "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | tail -n +6 | xargs -r rm
echo -e "${GREEN}🧹 Cleaned up old backups (keeping last 5)${NC}"
else
echo -e "${YELLOW}⚠️ No database found at $DB_PATH${NC}"
echo -e "${YELLOW} This might be a fresh installation${NC}"
fi
echo -e "${GREEN}🎉 Backup process complete!${NC}"
}
restore_database() {
echo -e "${BLUE}🔄 Restoring ChefBible database...${NC}"
# Create data directory if it doesn't exist
mkdir -p "$(dirname "$DB_PATH")"
# If no backup file specified, use the most recent one
if [ -z "$1" ]; then
BACKUP_FILE=$(ls -t "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | head -n1)
if [ -z "$BACKUP_FILE" ]; then
echo -e "${RED}❌ No backup files found in $BACKUP_DIR${NC}"
echo -e "${YELLOW} Run '$0 backup' first to create a backup${NC}"
exit 1
fi
echo -e "${BLUE}📁 Using most recent backup: $(basename "$BACKUP_FILE")${NC}"
else
BACKUP_FILE="$BACKUP_DIR/chefbible_backup_$1.db"
if [ ! -f "$BACKUP_FILE" ]; then
echo -e "${RED}❌ Backup file not found: $BACKUP_FILE${NC}"
echo -e "${YELLOW} Available backups:${NC}"
list_backups
exit 1
fi
fi
# Stop the container if it's running
echo -e "${YELLOW}⏹️ Stopping ChefBible container...${NC}"
docker stop "$CONTAINER_NAME" 2>/dev/null || echo -e "${YELLOW} Container not running${NC}"
# Restore the database
echo -e "${BLUE}📥 Restoring database from: $(basename "$BACKUP_FILE")${NC}"
cp "$BACKUP_FILE" "$DB_PATH"
# Set proper permissions
chown 1001:1001 "$DB_PATH" 2>/dev/null || echo -e "${YELLOW} Note: Run with sudo to set proper permissions${NC}"
echo -e "${GREEN}✅ Database restored successfully!${NC}"
echo -e "${GREEN}🚀 You can now start the ChefBible container${NC}"
# Show restored database size
if [ -f "$DB_PATH" ]; then
DB_SIZE=$(du -h "$DB_PATH" | cut -f1)
echo -e "${GREEN}📊 Restored database size: $DB_SIZE${NC}"
fi
}
list_backups() {
echo -e "${BLUE}📋 Available backups:${NC}"
if [ -d "$BACKUP_DIR" ] && [ "$(ls -A "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null)" ]; then
ls -lh "$BACKUP_DIR"/chefbible_backup_*.db | awk '{print " " $9 " (" $5 ", " $6 " " $7 " " $8 ")"}'
else
echo -e "${YELLOW} No backups found${NC}"
echo -e "${YELLOW} Run '$0 backup' to create your first backup${NC}"
fi
}
show_status() {
echo -e "${BLUE}📊 ChefBible Database Status${NC}"
echo ""
# Check if database exists
if [ -f "$DB_PATH" ]; then
DB_SIZE=$(du -h "$DB_PATH" | cut -f1)
echo -e "${GREEN}✅ Database exists: $DB_PATH${NC}"
echo -e "${GREEN}📊 Size: $DB_SIZE${NC}"
else
echo -e "${RED}❌ No database found at $DB_PATH${NC}"
fi
# Check container status
if docker ps | grep -q "$CONTAINER_NAME"; then
echo -e "${GREEN}✅ Container is running${NC}"
else
echo -e "${YELLOW}⚠️ Container is not running${NC}"
fi
# Show backup count
BACKUP_COUNT=$(ls "$BACKUP_DIR"/chefbible_backup_*.db 2>/dev/null | wc -l)
echo -e "${BLUE}📋 Backups available: $BACKUP_COUNT${NC}"
}
# Main script logic
case "$1" in
"backup")
backup_database
;;
"restore")
restore_database "$2"
;;
"list")
list_backups
;;
"status")
show_status
;;
"help"|"--help"|"-h"|"")
show_help
;;
*)
echo -e "${RED}❌ Unknown command: $1${NC}"
echo ""
show_help
exit 1
;;
esac

13
deploy.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
# Super Simple ChefBible Deploy
# Just run: ./deploy.sh
echo "🚀 Building and pushing ChefBible..."
# Build and push in one go
docker build -t git.redbackpack.ca/taogaetz/chefbible:latest . && \
docker push git.redbackpack.ca/taogaetz/chefbible:latest
echo "✅ Done! Now go to Portainer and click 'Update the stack'"
echo "💾 If data gets lost, run './db-manager.sh restore' on your server"

View File

@ -6,7 +6,7 @@ services:
container_name: chefbible container_name: chefbible
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "4000:3000"
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- DATABASE_URL=file:/app/data/database.db - DATABASE_URL=file:/app/data/database.db
@ -14,14 +14,10 @@ services:
- CLOUDINARY_URL=${CLOUDINARY_URL} - CLOUDINARY_URL=${CLOUDINARY_URL}
- ORIGIN=${ORIGIN} - ORIGIN=${ORIGIN}
volumes: volumes:
- chefbible_data:/app/data - /home/chefbible/data:/app/data
healthcheck: healthcheck:
test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" ] test: [ "CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })" ]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
volumes:
chefbible_data:
driver: local

View File

@ -11,8 +11,14 @@ if [ -z "$DATABASE_URL" ]; then
fi fi
npx prisma migrate deploy npx prisma migrate deploy
echo "Seeding database..." echo "Checking if database needs seeding..."
# Check if database file exists and has content
if [ ! -f "/app/data/database.db" ] || [ ! -s "/app/data/database.db" ]; then
echo "Database is empty or doesn't exist, seeding with sample data..."
npx prisma db seed npx prisma db seed
else
echo "Database already exists with data, skipping seed..."
fi
echo "Starting application..." echo "Starting application..."
exec "$@" exec "$@"

View File

@ -14,7 +14,7 @@ generator client {
datasource db { datasource db {
provider = "sqlite" provider = "sqlite"
url = "file:./database.db" url = env("DATABASE_URL")
} }
model Recipe { model Recipe {

View File

@ -3,13 +3,14 @@ import { PrismaClient } from '@prisma-app/client';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
async function main() { async function main() {
console.log('🧹 Clearing existing records...'); // Check if database already has data
await prisma.menuRecipe.deleteMany(); const existingRecipes = await prisma.recipe.count();
await prisma.recipeIngredient.deleteMany();
await prisma.menu.deleteMany(); if (existingRecipes > 0) {
await prisma.recipe.deleteMany(); console.log('📊 Database already contains data, skipping seed...');
await prisma.ingredient.deleteMany(); console.log(`Found ${existingRecipes} existing recipes`);
await prisma.allergen.deleteMany(); return;
}
console.log('🌱 Seeding database with new recipes...'); console.log('🌱 Seeding database with new recipes...');

View File

@ -4,7 +4,7 @@ import prisma from '$lib/server/prisma';
// Configure Cloudinary conditionally // Configure Cloudinary conditionally
let cloudinary: any = null; let cloudinary: any = null;
try { try {
const { CLOUDINARY_URL } = await import('$env/static/private'); const CLOUDINARY_URL = process.env.CLOUDINARY_URL;
if (CLOUDINARY_URL) { if (CLOUDINARY_URL) {
const { v2 } = await import('cloudinary'); const { v2 } = await import('cloudinary');
cloudinary = v2; cloudinary = v2;
@ -62,7 +62,7 @@ export const POST: RequestHandler = async ({ request, params }) => {
{ quality: 'auto:good' } { quality: 'auto:good' }
] ]
}, },
(error, result) => { (error: any, result: any) => {
if (error) reject(error); if (error) reject(error);
else if (result) resolve(result as { secure_url: string }); else if (result) resolve(result as { secure_url: string });
else reject(new Error('No result from Cloudinary')); else reject(new Error('No result from Cloudinary'));

View File

@ -4,7 +4,7 @@ import prisma from '$lib/server/prisma';
// Configure Cloudinary conditionally // Configure Cloudinary conditionally
let cloudinary: any = null; let cloudinary: any = null;
try { try {
const { CLOUDINARY_URL } = await import('$env/static/private'); const CLOUDINARY_URL = process.env.CLOUDINARY_URL;
if (CLOUDINARY_URL) { if (CLOUDINARY_URL) {
const { v2 } = await import('cloudinary'); const { v2 } = await import('cloudinary');
cloudinary = v2; cloudinary = v2;
@ -62,7 +62,7 @@ export const POST: RequestHandler = async ({ request }) => {
{ quality: 'auto:good' } { quality: 'auto:good' }
] ]
}, },
(error, result) => { (error: any, result: any) => {
if (error) reject(error); if (error) reject(error);
else if (result) resolve(result as { secure_url: string }); else if (result) resolve(result as { secure_url: string });
else reject(new Error('No result from Cloudinary')); else reject(new Error('No result from Cloudinary'));