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>
This commit is contained in:
320
.planning/phases/06-deployment/06-01-PLAN.md
Normal file
320
.planning/phases/06-deployment/06-01-PLAN.md
Normal file
@@ -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: \\."
|
||||
---
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user