Compare commits
4
Commits
v1.1.0
...
v1.1.0-docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a7dfd5aff | ||
|
|
aa45e9c873 | ||
|
|
5f7cef7646 | ||
|
|
fdda517e7b |
No files matched your search
@@ -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
|
||||||
@@ -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
@@ -1,25 +1,49 @@
|
|||||||
# ===== STAGE 1: Dependency Installation =====
|
# ===== STAGE 1: Dependency Installation =====
|
||||||
FROM node:18 AS deps
|
FROM node:20-slim AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files and install dependencies
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# ===== STAGE 2: Runtime =====
|
# ===== 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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
tini \
|
tini \
|
||||||
|
wget \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
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
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Use tini as init process and start application
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
@@ -40,10 +40,13 @@ docker run -d \
|
|||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
-v ./data:/app/data \
|
-v ./data:/app/data \
|
||||||
--restart unless-stopped \
|
--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
|
```yaml
|
||||||
version: "3.9"
|
version: "3.9"
|
||||||
services:
|
services:
|
||||||
@@ -54,9 +57,31 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- 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
|
### Option 2: CasaOS Installation
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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}"
|
||||||
@@ -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
@@ -2,11 +2,21 @@ version: "3.9"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
spelling-app:
|
spelling-app:
|
||||||
build: .
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ghcr.io/nmemmert/spelling:1.1.0
|
||||||
container_name: spelling-app
|
container_name: spelling-app
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./public/users.json:/app/public/users.json
|
- ./data:/app/data
|
||||||
- ./public/wordlists.json:/app/public/wordlists.json
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
restart: unless-stopped
|
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
|
||||||
@@ -25,6 +25,37 @@ const files = {
|
|||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
app.use(express.json());
|
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
|
// Define routes after middleware is set up
|
||||||
// Serve raw wordlists.json for admin UI
|
// Serve raw wordlists.json for admin UI
|
||||||
app.get('/getWordlistsRaw', (req, res) => {
|
app.get('/getWordlistsRaw', (req, res) => {
|
||||||
@@ -1145,9 +1176,27 @@ app.get('/checkBadges', (req, res) => {
|
|||||||
// Initialize all required data files
|
// Initialize all required data files
|
||||||
ensureAllRequiredFiles();
|
ensureAllRequiredFiles();
|
||||||
|
|
||||||
// 🚀 Server start
|
// Get environment variables or use defaults
|
||||||
app.listen(PORT, () => {
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
console.log(`✅ Server listening at http://localhost:${PORT}`);
|
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
|
// Display available users for easy login
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user