---
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: \\."
---

<objective>
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
</objective>

<execution_context>
@/home/tho/.claude/get-shit-done/workflows/execute-plan.md
@/home/tho/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.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
</context>

<tasks>

<task type="auto">
  <name>Task 1: Switch to adapter-node with environment prefix</name>
  <files>svelte.config.js, package.json</files>
  <action>
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.
  </action>
  <verify>
    - `npm run build` completes successfully
    - `build/` directory is created with server files
    - `ls build/` shows index.js, handler.js, etc.
  </verify>
  <done>svelte.config.js uses adapter-node, npm run build produces build/ directory</done>
</task>

<task type="auto">
  <name>Task 2: Create Docker configuration files</name>
  <files>Dockerfile, .dockerignore, docker-compose.yml</files>
  <action>
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.
  </action>
  <verify>
    - `docker build -t taskplaner .` completes without errors
    - Image size is under 250MB: `docker images taskplaner`
    - `docker-compose config` shows valid configuration
  </verify>
  <done>Dockerfile builds successfully, docker-compose.yml is valid YAML, .dockerignore excludes node_modules and build outputs</done>
</task>

<task type="auto">
  <name>Task 3: Update data paths for environment variable configuration</name>
  <files>src/lib/server/db/index.ts, src/lib/server/images/storage.ts</files>
  <action>
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
  </action>
  <verify>
    - `npm run dev` still works (uses ./data default)
    - `npm run build` completes without type errors
    - Database and uploads work in development mode
  </verify>
  <done>Data paths read from TASKPLANER_DATA_DIR env var with ./data fallback for local development</done>
</task>

</tasks>

<verification>
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
</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/06-deployment/06-01-SUMMARY.md`
</output>
