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:
@@ -122,11 +122,11 @@ Plans:
|
|||||||
3. Data persists across container restarts via named volumes
|
3. Data persists across container restarts via named volumes
|
||||||
4. Single docker-compose.yml starts the entire application
|
4. Single docker-compose.yml starts the entire application
|
||||||
5. Backup of data directory preserves all entries and images
|
5. Backup of data directory preserves all entries and images
|
||||||
**Plans**: TBD
|
**Plans**: 2 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 06-01: TBD
|
- [ ] 06-01-PLAN.md — Docker configuration with adapter-node, Dockerfile, and docker-compose.yml
|
||||||
- [ ] 06-02: TBD
|
- [ ] 06-02-PLAN.md — Health endpoint, environment documentation, and backup script
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
384
.planning/phases/06-deployment/06-02-PLAN.md
Normal file
384
.planning/phases/06-deployment/06-02-PLAN.md
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
---
|
||||||
|
phase: 06-deployment
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["06-01"]
|
||||||
|
files_modified:
|
||||||
|
- src/routes/health/+server.ts
|
||||||
|
- .env.example
|
||||||
|
- backup.sh
|
||||||
|
- README.md
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Health endpoint returns 200 when database is accessible"
|
||||||
|
- "Health endpoint returns 503 when database fails"
|
||||||
|
- "Environment variables are documented with examples"
|
||||||
|
- "Backup script creates timestamped archive of data volume"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/routes/health/+server.ts"
|
||||||
|
provides: "Health check endpoint for Docker"
|
||||||
|
exports: ["GET"]
|
||||||
|
- path: ".env.example"
|
||||||
|
provides: "Environment variable documentation"
|
||||||
|
contains: "TASKPLANER_DATA_DIR"
|
||||||
|
- path: "backup.sh"
|
||||||
|
provides: "Volume backup script"
|
||||||
|
contains: "tar czf"
|
||||||
|
key_links:
|
||||||
|
- from: "Dockerfile"
|
||||||
|
to: "src/routes/health/+server.ts"
|
||||||
|
via: "HEALTHCHECK wget command"
|
||||||
|
pattern: "/health"
|
||||||
|
- from: "docker-compose.yml"
|
||||||
|
to: ".env.example"
|
||||||
|
via: "environment variable reference"
|
||||||
|
pattern: "ORIGIN"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Runtime configuration with health checks, environment documentation, and backup tooling
|
||||||
|
|
||||||
|
Purpose: Complete the production deployment setup with health monitoring for Docker, clear documentation of configuration options, and a backup script for data preservation.
|
||||||
|
|
||||||
|
Output: /health endpoint, .env.example template, backup.sh script, updated README with Docker instructions
|
||||||
|
</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
|
||||||
|
@.planning/phases/06-deployment/06-01-SUMMARY.md
|
||||||
|
|
||||||
|
Key from Plan 01:
|
||||||
|
- Dockerfile has HEALTHCHECK pointing to /health
|
||||||
|
- Data paths use TASKPLANER_DATA_DIR env var
|
||||||
|
- docker-compose.yml uses named volume taskplaner_data
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create health check endpoint</name>
|
||||||
|
<files>src/routes/health/+server.ts</files>
|
||||||
|
<action>
|
||||||
|
Create src/routes/health/+server.ts:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 with a simple query
|
||||||
|
db.select().from(entries).limit(1).all();
|
||||||
|
|
||||||
|
return new Response('ok', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Health check failed:', error);
|
||||||
|
|
||||||
|
return new Response('unhealthy', {
|
||||||
|
status: 503,
|
||||||
|
headers: { 'Content-Type': 'text/plain' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The endpoint:
|
||||||
|
- Returns 200 "ok" when database is accessible
|
||||||
|
- Returns 503 "unhealthy" when database query fails
|
||||||
|
- Logs errors for debugging but doesn't expose details
|
||||||
|
- Uses simple text response (Docker just needs status code)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- `npm run dev` and visit http://localhost:5173/health returns "ok"
|
||||||
|
- Health endpoint returns 200 status code
|
||||||
|
- Database query is executed (visible in dev server logs on first request)
|
||||||
|
</verify>
|
||||||
|
<done>/health endpoint returns 200 with database connectivity check</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create environment documentation and backup script</name>
|
||||||
|
<files>.env.example, backup.sh</files>
|
||||||
|
<action>
|
||||||
|
1. Create .env.example:
|
||||||
|
```bash
|
||||||
|
# TaskPlaner Environment Configuration
|
||||||
|
# Copy to .env and customize for your deployment
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Server Configuration
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Port the server listens on (inside container)
|
||||||
|
# Map to host port via docker-compose ports setting
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Data Storage
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Directory for database and uploads
|
||||||
|
# Docker: /data (must match volume mount)
|
||||||
|
# Local development: ./data
|
||||||
|
TASKPLANER_DATA_DIR=/data
|
||||||
|
|
||||||
|
# Optional: Direct database path override
|
||||||
|
# DATABASE_PATH=/data/db/taskplaner.db
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Production URL (REQUIRED for production)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# The full URL where users access the app
|
||||||
|
# Used for CSRF validation and generating absolute URLs
|
||||||
|
# Example: https://tasks.example.com
|
||||||
|
ORIGIN=http://localhost:3000
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Request Limits
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Maximum request body size
|
||||||
|
# Supports K, M, G suffixes
|
||||||
|
# Default: 512kb, recommended for images: 10M
|
||||||
|
BODY_SIZE_LIMIT=10M
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Reverse Proxy Configuration
|
||||||
|
# Uncomment when running behind nginx/traefik/etc
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Header containing original protocol (http/https)
|
||||||
|
# PROTOCOL_HEADER=x-forwarded-proto
|
||||||
|
|
||||||
|
# Header containing original host
|
||||||
|
# HOST_HEADER=x-forwarded-host
|
||||||
|
|
||||||
|
# Header containing original client IP
|
||||||
|
# ADDRESS_HEADER=x-forwarded-for
|
||||||
|
|
||||||
|
# Number of trusted proxies in front of app
|
||||||
|
# XFF_DEPTH=1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create backup.sh:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# TaskPlaner Data Backup Script
|
||||||
|
# Creates a timestamped backup of the Docker volume
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration (override via environment)
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||||||
|
VOLUME_NAME="${VOLUME_NAME:-taskplaner_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 "========================================="
|
||||||
|
echo "TaskPlaner Backup"
|
||||||
|
echo "========================================="
|
||||||
|
echo "Volume: $VOLUME_NAME"
|
||||||
|
echo "Output: $BACKUP_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if volume exists
|
||||||
|
if ! docker volume inspect "$VOLUME_NAME" > /dev/null 2>&1; then
|
||||||
|
echo "Error: Volume '$VOLUME_NAME' not found"
|
||||||
|
echo ""
|
||||||
|
echo "Available volumes:"
|
||||||
|
docker volume ls --format ' - {{.Name}}' | grep -i taskplaner || echo " (none with 'taskplaner' in name)"
|
||||||
|
echo ""
|
||||||
|
echo "Tip: Set VOLUME_NAME environment variable to use a different volume"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create backup using temporary Alpine container
|
||||||
|
echo "Creating backup..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "${VOLUME_NAME}:/data:ro" \
|
||||||
|
-v "$(cd "$BACKUP_DIR" && pwd):/backup" \
|
||||||
|
alpine:latest \
|
||||||
|
tar czf "/backup/taskplaner_backup_${TIMESTAMP}.tar.gz" -C /data .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Backup complete!"
|
||||||
|
echo "File: $BACKUP_FILE"
|
||||||
|
echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
echo "To restore: docker run --rm -v ${VOLUME_NAME}:/data -v \$(pwd)/${BACKUP_DIR}:/backup alpine tar xzf /backup/taskplaner_backup_${TIMESTAMP}.tar.gz -C /data"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Make backup.sh executable:
|
||||||
|
After creating the file, run: chmod +x backup.sh
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- .env.example exists with all documented variables
|
||||||
|
- backup.sh exists and is executable: `ls -la backup.sh`
|
||||||
|
- backup.sh syntax is valid: `bash -n backup.sh`
|
||||||
|
</verify>
|
||||||
|
<done>.env.example documents all configuration options, backup.sh creates timestamped archive</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Add Docker deployment section to README</name>
|
||||||
|
<files>README.md</files>
|
||||||
|
<action>
|
||||||
|
Check if README.md exists. If it does, add a Docker Deployment section. If not, create a minimal README with the Docker section.
|
||||||
|
|
||||||
|
Add this section (after any existing content, or as the main content):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start the container
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop the container
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at http://localhost:3000
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and customize:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Key settings:
|
||||||
|
- `ORIGIN` - Required for production. Set to your public URL (e.g., `https://tasks.example.com`)
|
||||||
|
- `BODY_SIZE_LIMIT` - Max upload size. Default: `512kb`, recommended: `10M`
|
||||||
|
- `PORT` - Server port inside container. Default: `3000`
|
||||||
|
|
||||||
|
### Behind a Reverse Proxy
|
||||||
|
|
||||||
|
When running behind nginx, traefik, or similar, uncomment these in `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROTOCOL_HEADER=x-forwarded-proto
|
||||||
|
HOST_HEADER=x-forwarded-host
|
||||||
|
ADDRESS_HEADER=x-forwarded-for
|
||||||
|
XFF_DEPTH=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Persistence
|
||||||
|
|
||||||
|
Data is stored in a Docker named volume (`taskplaner_data`). This includes:
|
||||||
|
- SQLite database (`/data/db/taskplaner.db`)
|
||||||
|
- Uploaded images (`/data/uploads/`)
|
||||||
|
|
||||||
|
The volume persists across container restarts and updates.
|
||||||
|
|
||||||
|
### Backup & Restore
|
||||||
|
|
||||||
|
Create a backup:
|
||||||
|
```bash
|
||||||
|
./backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Backups are saved to `./backups/` with timestamps.
|
||||||
|
|
||||||
|
Restore from backup:
|
||||||
|
```bash
|
||||||
|
# Stop the container first
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Restore (replace TIMESTAMP with actual backup filename)
|
||||||
|
docker run --rm \
|
||||||
|
-v taskplaner_taskplaner_data:/data \
|
||||||
|
-v $(pwd)/backups:/backup \
|
||||||
|
alpine tar xzf /backup/taskplaner_backup_TIMESTAMP.tar.gz -C /data
|
||||||
|
|
||||||
|
# Start the container
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
The container includes a health check at `/health`. View status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
A healthy container shows `(healthy)` in the status column.
|
||||||
|
```
|
||||||
|
|
||||||
|
If README.md exists, preserve existing content and add this as a new section.
|
||||||
|
If README.md doesn't exist, create it with a title "# TaskPlaner" followed by this Docker section.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
- README.md exists with Docker Deployment section
|
||||||
|
- Section includes Quick Start, Configuration, Backup instructions
|
||||||
|
- Commands are correct and copy-pasteable
|
||||||
|
</verify>
|
||||||
|
<done>README.md documents Docker deployment with quick start, configuration, and backup instructions</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Health endpoint works: `curl http://localhost:5173/health` returns "ok" (dev mode)
|
||||||
|
2. Environment documented: `.env.example` has all configuration options
|
||||||
|
3. Backup script works: `./backup.sh` (requires running container with volume)
|
||||||
|
4. README complete: Docker section with quick start and backup instructions
|
||||||
|
|
||||||
|
Full Docker verification (after both plans):
|
||||||
|
```bash
|
||||||
|
# Build and start
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
docker-compose ps # Should show (healthy)
|
||||||
|
curl http://localhost:3000/health # Should return "ok"
|
||||||
|
|
||||||
|
# Verify data persistence
|
||||||
|
# Add an entry via the UI, then:
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d
|
||||||
|
# Entry should still exist
|
||||||
|
|
||||||
|
# Test backup
|
||||||
|
./backup.sh
|
||||||
|
ls -la backups/ # Should show timestamped tar.gz
|
||||||
|
```
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- /health endpoint returns 200 when database accessible, 503 on failure
|
||||||
|
- .env.example documents PORT, TASKPLANER_DATA_DIR, ORIGIN, BODY_SIZE_LIMIT, proxy headers
|
||||||
|
- backup.sh creates timestamped tar.gz of data volume
|
||||||
|
- README.md has Docker deployment section with quick start and backup instructions
|
||||||
|
- Container shows "healthy" status in docker-compose ps
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/06-deployment/06-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user