4 Commits
9 changed files with 366 additions and 14 deletions

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# Version control
.git
.gitignore
.github
# Node.js
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
# Development files
*.md
!README.md
!CHANGELOG.md
*.log
*.bak
.DS_Store
.vscode
.idea
.editorconfig
.eslintrc
.prettierrc
# Test files
test
__tests__
coverage
# Docker
Dockerfile
docker-compose.yml
.dockerignore
# Local data (should be mounted as volume)
data/*
!data/.gitkeep
# Debug scripts
debug-*.js
test-*.js
# Documentation
docs
+55
View File
@@ -0,0 +1,55 @@
name: Build and Push Docker Image
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: nmemmert/spelling
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=sha,format=long
latest
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+29 -5
View File
@@ -1,25 +1,49 @@
# ===== STAGE 1: Dependency Installation =====
FROM node:18 AS deps
FROM node:20-slim AS deps
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# ===== STAGE 2: Runtime =====
FROM node:18-slim
FROM node:20-slim
# Install tini for proper signal handling
RUN apt-get update && apt-get install -y --no-install-recommends \
tini \
wget \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user for better security
RUN groupadd -r spelling && useradd -r -g spelling spelling
# Setup application directory
WORKDIR /app
COPY --from=deps /app /app
# Copy node modules and package files
COPY --from=deps /app/node_modules /app/node_modules
COPY --from=deps /app/package*.json /app/
# Copy application code
COPY . .
# Create and set permissions for data directory
RUN mkdir -p /app/data && chown -R spelling:spelling /app/data
# Switch to non-root user
USER spelling
# Set version label
LABEL version="1.1.0" \
description="Spelling Practice Application" \
maintainer="nmemmert" \
org.opencontainers.image.source="https://github.com/nmemmert/spelling"
# Expose application port
EXPOSE 3000
# Use tini as init process and start application
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["node", "server.js"]
+28 -3
View File
@@ -40,10 +40,13 @@ docker run -d \
-p 3000:3000 \
-v ./data:/app/data \
--restart unless-stopped \
ghcr.io/nmemmert/spelling:latest
--health-cmd "wget -q -O - http://localhost:3000/health || exit 1" \
--health-interval 30s \
--health-retries 3 \
ghcr.io/nmemmert/spelling:latest
```
#### Custom Docker Compose
#### Advanced Docker Compose
```yaml
version: "3.9"
services:
@@ -54,9 +57,31 @@ services:
- "3000:3000"
volumes:
- ./data:/app/data
restart: unless-stopped
environment:
- NODE_ENV=production
# Optional environment variables:
# - PORT=3000
# - HOST=0.0.0.0
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', res => res.statusCode === 200 ? process.exit(0) : process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```
#### Building Your Own Docker Image
```bash
# Clone the repository
git clone https://github.com/nmemmert/spelling.git
cd spelling
# Build the Docker image
docker build -t spelling:local .
# Run your local image
docker run -d --name spelling-app -p 3000:3000 -v ./data:/app/data spelling:local
```
### Option 2: CasaOS Installation
+51
View File
@@ -0,0 +1,51 @@
# PowerShell script to build and push Docker image for Spelling Practice App
# Set variables
$IMAGE_NAME = "ghcr.io/nmemmert/spelling"
$VERSION = (Get-Content -Path "version.json" | ConvertFrom-Json).version
Write-Host "🔨 Building Docker image ${IMAGE_NAME}:${VERSION}..." -ForegroundColor Cyan
# Build the image
docker build -t ${IMAGE_NAME}:${VERSION} -t ${IMAGE_NAME}:latest .
Write-Host "✅ Build completed." -ForegroundColor Green
Write-Host "🧪 Testing the image..." -ForegroundColor Cyan
# Run container for testing
docker run --name spelling-test -d -p 3001:3000 ${IMAGE_NAME}:${VERSION}
# Wait for container to start
Start-Sleep -Seconds 5
# Test health endpoint
try {
$response = Invoke-WebRequest -Uri "http://localhost:3001/health" -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Host "✅ Health check passed!" -ForegroundColor Green
} else {
Write-Host "❌ Health check failed with status $($response.StatusCode)" -ForegroundColor Red
docker logs spelling-test
docker stop spelling-test
docker rm spelling-test
exit 1
}
} catch {
Write-Host "❌ Health check failed: $_" -ForegroundColor Red
docker logs spelling-test
docker stop spelling-test
docker rm spelling-test
exit 1
}
# Stop and remove test container
docker stop spelling-test
docker rm spelling-test
Write-Host "📤 Pushing image to GitHub Container Registry..." -ForegroundColor Cyan
docker push ${IMAGE_NAME}:${VERSION}
docker push ${IMAGE_NAME}:latest
Write-Host "🚀 Successfully built and pushed ${IMAGE_NAME}:${VERSION}" -ForegroundColor Green
Write-Host "Press any key to exit..."
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Build and push Docker image for Spelling Practice App
# Set variables
IMAGE_NAME="ghcr.io/nmemmert/spelling"
VERSION=$(grep -o '"version": "[^"]*"' version.json | cut -d'"' -f4)
echo "🔨 Building Docker image ${IMAGE_NAME}:${VERSION}..."
# Build the image
docker build -t ${IMAGE_NAME}:${VERSION} -t ${IMAGE_NAME}:latest .
echo "✅ Build completed."
echo "🧪 Testing the image..."
# Run container for testing
docker run --name spelling-test -d -p 3001:3000 ${IMAGE_NAME}:${VERSION}
# Wait for container to start
sleep 5
# Test health endpoint
HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/health)
if [ "$HEALTH_STATUS" = "200" ]; then
echo "✅ Health check passed!"
else
echo "❌ Health check failed with status $HEALTH_STATUS"
docker logs spelling-test
docker stop spelling-test
docker rm spelling-test
exit 1
fi
# Stop and remove test container
docker stop spelling-test
docker rm spelling-test
echo "📤 Pushing image to GitHub Container Registry..."
docker push ${IMAGE_NAME}:${VERSION}
docker push ${IMAGE_NAME}:latest
echo "🚀 Successfully built and pushed ${IMAGE_NAME}:${VERSION}"
+51
View File
@@ -0,0 +1,51 @@
# PowerShell script to build and push Docker image for Spelling Practice App
# Set variables
$IMAGE_NAME = "ghcr.io/nmemmert/spelling"
$VERSION = (Get-Content -Path "version.json" | ConvertFrom-Json).version
Write-Host "🔨 Building Docker image ${IMAGE_NAME}:${VERSION}..." -ForegroundColor Cyan
# Build the image
docker build -t ${IMAGE_NAME}:${VERSION} -t ${IMAGE_NAME}:latest .
Write-Host "✅ Build completed." -ForegroundColor Green
Write-Host "🧪 Testing the image..." -ForegroundColor Cyan
# Run container for testing
docker run --name spelling-test -d -p 3001:3000 ${IMAGE_NAME}:${VERSION}
# Wait for container to start
Start-Sleep -Seconds 5
# Test health endpoint
try {
$response = Invoke-WebRequest -Uri "http://localhost:3001/health" -UseBasicParsing
if ($response.StatusCode -eq 200) {
Write-Host "✅ Health check passed!" -ForegroundColor Green
} else {
Write-Host "❌ Health check failed with status $($response.StatusCode)" -ForegroundColor Red
docker logs spelling-test
docker stop spelling-test
docker rm spelling-test
exit 1
}
} catch {
Write-Host "❌ Health check failed: $_" -ForegroundColor Red
docker logs spelling-test
docker stop spelling-test
docker rm spelling-test
exit 1
}
# Stop and remove test container
docker stop spelling-test
docker rm spelling-test
Write-Host "📤 Pushing image to GitHub Container Registry..." -ForegroundColor Cyan
docker push ${IMAGE_NAME}:${VERSION}
docker push ${IMAGE_NAME}:latest
Write-Host "🚀 Successfully built and pushed ${IMAGE_NAME}:${VERSION}" -ForegroundColor Green
Write-Host "Press any key to exit..."
$null = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+13 -3
View File
@@ -2,11 +2,21 @@ version: "3.9"
services:
spelling-app:
build: .
build:
context: .
dockerfile: Dockerfile
image: ghcr.io/nmemmert/spelling:1.1.0
container_name: spelling-app
ports:
- "3000:3000"
volumes:
- ./public/users.json:/app/public/users.json
- ./public/wordlists.json:/app/public/wordlists.json
- ./data:/app/data
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', res => res.statusCode === 200 ? process.exit(0) : process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
+52 -3
View File
@@ -25,6 +25,37 @@ const files = {
app.use(express.static('public'));
app.use(express.json());
// Health check endpoint for container monitoring
app.get('/health', (req, res) => {
try {
// Check if essential files exist
const usersExists = fs.existsSync(path.join(DATA_DIR, files.users));
const wordlistsExists = fs.existsSync(path.join(DATA_DIR, files.wordlists));
if (usersExists && wordlistsExists) {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
version: require('./version.json').version
});
} else {
res.status(503).json({
status: 'error',
message: 'Essential data files not available',
missing: {
users: !usersExists,
wordlists: !wordlistsExists
}
});
}
} catch (error) {
res.status(500).json({
status: 'error',
message: error.message
});
}
});
// Define routes after middleware is set up
// Serve raw wordlists.json for admin UI
app.get('/getWordlistsRaw', (req, res) => {
@@ -1145,9 +1176,27 @@ app.get('/checkBadges', (req, res) => {
// Initialize all required data files
ensureAllRequiredFiles();
// 🚀 Server start
app.listen(PORT, () => {
console.log(`✅ Server listening at http://localhost:${PORT}`);
// Get environment variables or use defaults
const HOST = process.env.HOST || '0.0.0.0';
const PORT_ENV = process.env.PORT || PORT;
// Handle graceful shutdown for container orchestration
process.on('SIGTERM', () => {
console.log('🛑 SIGTERM received. Shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('� SIGINT received. Shutting down gracefully...');
process.exit(0);
});
// �🚀 Server start
app.listen(PORT_ENV, HOST, () => {
console.log(`✅ Server listening at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT_ENV}`);
console.log(`📁 Data directory: ${DATA_DIR}`);
console.log(`🌐 Node environment: ${process.env.NODE_ENV || 'development'}`);
console.log(`📦 Version: ${require('./version.json').version}`);
// Display available users for easy login
try {