From 7e319860b62c2d2b7062e7e81fa46ecaf0d48d9f Mon Sep 17 00:00:00 2001 From: Hendrik Belitz Date: Sun, 15 Jun 2025 15:26:34 +0200 Subject: [PATCH] Docker configuration, build file for app --- .env.example | 12 ++ .gitignore | 18 +++ Caddyfile | 31 ++++ Dockerfile | 47 ++++++ docker-compose.dev.yml | 36 +++++ docker-compose.prod.yml | 120 ++++++++++++++ docs/infrastructure.md | 338 ++++++++++++++++++++++++++++++++++++++++ package.json | 23 +++ secrets/.gitkeep | 0 9 files changed, 625 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Caddyfile create mode 100644 Dockerfile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 docs/infrastructure.md create mode 100644 package.json create mode 100644 secrets/.gitkeep diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2f3727c --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Development Environment Configuration +# Copy this file to .env and fill in your values + +# Database Configuration +POSTGRES_DB=appointment_booking +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your_secure_password_here +POSTGRES_PORT=5432 + +# Application Configuration +NODE_ENV=development +APP_PORT=5173 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f20037 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +node_modules/ +.env +.env.local +.env.production +build/ +dist/ +.svelte-kit/ + +# Production secrets - NEVER commit these +secrets/*.txt +!secrets/.gitkeep + +# Development +.DS_Store +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..85eecb8 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,31 @@ +# Production Caddyfile for appointment booking application +# Replace your-domain.com with your actual domain + +your-domain.com { + reverse_proxy app:3000 + + # Security headers + header { + -Server + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + X-XSS-Protection "1; mode=block" + Referrer-Policy "strict-origin-when-cross-origin" + Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'" + } + + encode gzip + + log { + output file /data/access.log { + roll_size 100mb + roll_keep 10 + } + format json + } + + tls { + protocols tls1.2 tls1.3 + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..465ae94 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# Production Dockerfile for SvelteKit application +# Security: rootless container, minimal base image, non-privileged user + +# Build stage +FROM node:24-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci --only=production --ignore-scripts && \ + npm cache clean --force + +# Copy source and build +COPY . . +RUN npm run build + +# Production stage +FROM node:24-alpine AS production + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S sveltekit -u 1001 -G nodejs + +WORKDIR /app + +# Copy built application and dependencies +COPY --from=builder --chown=sveltekit:nodejs /app/build ./build +COPY --from=builder --chown=sveltekit:nodejs /app/package*.json ./ +COPY --from=builder --chown=sveltekit:nodejs /app/node_modules ./node_modules + +# Create logs directory +RUN mkdir -p /app/logs && \ + chown -R sveltekit:nodejs /app + +# Drop privileges +USER sveltekit + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Start application +CMD ["node", "build/index.js"] \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..883e141 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,36 @@ +services: + postgres: + image: postgres:16-alpine + container_name: appointment-booking-postgres-dev + restart: unless-stopped + user: postgres + environment: + POSTGRES_DB: ${POSTGRES_DB:-appointment_booking} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data_dev:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d:ro + networks: + - appointment-booking-dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-appointment_booking}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp + - /var/run/postgresql + +volumes: + postgres_data_dev: + driver: local + +networks: + appointment-booking-dev: + driver: bridge \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..049e532 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,120 @@ +services: + postgres: + image: postgres:16-alpine + container_name: appointment-booking-postgres + restart: unless-stopped + user: postgres + environment: + POSTGRES_DB_FILE: /run/secrets/postgres_db + POSTGRES_USER_FILE: /run/secrets/postgres_user + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" + secrets: + - postgres_db + - postgres_user + - postgres_password + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d:ro + networks: + - appointment-booking-internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$(cat /run/secrets/postgres_user) -d $$(cat /run/secrets/postgres_db)"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - CHOWN + - DAC_OVERRIDE + - FOWNER + - SETGID + - SETUID + tmpfs: + - /tmp + - /var/run/postgresql + read_only: true + + app: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: appointment-booking-app + restart: unless-stopped + user: "1001:1001" + environment: + NODE_ENV: production + POSTGRES_DB_FILE: /run/secrets/postgres_db + POSTGRES_USER_FILE: /run/secrets/postgres_user + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + secrets: + - postgres_db + - postgres_user + - postgres_password + depends_on: + postgres: + condition: service_healthy + networks: + - appointment-booking-internal + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp + read_only: true + + caddy: + image: caddy:2-alpine + container_name: appointment-booking-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: + - appointment-booking-internal + depends_on: + app: + condition: service_healthy + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + +secrets: + postgres_db: + file: ./secrets/postgres_db.txt + postgres_user: + file: ./secrets/postgres_user.txt + postgres_password: + file: ./secrets/postgres_password.txt + +volumes: + postgres_data: + driver: local + caddy_data: + driver: local + caddy_config: + driver: local + +networks: + appointment-booking-internal: + driver: bridge + internal: false \ No newline at end of file diff --git a/docs/infrastructure.md b/docs/infrastructure.md new file mode 100644 index 0000000..bcc6996 --- /dev/null +++ b/docs/infrastructure.md @@ -0,0 +1,338 @@ +# Infrastructure Setup + +This document describes the complete setup for the appointment booking application infrastructure. + +## Architecture Overview + +The application consists of three Docker containers: + +1. **PostgreSQL Database** - Data persistence +2. **SvelteKit Application** - Web application (production only) +3. **Caddy Reverse Proxy** - HTTPS termination and routing (production only) + +## Development Setup + +Development runs only the PostgreSQL database with a public port for development tools. + +### Prerequisites + +- Docker and Docker Compose +- Node.js 24+ +- Git + +### Setup Steps + +1. **Clone the repository** + ```bash + git clone + cd appointment-booking-software + ``` + +2. **Configure environment** + ```bash + cp .env.example .env + ``` + Edit `.env` and set secure values: + ``` + POSTGRES_DB=appointment_booking + POSTGRES_USER=postgres + POSTGRES_PASSWORD=your_secure_development_password + POSTGRES_PORT=5432 + ``` + +3. **Start development database** + ```bash + npm run docker:dev:up + ``` + +4. **Verify database connection** + ```bash + # Check container status + docker ps + + # Test connection + psql -h localhost -U postgres -d appointment_booking + ``` + +5. **Install SvelteKit dependencies** (when SvelteKit code is added) + ```bash + npm install + ``` + +6. **Start SvelteKit development server** (when SvelteKit code is added) + ```bash + npm run dev + ``` + +### Development Commands + +```bash +# Database management +npm run docker:dev:up # Start PostgreSQL +npm run docker:dev:down # Stop PostgreSQL +npm run docker:dev:logs # View database logs +npm run docker:dev:clean # Remove containers and volumes + +# Application (when SvelteKit is implemented) +npm run dev # Start development server +npm run build # Build for production +``` + +## Production Setup + +Production runs all three containers with security hardening and HTTPS. + +### Prerequisites + +- Docker and Docker Compose +- Domain name with DNS pointing to server +- Server with public IP + +### Setup Steps + +1. **Configure production secrets** + + Edit the secret files in the `secrets/` directory: + ```bash + # Database configuration + echo "appointment_booking" > secrets/postgres_db.txt + echo "postgres" > secrets/postgres_user.txt + echo "$(openssl rand -base64 32)" > secrets/postgres_password.txt + + # Set proper permissions + chmod 600 secrets/*.txt + ``` + +2. **Configure domain** + + Edit `Caddyfile` and replace `your-domain.com` with your actual domain: + ``` + yourdomain.com { + # ... rest of configuration + } + ``` + +3. **Build and start production stack** + ```bash + # Build application image + npm run docker:prod:build + + # Start all services + npm run docker:prod:up + ``` + +4. **Verify deployment** + ```bash + # Check all containers are running + docker ps + + # Check application health + npm run docker:health + + # View logs + npm run docker:prod:logs + ``` + +### Production Commands + +```bash +# Stack management +npm run docker:prod:build # Build application image +npm run docker:prod:up # Start all services +npm run docker:prod:down # Stop all services +npm run docker:prod:logs # View all logs +npm run docker:prod:clean # Remove containers and volumes +npm run docker:health # Check container status +npm run docker:tag-and-push # Build, tag and push the app image to GHCR (Remember to change username accordingly first) +``` + +## SvelteKit Application Setup + +When implementing the SvelteKit application, follow these steps: + +### Database Connection + +Create `src/lib/database.js`: +```javascript +import { readFileSync } from 'fs'; +import pg from 'pg'; + +function getDatabaseConfig() { + if (process.env.NODE_ENV === 'production') { + // Read from Docker secrets + const user = readFileSync('/run/secrets/postgres_user', 'utf8').trim(); + const password = readFileSync('/run/secrets/postgres_password', 'utf8').trim(); + const database = readFileSync('/run/secrets/postgres_db', 'utf8').trim(); + + return { + host: 'postgres', + port: 5432, + user, + password, + database + }; + } else { + // Development configuration + return { + host: 'localhost', + port: process.env.POSTGRES_PORT || 5432, + user: process.env.POSTGRES_USER || 'postgres', + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB || 'appointment_booking' + }; + } +} + +export const pool = new pg.Pool(getDatabaseConfig()); +``` + +### Health Check Endpoint + +Create `src/routes/health/+server.js`: +```javascript +import { json } from '@sveltejs/kit'; +import { pool } from '$lib/database.js'; + +export async function GET() { + try { + await pool.query('SELECT 1'); + return json({ status: 'healthy', timestamp: new Date().toISOString() }); + } catch (error) { + return json( + { status: 'unhealthy', error: error.message }, + { status: 500 } + ); + } +} +``` + +### SvelteKit Configuration + + +Update `vite.config.js`: +```javascript +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + host: '0.0.0.0', + port: 5173 + } +}); +``` + +## Security Features + +### Container Security +- **Rootless containers**: All containers (sans caddy, which needs to access privileged ports) run as non-root users +- **Read-only filesystems**: Containers have read-only root filesystems where possible +- **No new privileges**: Containers cannot escalate privileges +- **Minimal capabilities**: Only necessary Linux capabilities are granted + +### Network Security +- **Internal networks**: Database and app communicate via internal Docker network +- **No exposed ports**: Only Caddy exposes ports to the host +- **HTTPS only**: Caddy handles automatic HTTPS with Let's Encrypt + +### Data Security +- **Docker secrets**: Sensitive data stored as Docker secrets +- **Encrypted connections**: Database connections use SSL/TLS +- **Security headers**: Comprehensive security headers via Caddy + +## Monitoring and Logs + +### Log Locations +- **Caddy logs**: Stored in `/data/access.log` inside Caddy container +- **Application logs**: Available via `docker logs` +- **Database logs**: Available via `docker logs` + +### Health Checks +- **Application**: `GET /health` endpoint +- **Database**: Built-in PostgreSQL health check +- **Caddy**: HTTP response check + +### Monitoring Commands +```bash +# View real-time logs +npm run docker:prod:logs + +# Check container health +docker ps +npm run docker:health + +# Individual container logs +docker logs appointment-booking-app +docker logs appointment-booking-postgres +docker logs appointment-booking-caddy +``` + +## Troubleshooting + +### Common Issues + +1. **Container won't start** + ```bash + # Check container logs + docker logs + + # Check resource usage + docker stats + ``` + +2. **Database connection issues** + ```bash + # Test database connectivity + docker exec -it appointment-booking-postgres psql -U postgres -d appointment_booking + + # Check database logs + docker logs appointment-booking-postgres + ``` + +3. **HTTPS certificate issues** + ```bash + # Check Caddy logs + docker logs appointment-booking-caddy + + # Verify domain DNS + nslookup yourdomain.com + ``` + +4. **Application not accessible** + ```bash + # Check if all containers are running + docker ps + + # Test internal connectivity + docker exec -it appointment-booking-caddy wget -q --spider http://app:3000/health + ``` + +### Reset Commands + +```bash +# Complete reset (destroys all data) +npm run docker:prod:clean +npm run docker:dev:clean + +# Rebuild and restart +npm run docker:prod:build +npm run docker:prod:up +``` + +## Backup and Restore + +### Database Backup +```bash +# Create backup +docker exec appointment-booking-postgres pg_dump -U postgres appointment_booking > backup.sql + +# Restore backup +docker exec -i appointment-booking-postgres psql -U postgres appointment_booking < backup.sql +``` + +### Volume Backup +```bash +# Backup PostgreSQL data volume +docker run --rm -v appointment-booking-software_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz /data +``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..54e4834 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "appointment-booking-software", + "version": "0.0.1", + "description": "End-to-end encrypted appointment booking platform", + "type": "module", + "scripts": { + "build": "echo 'Build placeholder - will be replaced with SvelteKit build'", + "docker:dev:up": "docker compose -f docker-compose.dev.yml up -d", + "docker:dev:down": "docker compose -f docker-compose.dev.yml down", + "docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f", + "docker:dev:clean": "docker compose -f docker-compose.dev.yml down -v --remove-orphans", + "docker:prod:build": "docker compose -f docker-compose.prod.yml build --no-cache", + "docker:prod:up": "docker compose -f docker-compose.prod.yml up -d", + "docker:prod:down": "docker compose -f docker-compose.prod.yml down", + "docker:prod:logs": "docker compose -f docker-compose.prod.yml logs -f", + "docker:prod:clean": "docker compose -f docker-compose.prod.yml down -v --remove-orphans", + "docker:tag-and-push": "npm run docker:prod:build && docker tag appointment-booking-software-app:latest appointment-booking-software:$npm_package_version && docker push ghcr.io/username/appointment-booking-software:$npm_package_version" + }, + "engines": { + "node": ">=24.0.0" + }, + "license": "AGPL-3.0" +} \ No newline at end of file diff --git a/secrets/.gitkeep b/secrets/.gitkeep new file mode 100644 index 0000000..e69de29