diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 483dea2..4f8ea93 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -122,11 +122,11 @@ Plans:
3. Data persists across container restarts via named volumes
4. Single docker-compose.yml starts the entire application
5. Backup of data directory preserves all entries and images
-**Plans**: TBD
+**Plans**: 2 plans
Plans:
-- [ ] 06-01: TBD
-- [ ] 06-02: TBD
+- [ ] 06-01-PLAN.md — Docker configuration with adapter-node, Dockerfile, and docker-compose.yml
+- [ ] 06-02-PLAN.md — Health endpoint, environment documentation, and backup script
## Progress
diff --git a/.planning/phases/06-deployment/06-01-PLAN.md b/.planning/phases/06-deployment/06-01-PLAN.md
new file mode 100644
index 0000000..68a8dab
--- /dev/null
+++ b/.planning/phases/06-deployment/06-01-PLAN.md
@@ -0,0 +1,320 @@
+---
+phase: 06-deployment
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - svelte.config.js
+ - Dockerfile
+ - .dockerignore
+ - docker-compose.yml
+autonomous: true
+
+must_haves:
+ truths:
+ - "Application builds with adapter-node for production Node.js server"
+ - "Docker image is multi-stage with small Alpine base (~150MB)"
+ - "Container runs as non-root 'node' user"
+ - "docker-compose up -d starts the application"
+ artifacts:
+ - path: "svelte.config.js"
+ provides: "adapter-node configuration with TASKPLANER_ prefix"
+ contains: "adapter-node"
+ - path: "Dockerfile"
+ provides: "Multi-stage build for production"
+ contains: "FROM node:22-alpine"
+ - path: ".dockerignore"
+ provides: "Build context exclusions"
+ contains: "node_modules"
+ - path: "docker-compose.yml"
+ provides: "Single-service compose with named volume"
+ contains: "taskplaner_data"
+ key_links:
+ - from: "svelte.config.js"
+ to: "adapter-node"
+ via: "adapter import and configuration"
+ pattern: "adapter-node"
+ - from: "Dockerfile"
+ to: "docker-compose.yml"
+ via: "build context"
+ pattern: "build: \\."
+---
+
+
+Docker configuration for SvelteKit production deployment
+
+Purpose: Enable the application to run in a Docker container with proper production settings, multi-stage build for small image size, and non-root user for security.
+
+Output: Dockerfile, docker-compose.yml, .dockerignore, and updated svelte.config.js with adapter-node
+
+
+
+@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
+@/home/tho/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-deployment/06-RESEARCH.md
+
+Key existing patterns:
+- Database at ./data/taskplaner.db with WAL mode
+- Image storage at ./data/uploads/originals and ./data/uploads/thumbnails
+- DATABASE_PATH env var already used in src/lib/server/db/index.ts
+
+
+
+
+
+ Task 1: Switch to adapter-node with environment prefix
+ svelte.config.js, package.json
+
+1. Install adapter-node as dev dependency:
+ ```bash
+ npm install -D @sveltejs/adapter-node
+ ```
+
+2. Update svelte.config.js:
+ - Import adapter from '@sveltejs/adapter-node' (not adapter-auto)
+ - Configure adapter with:
+ - out: 'build'
+ - precompress: true (gzip/brotli static assets)
+ - envPrefix: 'TASKPLANER_' (custom env var prefix)
+
+Example:
+```javascript
+import adapter from '@sveltejs/adapter-node';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ kit: {
+ adapter: adapter({
+ out: 'build',
+ precompress: true,
+ envPrefix: 'TASKPLANER_'
+ })
+ }
+};
+
+export default config;
+```
+
+NOTE: Keep adapter-auto in devDependencies as fallback for non-Docker dev environments. adapter-node is what the Dockerfile will use.
+
+
+ - `npm run build` completes successfully
+ - `build/` directory is created with server files
+ - `ls build/` shows index.js, handler.js, etc.
+
+ svelte.config.js uses adapter-node, npm run build produces build/ directory
+
+
+
+ Task 2: Create Docker configuration files
+ Dockerfile, .dockerignore, docker-compose.yml
+
+1. Create .dockerignore with exclusions:
+ ```
+ # Dependencies - rebuild inside container for correct architecture
+ node_modules
+
+ # Build outputs
+ build
+ .svelte-kit
+
+ # Development/local data
+ data
+ .git
+ .gitignore
+ .env
+ .env.*
+
+ # Documentation
+ *.md
+
+ # IDE/Editor
+ .vscode
+ .idea
+
+ # Logs
+ *.log
+
+ # Docker files (prevent recursion)
+ Dockerfile
+ .dockerignore
+ docker-compose*.yml
+
+ # Planning docs
+ .planning
+ ```
+
+2. Create Dockerfile with multi-stage build:
+ ```dockerfile
+ # Stage 1: Build
+ FROM node:22-alpine AS builder
+ WORKDIR /app
+
+ # Copy package files for layer caching
+ COPY package*.json ./
+ RUN npm ci
+
+ # Copy source and build
+ COPY . .
+ RUN npm run build
+
+ # Remove dev dependencies for smaller production image
+ RUN npm prune --production
+
+ # Stage 2: Production
+ FROM node:22-alpine
+ WORKDIR /app
+
+ # Copy built app and production dependencies
+ COPY --from=builder /app/build build/
+ COPY --from=builder /app/node_modules node_modules/
+ COPY package.json .
+
+ # Create data directories and set ownership
+ # App expects /data/db for database and /data/uploads for images
+ RUN mkdir -p /data/db /data/uploads/originals /data/uploads/thumbnails \
+ && chown -R node:node /data /app
+
+ # Switch to non-root user for security
+ USER node
+
+ # Environment defaults
+ ENV NODE_ENV=production
+ ENV PORT=3000
+ ENV TASKPLANER_DATA_DIR=/data
+
+ EXPOSE 3000
+
+ # Health check using wget (available in Alpine, curl is not)
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
+
+ CMD ["node", "build"]
+ ```
+
+3. Create docker-compose.yml:
+ ```yaml
+ services:
+ taskplaner:
+ build: .
+ container_name: taskplaner
+ restart: unless-stopped
+ ports:
+ - "${PORT:-3000}:3000"
+ volumes:
+ - taskplaner_data:/data
+ environment:
+ - NODE_ENV=production
+ - PORT=3000
+ - TASKPLANER_DATA_DIR=/data
+ - ORIGIN=${ORIGIN:-http://localhost:3000}
+ - BODY_SIZE_LIMIT=10M
+ # Uncomment when behind reverse proxy (nginx/traefik):
+ # - PROTOCOL_HEADER=x-forwarded-proto
+ # - HOST_HEADER=x-forwarded-host
+ # - ADDRESS_HEADER=x-forwarded-for
+ # - XFF_DEPTH=1
+
+ volumes:
+ taskplaner_data:
+ ```
+
+IMPORTANT: The Dockerfile HEALTHCHECK references /health endpoint which will be created in Plan 02. The container will show "unhealthy" until Plan 02 adds that endpoint, but the app will still run.
+
+
+ - `docker build -t taskplaner .` completes without errors
+ - Image size is under 250MB: `docker images taskplaner`
+ - `docker-compose config` shows valid configuration
+
+ Dockerfile builds successfully, docker-compose.yml is valid YAML, .dockerignore excludes node_modules and build outputs
+
+
+
+ Task 3: Update data paths for environment variable configuration
+ src/lib/server/db/index.ts, src/lib/server/images/storage.ts
+
+1. Update src/lib/server/db/index.ts to use TASKPLANER_DATA_DIR:
+ ```typescript
+ import Database from 'better-sqlite3';
+ import { drizzle } from 'drizzle-orm/better-sqlite3';
+ import * as schema from './schema';
+ import { existsSync, mkdirSync } from 'fs';
+ import { dirname, join } from 'path';
+
+ // Data directory from env (Docker: /data, local: ./data)
+ const DATA_DIR = process.env.TASKPLANER_DATA_DIR || './data';
+ const DB_PATH = process.env.DATABASE_PATH || join(DATA_DIR, 'db', 'taskplaner.db');
+
+ // Ensure data directory exists
+ const dbDir = dirname(DB_PATH);
+ if (!existsSync(dbDir)) {
+ mkdirSync(dbDir, { recursive: true });
+ }
+
+ const sqlite = new Database(DB_PATH);
+
+ // Enable WAL mode for better concurrent read performance
+ sqlite.pragma('journal_mode = WAL');
+
+ export const db = drizzle(sqlite, { schema });
+
+ export { schema };
+ ```
+
+2. Update src/lib/server/images/storage.ts to use TASKPLANER_DATA_DIR:
+ ```typescript
+ import { mkdir, writeFile, unlink } from 'node:fs/promises';
+ import { join } from 'node:path';
+
+ // Data directory from env (Docker: /data, local: ./data)
+ const DATA_DIR = process.env.TASKPLANER_DATA_DIR || './data';
+
+ export const UPLOAD_DIR = join(DATA_DIR, 'uploads');
+ export const ORIGINALS_DIR = join(DATA_DIR, 'uploads', 'originals');
+ export const THUMBNAILS_DIR = join(DATA_DIR, 'uploads', 'thumbnails');
+
+ // Rest of file unchanged...
+ ```
+
+This makes paths configurable:
+- Local development: Uses ./data (default)
+- Docker: Uses /data (from TASKPLANER_DATA_DIR env var)
+- DATABASE_PATH still works as override for backward compatibility
+
+
+ - `npm run dev` still works (uses ./data default)
+ - `npm run build` completes without type errors
+ - Database and uploads work in development mode
+
+ Data paths read from TASKPLANER_DATA_DIR env var with ./data fallback for local development
+
+
+
+
+
+1. Build completes: `npm run build` produces build/ directory
+2. Docker build works: `docker build -t taskplaner .`
+3. Image is small: `docker images taskplaner` shows < 250MB
+4. Local dev still works: `npm run dev` uses ./data directory
+5. Compose is valid: `docker-compose config` shows no errors
+
+
+
+- svelte.config.js uses adapter-node with TASKPLANER_ prefix
+- Dockerfile uses multi-stage build with node:22-alpine
+- Container runs as non-root 'node' user
+- docker-compose.yml starts app with named volume for /data
+- Data paths are configurable via TASKPLANER_DATA_DIR
+- Local development still works with default ./data paths
+
+
+
diff --git a/.planning/phases/06-deployment/06-02-PLAN.md b/.planning/phases/06-deployment/06-02-PLAN.md
new file mode 100644
index 0000000..6c32656
--- /dev/null
+++ b/.planning/phases/06-deployment/06-02-PLAN.md
@@ -0,0 +1,384 @@
+---
+phase: 06-deployment
+plan: 02
+type: execute
+wave: 2
+depends_on: ["06-01"]
+files_modified:
+ - src/routes/health/+server.ts
+ - .env.example
+ - backup.sh
+ - README.md
+autonomous: true
+
+must_haves:
+ truths:
+ - "Health endpoint returns 200 when database is accessible"
+ - "Health endpoint returns 503 when database fails"
+ - "Environment variables are documented with examples"
+ - "Backup script creates timestamped archive of data volume"
+ artifacts:
+ - path: "src/routes/health/+server.ts"
+ provides: "Health check endpoint for Docker"
+ exports: ["GET"]
+ - path: ".env.example"
+ provides: "Environment variable documentation"
+ contains: "TASKPLANER_DATA_DIR"
+ - path: "backup.sh"
+ provides: "Volume backup script"
+ contains: "tar czf"
+ key_links:
+ - from: "Dockerfile"
+ to: "src/routes/health/+server.ts"
+ via: "HEALTHCHECK wget command"
+ pattern: "/health"
+ - from: "docker-compose.yml"
+ to: ".env.example"
+ via: "environment variable reference"
+ pattern: "ORIGIN"
+---
+
+
+Runtime configuration with health checks, environment documentation, and backup tooling
+
+Purpose: Complete the production deployment setup with health monitoring for Docker, clear documentation of configuration options, and a backup script for data preservation.
+
+Output: /health endpoint, .env.example template, backup.sh script, updated README with Docker instructions
+
+
+
+@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
+@/home/tho/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/06-deployment/06-RESEARCH.md
+@.planning/phases/06-deployment/06-01-SUMMARY.md
+
+Key from Plan 01:
+- Dockerfile has HEALTHCHECK pointing to /health
+- Data paths use TASKPLANER_DATA_DIR env var
+- docker-compose.yml uses named volume taskplaner_data
+
+
+
+
+
+ Task 1: Create health check endpoint
+ src/routes/health/+server.ts
+
+Create src/routes/health/+server.ts:
+
+```typescript
+import type { RequestHandler } from './$types';
+import { db } from '$lib/server/db';
+import { entries } from '$lib/server/db/schema';
+
+export const GET: RequestHandler = async () => {
+ try {
+ // Verify database connectivity with a simple query
+ db.select().from(entries).limit(1).all();
+
+ return new Response('ok', {
+ status: 200,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ } catch (error) {
+ console.error('Health check failed:', error);
+
+ return new Response('unhealthy', {
+ status: 503,
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ }
+};
+```
+
+The endpoint:
+- Returns 200 "ok" when database is accessible
+- Returns 503 "unhealthy" when database query fails
+- Logs errors for debugging but doesn't expose details
+- Uses simple text response (Docker just needs status code)
+
+
+ - `npm run dev` and visit http://localhost:5173/health returns "ok"
+ - Health endpoint returns 200 status code
+ - Database query is executed (visible in dev server logs on first request)
+
+ /health endpoint returns 200 with database connectivity check
+
+
+
+ Task 2: Create environment documentation and backup script
+ .env.example, backup.sh
+
+1. Create .env.example:
+ ```bash
+ # TaskPlaner Environment Configuration
+ # Copy to .env and customize for your deployment
+
+ # ============================================
+ # Server Configuration
+ # ============================================
+
+ # Port the server listens on (inside container)
+ # Map to host port via docker-compose ports setting
+ PORT=3000
+
+ # ============================================
+ # Data Storage
+ # ============================================
+
+ # Directory for database and uploads
+ # Docker: /data (must match volume mount)
+ # Local development: ./data
+ TASKPLANER_DATA_DIR=/data
+
+ # Optional: Direct database path override
+ # DATABASE_PATH=/data/db/taskplaner.db
+
+ # ============================================
+ # Production URL (REQUIRED for production)
+ # ============================================
+
+ # The full URL where users access the app
+ # Used for CSRF validation and generating absolute URLs
+ # Example: https://tasks.example.com
+ ORIGIN=http://localhost:3000
+
+ # ============================================
+ # Request Limits
+ # ============================================
+
+ # Maximum request body size
+ # Supports K, M, G suffixes
+ # Default: 512kb, recommended for images: 10M
+ BODY_SIZE_LIMIT=10M
+
+ # ============================================
+ # Reverse Proxy Configuration
+ # Uncomment when running behind nginx/traefik/etc
+ # ============================================
+
+ # Header containing original protocol (http/https)
+ # PROTOCOL_HEADER=x-forwarded-proto
+
+ # Header containing original host
+ # HOST_HEADER=x-forwarded-host
+
+ # Header containing original client IP
+ # ADDRESS_HEADER=x-forwarded-for
+
+ # Number of trusted proxies in front of app
+ # XFF_DEPTH=1
+ ```
+
+2. Create backup.sh:
+ ```bash
+ #!/bin/bash
+ # TaskPlaner Data Backup Script
+ # Creates a timestamped backup of the Docker volume
+
+ set -e
+
+ # Configuration (override via environment)
+ BACKUP_DIR="${BACKUP_DIR:-./backups}"
+ VOLUME_NAME="${VOLUME_NAME:-taskplaner_taskplaner_data}"
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
+ BACKUP_FILE="${BACKUP_DIR}/taskplaner_backup_${TIMESTAMP}.tar.gz"
+
+ # Create backup directory if needed
+ mkdir -p "$BACKUP_DIR"
+
+ echo "========================================="
+ echo "TaskPlaner Backup"
+ echo "========================================="
+ echo "Volume: $VOLUME_NAME"
+ echo "Output: $BACKUP_FILE"
+ echo ""
+
+ # Check if volume exists
+ if ! docker volume inspect "$VOLUME_NAME" > /dev/null 2>&1; then
+ echo "Error: Volume '$VOLUME_NAME' not found"
+ echo ""
+ echo "Available volumes:"
+ docker volume ls --format ' - {{.Name}}' | grep -i taskplaner || echo " (none with 'taskplaner' in name)"
+ echo ""
+ echo "Tip: Set VOLUME_NAME environment variable to use a different volume"
+ exit 1
+ fi
+
+ # Create backup using temporary Alpine container
+ echo "Creating backup..."
+ docker run --rm \
+ -v "${VOLUME_NAME}:/data:ro" \
+ -v "$(cd "$BACKUP_DIR" && pwd):/backup" \
+ alpine:latest \
+ tar czf "/backup/taskplaner_backup_${TIMESTAMP}.tar.gz" -C /data .
+
+ echo ""
+ echo "Backup complete!"
+ echo "File: $BACKUP_FILE"
+ echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)"
+ echo ""
+ echo "To restore: docker run --rm -v ${VOLUME_NAME}:/data -v \$(pwd)/${BACKUP_DIR}:/backup alpine tar xzf /backup/taskplaner_backup_${TIMESTAMP}.tar.gz -C /data"
+ ```
+
+3. Make backup.sh executable:
+ After creating the file, run: chmod +x backup.sh
+
+
+ - .env.example exists with all documented variables
+ - backup.sh exists and is executable: `ls -la backup.sh`
+ - backup.sh syntax is valid: `bash -n backup.sh`
+
+ .env.example documents all configuration options, backup.sh creates timestamped archive
+
+
+
+ Task 3: Add Docker deployment section to README
+ README.md
+
+Check if README.md exists. If it does, add a Docker Deployment section. If not, create a minimal README with the Docker section.
+
+Add this section (after any existing content, or as the main content):
+
+```markdown
+## Docker Deployment
+
+### Quick Start
+
+```bash
+# Build and start the container
+docker-compose up -d
+
+# View logs
+docker-compose logs -f
+
+# Stop the container
+docker-compose down
+```
+
+The application will be available at http://localhost:3000
+
+### Configuration
+
+Copy `.env.example` to `.env` and customize:
+
+```bash
+cp .env.example .env
+```
+
+Key settings:
+- `ORIGIN` - Required for production. Set to your public URL (e.g., `https://tasks.example.com`)
+- `BODY_SIZE_LIMIT` - Max upload size. Default: `512kb`, recommended: `10M`
+- `PORT` - Server port inside container. Default: `3000`
+
+### Behind a Reverse Proxy
+
+When running behind nginx, traefik, or similar, uncomment these in `.env`:
+
+```bash
+PROTOCOL_HEADER=x-forwarded-proto
+HOST_HEADER=x-forwarded-host
+ADDRESS_HEADER=x-forwarded-for
+XFF_DEPTH=1
+```
+
+### Data Persistence
+
+Data is stored in a Docker named volume (`taskplaner_data`). This includes:
+- SQLite database (`/data/db/taskplaner.db`)
+- Uploaded images (`/data/uploads/`)
+
+The volume persists across container restarts and updates.
+
+### Backup & Restore
+
+Create a backup:
+```bash
+./backup.sh
+```
+
+Backups are saved to `./backups/` with timestamps.
+
+Restore from backup:
+```bash
+# Stop the container first
+docker-compose down
+
+# Restore (replace TIMESTAMP with actual backup filename)
+docker run --rm \
+ -v taskplaner_taskplaner_data:/data \
+ -v $(pwd)/backups:/backup \
+ alpine tar xzf /backup/taskplaner_backup_TIMESTAMP.tar.gz -C /data
+
+# Start the container
+docker-compose up -d
+```
+
+### Health Check
+
+The container includes a health check at `/health`. View status:
+
+```bash
+docker-compose ps
+```
+
+A healthy container shows `(healthy)` in the status column.
+```
+
+If README.md exists, preserve existing content and add this as a new section.
+If README.md doesn't exist, create it with a title "# TaskPlaner" followed by this Docker section.
+
+
+ - README.md exists with Docker Deployment section
+ - Section includes Quick Start, Configuration, Backup instructions
+ - Commands are correct and copy-pasteable
+
+ README.md documents Docker deployment with quick start, configuration, and backup instructions
+
+
+
+
+
+1. Health endpoint works: `curl http://localhost:5173/health` returns "ok" (dev mode)
+2. Environment documented: `.env.example` has all configuration options
+3. Backup script works: `./backup.sh` (requires running container with volume)
+4. README complete: Docker section with quick start and backup instructions
+
+Full Docker verification (after both plans):
+```bash
+# Build and start
+docker-compose up -d
+
+# Check health
+docker-compose ps # Should show (healthy)
+curl http://localhost:3000/health # Should return "ok"
+
+# Verify data persistence
+# Add an entry via the UI, then:
+docker-compose down
+docker-compose up -d
+# Entry should still exist
+
+# Test backup
+./backup.sh
+ls -la backups/ # Should show timestamped tar.gz
+```
+
+
+
+- /health endpoint returns 200 when database accessible, 503 on failure
+- .env.example documents PORT, TASKPLANER_DATA_DIR, ORIGIN, BODY_SIZE_LIMIT, proxy headers
+- backup.sh creates timestamped tar.gz of data volume
+- README.md has Docker deployment section with quick start and backup instructions
+- Container shows "healthy" status in docker-compose ps
+
+
+