Skip to main content

Deployment Guide

This guide covers the complete deployment process for PianoRhythm, including GitHub Pages deployment, production server setup, CI/CD pipelines, and monitoring strategies.

Deployment Architecture

GitHub Pages Deployment (Current)

1. Automatic Deployment

The project automatically deploys to GitHub Pages on every push to the main branch:

# .github/workflows/deploy-pages.yml
name: Deploy to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '19'
cache: 'pnpm'

- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown

- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli

- name: Install dependencies
run: pnpm install

- name: Build Rust core
run: |
cd pianorhythm_core
chmod +x ./build-core-release.sh
./build-core-release.sh

- name: Build for production
run: pnpm run build:production
env:
NODE_ENV: production

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './dist'

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

2. Build Configuration for GitHub Pages

// app.config.ts - GitHub Pages specific configuration
const isProduction = mode === "production";

export default defineConfig({
server: {
preset: "./preset",
routeRules: {
"/": {
prerender: isProduction
}
},
prerender: {
crawlLinks: isProduction
}
},
vite: {
base: isProduction ? '/pianorhythm-ssr/' : '/',
build: {
target: "esnext",
minify: "esbuild",
sourcemap: false
}
}
});

3. Static Site Generation

// preset/nitro.config.ts - GitHub Pages preset
export default defineNitroConfig({
preset: "github-pages",
prerender: {
routes: [
"/",
"/login",
"/app-loading"
],
crawlLinks: true
},
nitro: {
output: {
publicDir: "dist"
}
}
});

Production Server Deployment

1. Docker Configuration

# Dockerfile
FROM node:19-alpine AS builder

# Install Rust
RUN apk add --no-cache curl build-base
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN rustup install nightly
RUN rustup default nightly
RUN rustup target add wasm32-unknown-unknown
RUN cargo install wasm-bindgen-cli

WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile

COPY . .

# Build Rust core
RUN cd pianorhythm_core && \
chmod +x ./build-core-release.sh && \
./build-core-release.sh

# Build application
RUN pnpm run build:production

# Production stage
FROM node:19-alpine AS runtime

RUN npm install -g pnpm

WORKDIR /app

# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./

# Install production dependencies only
RUN pnpm install --prod --frozen-lockfile

EXPOSE 3000

CMD ["pnpm", "start"]

2. Docker Compose for Development

# docker-compose.yml
version: '3.8'

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- PIANORHYTHM_MONGODB_URI=mongodb://mongo:27017/pianorhythm
- PIANORHYTHM_SERVER_URL=http://localhost:7000
depends_on:
- mongo
- redis
volumes:
- ./logs:/app/logs

mongo:
image: mongo:7
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
environment:
- MONGO_INITDB_DATABASE=pianorhythm

redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data

volumes:
mongo_data:
redis_data:

3. DigitalOcean Deployment

# .github/workflows/deploy-production.yml
name: Deploy to Production

on:
release:
types: [published]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.digitalocean.com/pianorhythm/app:latest
registry.digitalocean.com/pianorhythm/app:${{ github.event.release.tag_name }}

- name: Deploy to DigitalOcean
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

- name: Update Kubernetes deployment
run: |
doctl kubernetes cluster kubeconfig save pianorhythm-cluster
kubectl set image deployment/pianorhythm-app app=registry.digitalocean.com/pianorhythm/app:${{ github.event.release.tag_name }}
kubectl rollout status deployment/pianorhythm-app

Environment Configuration

1. Environment Variables

# Production environment variables
NODE_ENV=production
PORT=3000

# Database
PIANORHYTHM_MONGODB_URI=mongodb+srv://user:[email protected]/pianorhythm
PIANORHYTHM_MONGODB_API_KEY=your-api-key

# External Services
PIANORHYTHM_SERVER_URL=https://api.pianorhythm.io
PR_ASSETS_URL=https://assets.pianorhythm.io

# Authentication
PIANORHYTHM_GITHUB_ACCESS_TOKEN=your-github-token
PIANORHYTHM_GITHUB_APP_ID=925579

# Monitoring
SENTRY_DSN=your-sentry-dsn
ANALYTICS_URL=your-analytics-url

# Security
JWT_SECRET=your-jwt-secret
CORS_ORIGIN=https://pianorhythm.io

2. Configuration Management

// src/lib/config.ts
import { z } from 'zod';

const ConfigSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.coerce.number().default(3000),
MONGODB_URI: z.string().url(),
SERVER_URL: z.string().url(),
ASSETS_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
});

export const config = ConfigSchema.parse(process.env);

export const isDevelopment = config.NODE_ENV === 'development';
export const isProduction = config.NODE_ENV === 'production';

Database Deployment

1. MongoDB Atlas Setup

// Database connection for production
export class ProductionDatabase {
private static instance: ProductionDatabase;
private client: MongoClient;

private constructor() {
this.client = new MongoClient(config.MONGODB_URI, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
});
}

async connect(): Promise<void> {
try {
await this.client.connect();
console.log('✅ Connected to MongoDB Atlas');
} catch (error) {
console.error('❌ MongoDB connection failed:', error);
throw error;
}
}

getDb(name: string = 'pianorhythm'): Db {
return this.client.db(name);
}
}

2. Database Migrations

// scripts/migrate.ts
import { MongoClient } from 'mongodb';

const migrations = [
{
version: 1,
name: 'create_indexes',
up: async (db: Db) => {
await db.collection('users').createIndex({ usertag: 1 }, { unique: true });
await db.collection('rooms').createIndex({ name: 1 });
await db.collection('sheet_music').createIndex({ title: 'text' });
}
},
{
version: 2,
name: 'add_user_settings',
up: async (db: Db) => {
await db.collection('users').updateMany(
{ settings: { $exists: false } },
{ $set: { settings: { theme: 'dark', notifications: true } } }
);
}
}
];

export async function runMigrations(): Promise<void> {
const client = new MongoClient(process.env.MONGODB_URI!);
await client.connect();

const db = client.db();
const migrationsCollection = db.collection('migrations');

for (const migration of migrations) {
const existing = await migrationsCollection.findOne({ version: migration.version });

if (!existing) {
console.log(`Running migration: ${migration.name}`);
await migration.up(db);
await migrationsCollection.insertOne({
version: migration.version,
name: migration.name,
appliedAt: new Date()
});
}
}

await client.close();
}

CDN and Asset Management

1. Asset Optimization

// vite.config.ts - Asset optimization
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['solid-js', '@solidjs/router'],
audio: ['@core/pkg'],
ui: ['@hope-ui/solid']
},
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];

if (/\.(wasm|sf2)$/.test(assetInfo.name)) {
return `assets/[name]-[hash][extname]`;
}

return `assets/[name]-[hash][extname]`;
}
}
},
assetsInlineLimit: 0, // Don't inline assets
chunkSizeWarningLimit: 1000
}
});

2. Service Worker for Caching

// public/sw.js
const CACHE_NAME = 'pianorhythm-v1';
const STATIC_ASSETS = [
'/',
'/login',
'/manifest.json',
'/pianorhythm_core/pkg/pianorhythm_core.wasm'
];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});

self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
// Don't cache API requests
return;
}

event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});

Monitoring and Logging

1. Application Monitoring

// src/lib/monitoring.ts
import * as Sentry from '@sentry/node';

export function initializeMonitoring(): void {
if (isProduction) {
Sentry.init({
dsn: config.SENTRY_DSN,
environment: config.NODE_ENV,
tracesSampleRate: 0.1,
});
}
}

export function logError(error: Error, context?: string): void {
console.error(`[${context || 'Unknown'}] Error:`, error);

if (isProduction) {
Sentry.captureException(error, {
tags: { context }
});
}
}

export function logPerformance(operation: string, duration: number): void {
console.log(`[Performance] ${operation}: ${duration}ms`);

if (isProduction && duration > 1000) {
Sentry.addBreadcrumb({
message: `Slow operation: ${operation}`,
level: 'warning',
data: { duration }
});
}
}

2. Health Checks

// src/routes/api/health.ts
export async function GET(): Promise<Response> {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
checks: {
database: await checkDatabase(),
memory: checkMemory(),
uptime: process.uptime()
}
};

const status = Object.values(health.checks).every(check =>
typeof check === 'object' ? check.status === 'ok' : true
) ? 200 : 503;

return new Response(JSON.stringify(health), {
status,
headers: { 'Content-Type': 'application/json' }
});
}

async function checkDatabase(): Promise<{ status: string; latency?: number }> {
try {
const start = Date.now();
await Database.getInstance().getDb().admin().ping();
const latency = Date.now() - start;

return { status: 'ok', latency };
} catch (error) {
return { status: 'error' };
}
}

function checkMemory(): { status: string; usage: NodeJS.MemoryUsage } {
const usage = process.memoryUsage();
const status = usage.heapUsed / usage.heapTotal > 0.9 ? 'warning' : 'ok';

return { status, usage };
}

Security Considerations

1. HTTPS and Security Headers

// Security middleware
export const securityMiddleware = createMiddleware({
onBeforeResponse: [
(event) => {
// Security headers
setResponseHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
setResponseHeader("X-Content-Type-Options", "nosniff");
setResponseHeader("X-Frame-Options", "DENY");
setResponseHeader("X-XSS-Protection", "1; mode=block");
setResponseHeader("Referrer-Policy", "strict-origin-when-cross-origin");

// CORS for WebAssembly
setResponseHeader("Cross-Origin-Embedder-Policy", "require-corp");
setResponseHeader("Cross-Origin-Opener-Policy", "same-origin");
}
]
});

2. Environment Secrets

# Use environment-specific secret management
# Development: .env files
# Production: Kubernetes secrets, Docker secrets, or cloud provider secret management

# Example Kubernetes secret
apiVersion: v1
kind: Secret
metadata:
name: pianorhythm-secrets
type: Opaque
data:
mongodb-uri: <base64-encoded-uri>
jwt-secret: <base64-encoded-secret>

Rollback Strategy

1. Blue-Green Deployment

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pianorhythm-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: pianorhythm
template:
metadata:
labels:
app: pianorhythm
spec:
containers:
- name: app
image: registry.digitalocean.com/pianorhythm/app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5

2. Automated Rollback

#!/bin/bash
# scripts/rollback.sh

PREVIOUS_VERSION=$(kubectl get deployment pianorhythm-app -o jsonpath='{.metadata.annotations.deployment\.kubernetes\.io/revision}')
PREVIOUS_VERSION=$((PREVIOUS_VERSION - 1))

echo "Rolling back to revision $PREVIOUS_VERSION"

kubectl rollout undo deployment/pianorhythm-app --to-revision=$PREVIOUS_VERSION
kubectl rollout status deployment/pianorhythm-app

# Verify rollback
if kubectl get pods -l app=pianorhythm | grep -q "Running"; then
echo "✅ Rollback successful"
else
echo "❌ Rollback failed"
exit 1
fi

Performance Optimization

1. Build Optimization

// Build performance optimizations
export default defineConfig({
build: {
target: 'esnext',
minify: 'esbuild',
cssMinify: true,
rollupOptions: {
treeshake: true,
output: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
if (id.includes('solid-js')) return 'solid';
if (id.includes('@hope-ui')) return 'ui';
if (id.includes('protobufjs')) return 'proto';
return 'vendor';
}
}
}
}
}
});

2. Runtime Performance

// Performance monitoring in production
if (isProduction) {
// Monitor Core Web Vitals
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);
});
}

Next Steps