Files
taskplaner/.planning/phases/06-deployment/06-01-PLAN.md
Thomas Richter 80e4769221 docs(06): create phase plan
Phase 06: Deployment
- 2 plans in 2 waves
- Wave 1: Docker configuration (adapter-node, Dockerfile, docker-compose)
- Wave 2: Runtime configuration (health endpoint, env docs, backup script)
- Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 13:06:33 +01:00

9.2 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
06-deployment 01 execute 1
svelte.config.js
Dockerfile
.dockerignore
docker-compose.yml
true
truths artifacts key_links
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
path provides contains
svelte.config.js adapter-node configuration with TASKPLANER_ prefix adapter-node
path provides contains
Dockerfile Multi-stage build for production FROM node:22-alpine
path provides contains
.dockerignore Build context exclusions node_modules
path provides contains
docker-compose.yml Single-service compose with named volume taskplaner_data
from to via pattern
svelte.config.js adapter-node adapter import and configuration adapter-node
from to via pattern
Dockerfile docker-compose.yml build context 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

<execution_context> @/home/tho/.claude/get-shit-done/workflows/execute-plan.md @/home/tho/.claude/get-shit-done/templates/summary.md </execution_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
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 ```
  1. 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:

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"]
  1. Create docker-compose.yml:
    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

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