# Phase 6: Deployment - Research **Researched:** 2026-02-01 **Domain:** Docker containerization, SvelteKit production deployment **Confidence:** HIGH ## Summary This phase containerizes the TaskPlaner SvelteKit application for production deployment with Docker. The research confirms a well-established pattern for deploying SvelteKit with adapter-node: multi-stage Alpine builds, runtime environment variables, health checks via API routes, and named volumes for data persistence. The key technical considerations are: 1. Switching from adapter-auto to adapter-node for production Node.js server 2. Native module compilation (better-sqlite3, sharp) requiring npm install inside the container 3. SQLite WAL mode working correctly with Docker named volumes (single host, same filesystem) 4. Environment variable configuration via adapter-node's built-in support **Primary recommendation:** Use a two-stage Dockerfile with node:22-alpine, run npm install in the builder stage to compile native modules correctly, and configure all runtime behavior via environment variables with sensible defaults. ## Standard Stack The established libraries/tools for this domain: ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | @sveltejs/adapter-node | ^5.x | SvelteKit Node.js server | Official adapter, supports all env vars natively | | node:22-alpine | LTS | Docker base image | Small (~40MB vs 350MB+), includes 'node' user | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | dumb-init | ^1.2.5 | PID 1 init process | Optional: proper signal handling in containers | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | node:alpine | node:slim (debian) | Debian avoids musl issues, but 3x larger image | | Built-in signals | tini/dumb-init | adapter-node handles SIGTERM natively, may not need | **Installation:** ```bash npm install -D @sveltejs/adapter-node ``` ## Architecture Patterns ### Recommended Project Structure ``` project-root/ ├── Dockerfile # Multi-stage build ├── docker-compose.yml # Single-service compose ├── .dockerignore # Build context exclusions ├── .env.example # Environment variable template ├── backup.sh # Volume backup script ├── build/ # (gitignored) Production build output └── data/ # (gitignored) Local dev data ├── db/ │ └── taskplaner.db └── uploads/ ├── originals/ └── thumbnails/ ``` ### Pattern 1: Multi-Stage Dockerfile **What:** Separate build dependencies from runtime to minimize image size **When to use:** Always for production Docker deployments **Example:** ```dockerfile # Source: https://gist.github.com/aradalvand/04b2cad14b00e5ffe8ec96a3afbb34fb # Stage 1: Build FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build RUN npm prune --production # Stage 2: Production FROM node:22-alpine WORKDIR /app COPY --from=builder /app/build build/ COPY --from=builder /app/node_modules node_modules/ COPY package.json . USER node EXPOSE 3000 ENV NODE_ENV=production CMD ["node", "build"] ``` ### Pattern 2: SvelteKit Health Check Endpoint **What:** API route returning 200 for container health checks **When to use:** Docker HEALTHCHECK, Kubernetes probes, load balancer checks **Example:** ```typescript // src/routes/health/+server.ts // Source: https://svelte.dev/docs/kit/routing import type { RequestHandler } from './$types'; export const GET: RequestHandler = async () => { return new Response('ok', { status: 200 }); }; ``` ### Pattern 3: Runtime Environment Configuration **What:** Configure app behavior via environment variables at runtime **When to use:** All deployments - allows same image for different environments **Example:** ```bash # adapter-node reads these automatically # Source: https://svelte.dev/docs/kit/adapter-node PORT=3000 HOST=0.0.0.0 ORIGIN=http://localhost:3000 BODY_SIZE_LIMIT=10M # Reverse proxy headers (when behind nginx/traefik) PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host ADDRESS_HEADER=x-forwarded-for XFF_DEPTH=1 ``` ### Pattern 4: Named Volume for Data Persistence **What:** Docker-managed volume mounted to container data directory **When to use:** Any stateful data (database, uploads) **Example:** ```yaml # docker-compose.yml services: app: volumes: - taskplaner_data:/data volumes: taskplaner_data: ``` ### Anti-Patterns to Avoid - **Copying node_modules from host:** Native modules compiled for host OS won't work in Alpine container. Always run npm install inside Dockerfile. - **Bind mounts for production data:** Use named volumes for better portability and Docker management. - **Running as root:** Security risk. Use `USER node` in production stage. - **Hardcoded configuration:** Makes the image environment-specific. Use env vars. - **Installing dev dependencies in production:** Bloats image. Use `npm prune --production` or `npm ci --only=production`. ## Don't Hand-Roll Problems that look simple but have existing solutions: | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Environment variables | Custom config parser | adapter-node env vars | Built-in, documented, handles edge cases | | Graceful shutdown | Signal handlers | adapter-node SHUTDOWN_TIMEOUT | Built-in, handles request draining | | Health checks | Complex readiness probe | Simple /health route | Docker just needs HTTP 200 | | Process management | supervisor/pm2 | Docker restart policies | Container-native, simpler | | Logging to files | File appenders | stdout/stderr | Docker captures, aggregators consume | **Key insight:** adapter-node provides most production concerns out of the box. Don't add middleware or wrappers - use its environment variables. ## Common Pitfalls ### Pitfall 1: Native Module Architecture Mismatch **What goes wrong:** App crashes with "Cannot find module" or "symbol not found" errors for better-sqlite3 or sharp **Why it happens:** node_modules from macOS/Windows host copied to Linux container **How to avoid:** 1. Add `node_modules` to `.dockerignore` 2. Run `npm ci` inside Dockerfile builder stage 3. For sharp on Alpine, npm will download musl-compatible prebuilt binaries automatically (sharp >= 0.33) **Warning signs:** Errors mentioning `linuxmusl`, `glibc`, or module loading failures ### Pitfall 2: SQLite WAL File Permissions **What goes wrong:** "database is locked" or "read-only" errors **Why it happens:** SQLite creates `-wal` and `-shm` files with same permissions as main db file. If user/group mismatch, can't access. **How to avoid:** 1. Run container as `node` user (uid 1000) 2. Use named volumes (Docker manages permissions) 3. Ensure app creates data directories with correct ownership **Warning signs:** Works first time, fails on restart; "attempt to write a readonly database" ### Pitfall 3: ORIGIN Environment Variable Missing **What goes wrong:** CSRF errors on form submissions: "Cross-site POST form submissions are forbidden" **Why it happens:** SvelteKit validates Origin header against expected origin **How to avoid:** Always set `ORIGIN` env var to match how users access the app **Warning signs:** Forms work in development but fail in production ### Pitfall 4: Missing Reverse Proxy Headers **What goes wrong:** App sees internal Docker IPs instead of real client IPs; redirects go to wrong protocol/host **Why it happens:** Default behavior ignores X-Forwarded-* headers for security **How to avoid:** Set `PROTOCOL_HEADER`, `HOST_HEADER`, `ADDRESS_HEADER` only when behind trusted proxy **Warning signs:** Logs show 172.x.x.x instead of real IPs; HTTPS redirects to HTTP ### Pitfall 5: Volume Data Not Persisting **What goes wrong:** Data disappears after container restart **Why it happens:** Using anonymous volumes or not declaring volume in compose file **How to avoid:** 1. Declare named volume in `volumes:` section of docker-compose.yml 2. Use consistent mount path in container 3. Don't use `docker-compose down -v` unless intentionally deleting data **Warning signs:** Fresh database on each restart ### Pitfall 6: Health Check Curl Not Installed **What goes wrong:** Container marked unhealthy despite app running **Why it happens:** Alpine doesn't include curl by default **How to avoid:** Use wget (included in Alpine) or node for health checks **Warning signs:** Container in "unhealthy" state but app responds normally ## Code Examples Verified patterns from official sources: ### svelte.config.js with adapter-node ```javascript // Source: https://svelte.dev/docs/kit/adapter-node 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; ``` ### Complete Dockerfile ```dockerfile # Stage 1: Build FROM node:22-alpine AS builder WORKDIR /app # Copy package files first for layer caching COPY package*.json ./ RUN npm ci # Copy source and build COPY . . RUN npm run build # Remove dev dependencies RUN npm prune --production # Stage 2: Production FROM node:22-alpine # Create app directory 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 directory and set ownership RUN mkdir -p /data/db /data/uploads/originals /data/uploads/thumbnails \ && chown -R node:node /data /app # Switch to non-root user 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) 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"] ``` ### docker-compose.yml ```yaml # Source: https://docs.docker.com/reference/compose-file/ 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} # Uncomment when behind reverse proxy: # - PROTOCOL_HEADER=x-forwarded-proto # - HOST_HEADER=x-forwarded-host # - ADDRESS_HEADER=x-forwarded-for # - XFF_DEPTH=1 healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 10s volumes: taskplaner_data: ``` ### .dockerignore ``` # Source: https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/ # Dependencies - rebuild inside container node_modules # Build outputs build .svelte-kit # Development files .git .gitignore .env .env.* *.md *.log # IDE/Editor .vscode .idea # Docker files (prevent recursion) Dockerfile .dockerignore docker-compose*.yml # Test files *.test.ts *.spec.ts coverage ``` ### .env.example ```bash # TaskPlaner Environment Configuration # Copy to .env and customize for your deployment # === Server Configuration === # Port the server listens on (inside container, map with docker-compose ports) PORT=3000 # === Data Storage === # Directory for database and uploads (must match volume mount) TASKPLANER_DATA_DIR=/data # === Production URL === # Required: The full URL where users access the app # Used for CSRF validation and generating absolute URLs ORIGIN=http://localhost:3000 # === Reverse Proxy (uncomment when behind nginx/traefik) === # PROTOCOL_HEADER=x-forwarded-proto # HOST_HEADER=x-forwarded-host # ADDRESS_HEADER=x-forwarded-for # XFF_DEPTH=1 # === Request Limits === # Maximum request body size (default: 512kb) # Supports K, M, G suffixes BODY_SIZE_LIMIT=10M # === Logging === # Log level: debug, info, warn, error # TASKPLANER_LOG_LEVEL=info ``` ### backup.sh ```bash #!/bin/bash # Source: https://docs.docker.com/engine/storage/volumes/ # Backup TaskPlaner data volume set -e BACKUP_DIR="${BACKUP_DIR:-./backups}" CONTAINER_NAME="${CONTAINER_NAME:-taskplaner}" VOLUME_NAME="${VOLUME_NAME:-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 "Backing up TaskPlaner data..." echo "Volume: $VOLUME_NAME" echo "Output: $BACKUP_FILE" # Create backup using temporary container docker run --rm \ -v "${VOLUME_NAME}:/data:ro" \ -v "$(pwd)/${BACKUP_DIR}:/backup" \ alpine:latest \ tar czf "/backup/taskplaner_backup_${TIMESTAMP}.tar.gz" -C /data . echo "Backup complete: $BACKUP_FILE" echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)" ``` ### Health Check Endpoint ```typescript // src/routes/health/+server.ts 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 db.select().from(entries).limit(1).all(); return new Response('ok', { status: 200, headers: { 'Content-Type': 'text/plain' } }); } catch (error) { return new Response('unhealthy', { status: 503, headers: { 'Content-Type': 'text/plain' } }); } }; ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | adapter-auto | adapter-node for Docker | SvelteKit 2.x | Explicit Node.js server, env var support | | COPY node_modules | npm ci in Dockerfile | Always best practice | Correct native module binaries | | curl in healthcheck | wget (Alpine native) | Alpine best practice | No extra packages needed | | Root user | node:node (uid 1000) | Security standard | Reduced attack surface | | JSON config files | Environment variables | 12-factor app | Same image, different configs | **Deprecated/outdated:** - `adapter-node@1.x`: Use 5.x for SvelteKit 2 compatibility - Installing sharp with `--platform` flags: sharp 0.33+ auto-detects musl - Manual signal handling: adapter-node handles SIGTERM/SIGINT natively ## Open Questions Things that couldn't be fully resolved: 1. **JSON logging integration** - What we know: Pino is the standard for JSON logging in Node.js - What's unclear: Pino has bundling issues with SvelteKit production builds (Worker Threads) - Recommendation: Defer structured logging to a future enhancement. Console.log output goes to stdout and Docker captures it. Can add pino later if needed. 2. **Drizzle migrations in production** - What we know: Currently using `drizzle-kit push` for development - What's unclear: Best practice for running migrations in Docker container - Recommendation: For single-user app, continue using push mode via db initialization in hooks.server.ts. Schema is simple enough. Could add migration script later if needed. 3. **Specific extended environment variables** - What we know: CONTEXT.md says "extended config scope" beyond PORT/DATA_DIR - What's unclear: Exactly which additional env vars users need - Recommendation: Start with essentials (PORT, DATA_DIR, ORIGIN, BODY_SIZE_LIMIT, log level). Add more based on user feedback. ## Sources ### Primary (HIGH confidence) - [SvelteKit adapter-node documentation](https://svelte.dev/docs/kit/adapter-node) - Environment variables, configuration, graceful shutdown - [Docker official volumes documentation](https://docs.docker.com/engine/storage/volumes/) - Named volumes, backup procedures - [Docker Compose services documentation](https://docs.docker.com/reference/compose-file/services/) - Health checks, configuration - [Sharp installation documentation](https://sharp.pixelplumbing.com/install/) - Cross-platform, musl support ### Secondary (MEDIUM confidence) - [SvelteKit Dockerfile gist by aradalvand](https://gist.github.com/aradalvand/04b2cad14b00e5ffe8ec96a3afbb34fb) - Multi-stage build pattern - [Snyk Node.js Docker best practices](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) - Security, .dockerignore - [Node.js docker-node best practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md) - Non-root user, Alpine considerations ### Tertiary (LOW confidence) - Various Medium/DEV articles on SvelteKit Docker deployment - General patterns confirmed by official docs ## Metadata **Confidence breakdown:** - Standard stack: HIGH - adapter-node is official, well-documented - Architecture: HIGH - Multi-stage builds, named volumes are established Docker patterns - Pitfalls: HIGH - Native modules, SQLite permissions are well-known issues with documented solutions - Code examples: HIGH - Based on official documentation **Research date:** 2026-02-01 **Valid until:** 2026-03-01 (30 days - Docker/SvelteKit patterns are stable)