--- 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 After completion, create `.planning/phases/06-deployment/06-01-SUMMARY.md`