Add Docker improvements and containerization
This commit is contained in:
1 parent
545cf4c8dc
commit
fdda517e7b
8 files changed
+312
-13
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
|
||||
+27
-5
@@ -1,25 +1,47 @@
|
||||
# ===== 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 \
|
||||
&& 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"
|
||||
|
||||
# Expose application port
|
||||
EXPOSE 3000
|
||||
|
||||
# Use tini as init process and start application
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["node", "server.js"]
|
||||
@@ -40,10 +40,13 @@ docker run -d \
|
||||
-p 3000:3000 \
|
||||
-v ./data:/app/data \
|
||||
--restart unless-stopped \
|
||||
--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
|
||||
|
||||
@@ -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}"
|
||||
+13
-3
@@ -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', 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.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 {
|
||||
|
||||
Reference in new issue
Block a user