diff --git a/Dockerfile b/Dockerfile index b166b9b..dbb7750 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,8 @@ ARG NODEVER=20 RUN dnf install -y \ https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm && \ dnf update -y && \ - dnf install -y wget procps cronie iproute nginx openssl && \ + dnf install -y wget procps cronie iproute nginx openssl git microdnf make gcc gcc-c++ && \ + yum groupinstall 'Development Tools' && \ dnf clean all && \ rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/* \ /var/cache/yum /tmp/* /var/tmp/* @@ -25,11 +26,11 @@ RUN npm install -g pm2@latest --production && \ npm cache clean --force && \ rm -rf /tmp/* -# Copy configs and web files +# Copy nginx config COPY ./configs/nginx.conf /etc/nginx/nginx.conf -COPY ./configs/index.js /var/www/html/ -COPY ./configs/package.json /var/www/html/ -COPY ./configs/ecosystem.config.js /var/www/html/ + +# Copy examples directory for default app fallback +COPY ./examples/ /examples/ # Set up cron job for log rotation RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab diff --git a/README.md b/README.md index 6edfc4e..946a539 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Your Node.js application needs just two files to get started: **Optional Files:** - `public/` folder for static files (HTML, CSS, images) -- `ecosystem.config.js` for advanced PM2 configuration +- `ecosystem.config.js` for advanced PM2 configuration (auto-generated if not provided) ### Step 2: What Users Need to Do @@ -134,10 +134,18 @@ app.listen(port, () => { **That's it!** The container will: - Install your dependencies automatically +- Generate PM2 configuration from your package.json - Start your application with PM2 - Handle SSL and reverse proxy - Provide health monitoring +#### Important package.json fields: +- **name**: Used as the PM2 process name (defaults to 'node-app') +- **main**: Entry point file (defaults to 'index.js') +- **scripts.start**: Alternative way to specify entry point (e.g., "node server.js") + +The container automatically generates an ecosystem.config.js file from your package.json if you don't provide one. + ### Step 3: Example Applications See the `examples/` directory for complete working examples: diff --git a/configs/ecosystem.config.js b/configs/ecosystem.config.js deleted file mode 100644 index 2311a04..0000000 --- a/configs/ecosystem.config.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - apps: [{ - name: 'node-app', - script: 'index.js', - instances: 1, - autorestart: true, - watch: false, - max_memory_restart: '256M', // Restart if app uses more than 256MB - kill_timeout: 3000, - wait_ready: true, - listen_timeout: 3000, - env: { - NODE_ENV: 'development', - PORT: 3000, - NODE_OPTIONS: '--max-old-space-size=200' // Limit V8 heap to 200MB - }, - env_production: { - NODE_ENV: 'production', - PORT: 3000, - NODE_OPTIONS: '--max-old-space-size=200' - }, - log_file: '/home/myuser/logs/nodejs/app.log', - error_file: '/home/myuser/logs/nodejs/error.log', - out_file: '/home/myuser/logs/nodejs/out.log', - log_date_format: 'YYYY-MM-DD HH:mm:ss Z', - log_type: 'json', - merge_logs: true, - max_restarts: 5, - min_uptime: '10s' - }] -}; \ No newline at end of file diff --git a/configs/index.js b/configs/index.js deleted file mode 100644 index 63efbb7..0000000 --- a/configs/index.js +++ /dev/null @@ -1,101 +0,0 @@ -const express = require('express'); -const session = require('express-session'); -const app = express(); -const port = process.env.PORT || 3000; - -// Middleware -app.use(express.json()); -app.use(express.static('public')); - -// Session configuration with Memcache (only in DEV mode when memcached is available) -if (process.env.NODE_ENV !== 'production') { - try { - const MemcachedStore = require('connect-memcached')(session); - app.use(session({ - store: new MemcachedStore({ - hosts: ['localhost:11211'] - }), - secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production', - resave: false, - saveUninitialized: false, - cookie: { - secure: false, // Allow HTTP in development - maxAge: 24 * 60 * 60 * 1000 // 24 hours - } - })); - console.log('Memcached session store initialized'); - } catch (err) { - console.log('Memcached not available, using memory store'); - app.use(session({ - secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production', - resave: false, - saveUninitialized: false, - cookie: { - secure: false, - maxAge: 24 * 60 * 60 * 1000 - } - })); - } -} else { - // Production session configuration (expects external session store) - app.use(session({ - secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production', - resave: false, - saveUninitialized: false, - cookie: { - secure: true, // HTTPS only in production - maxAge: 24 * 60 * 60 * 1000 - } - })); -} - -// Health check endpoint -app.get('/ping', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - version: process.env.npm_package_version || '1.0.0' - }); -}); - -// Default route -app.get('/', (req, res) => { - res.json({ - message: 'Cloud Node Container is running!', - nodeVersion: process.version, - environment: process.env.NODE_ENV || 'development', - timestamp: new Date().toISOString() - }); -}); - -// Info endpoint -app.get('/info', (req, res) => { - res.json({ - nodeVersion: process.version, - platform: process.platform, - arch: process.arch, - uptime: process.uptime(), - memory: process.memoryUsage(), - env: process.env.NODE_ENV || 'development' - }); -}); - -// Session demo endpoint -app.get('/session', (req, res) => { - if (!req.session.visits) { - req.session.visits = 0; - } - req.session.visits++; - - res.json({ - sessionId: req.sessionID, - visits: req.session.visits, - message: 'Session is working with ' + (process.env.NODE_ENV !== 'production' ? 'Memcached' : 'default store') - }); -}); - -app.listen(port, () => { - console.log(`Server running on port ${port}`); - console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); -}); \ No newline at end of file diff --git a/configs/nginx.conf b/configs/nginx.conf index b5fc935..1fad60f 100644 --- a/configs/nginx.conf +++ b/configs/nginx.conf @@ -17,6 +17,14 @@ http { client_max_body_size 8m; large_client_header_buffers 2 1k; + # Real IP configuration for HAProxy + set_real_ip_from 10.0.0.0/8; # Private network range + set_real_ip_from 172.16.0.0/12; # Private network range + set_real_ip_from 192.168.0.0/16; # Private network range + set_real_ip_from 127.0.0.1; # Localhost + real_ip_header X-Forwarded-For; + real_ip_recursive on; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; diff --git a/configs/package.json b/configs/package.json deleted file mode 100644 index 7714f62..0000000 --- a/configs/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "cnoc-default-app", - "version": "1.0.0", - "description": "Default Node.js application for Cloud Node Container", - "main": "index.js", - "scripts": { - "start": "node index.js", - "dev": "nodemon index.js", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "express": "^4.18.2", - "express-session": "^1.17.3", - "connect-memcached": "^1.0.0" - }, - "devDependencies": { - "nodemon": "^3.0.1" - }, - "keywords": [ - "nodejs", - "express", - "container", - "docker" - ], - "author": "", - "license": "MIT" -} \ No newline at end of file diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100755 index c59daee..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -USER=$1 -BACKUP_DIR="/home/$USER/_backups" -DATE=$(date +%Y%m%d_%H%M%S) - -# Create backup directory if it doesn't exist -mkdir -p $BACKUP_DIR - -# Backup application files -tar -czf $BACKUP_DIR/app_backup_$DATE.tar.gz -C /home/$USER app/ - -# Keep only last 10 backups -cd $BACKUP_DIR -ls -t app_backup_*.tar.gz | tail -n +11 | xargs -r rm - -echo "Backup completed: app_backup_$DATE.tar.gz" \ No newline at end of file diff --git a/scripts/create-nginx-config.sh b/scripts/create-nginx-config.sh index 7adae60..8d4dd64 100755 --- a/scripts/create-nginx-config.sh +++ b/scripts/create-nginx-config.sh @@ -35,7 +35,8 @@ server { proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; + proxy_set_header X-Forwarded-Proto \$http_x_forwarded_proto; + proxy_set_header X-CLIENT-IP \$http_x_client_ip; proxy_cache_bypass \$http_upgrade; } diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 199e924..fcbb442 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -30,7 +30,6 @@ nginx if [[ $environment == 'DEV' ]]; then echo "Starting Dev Deployment" - mkdir -p /home/$user/_backups # Ensure microdnf is available for installing additional packages in DEV mode if ! command -v microdnf &> /dev/null; then @@ -43,25 +42,38 @@ if [[ $environment == 'DEV' ]]; then # Start memcached with 32MB memory limit nohup memcached -d -u $user -p 11211 -m 32 - # Set up automatic backups - echo "*/30 * * * * root /scripts/backup.sh $user" >> /etc/crontab fi # Start cron for log rotation and backups /usr/sbin/crond -# If there's an app in the user directory, start it with PM2 -if [ -f /home/$user/app/package.json ]; then - cd /home/$user/app - su -c "npm install" $user - su -c "pm2 start ecosystem.config.js" $user -else - # Start default app - cd /var/www/html - npm install - su -c "pm2 start ecosystem.config.js" $user +# Create app directory if it doesn't exist +if [ ! -d /home/$user/app ]; then + echo "Creating app directory at /home/$user/app" + mkdir -p /home/$user/app + chown -R $user:$user /home/$user/app fi +# If app directory is empty, copy the simple-website example +if [ -z "$(ls -A /home/$user/app)" ]; then + echo "App directory is empty, copying simple-website example..." + cp -r /examples/simple-website/* /home/$user/app/ + chown -R $user:$user /home/$user/app + echo "Copied simple-website example to provide a working application" +fi + +# Now there's always an app in the user directory (either user's or example) +cd /home/$user/app +su -c "npm install" $user + +# Check if ecosystem.config.js exists, if not generate it +if [ ! -f /home/$user/app/ecosystem.config.js ]; then + echo "No ecosystem.config.js found, generating from package.json..." + /scripts/generate-ecosystem-config.sh "$user" "/home/$user/app" +fi + +su -c "pm2 start ecosystem.config.js" $user + # Follow logs tail -f /home/$user/logs/nginx/* /home/$user/logs/nodejs/* diff --git a/scripts/generate-ecosystem-config.sh b/scripts/generate-ecosystem-config.sh new file mode 100755 index 0000000..bac907d --- /dev/null +++ b/scripts/generate-ecosystem-config.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Generate ecosystem.config.js from package.json +# Usage: ./generate-ecosystem-config.sh + +user=$1 +app_path=$2 + +if [ -z "$user" ] || [ -z "$app_path" ]; then + echo "Usage: $0 " + exit 1 +fi + +package_json="$app_path/package.json" +ecosystem_config="$app_path/ecosystem.config.js" + +# Check if package.json exists +if [ ! -f "$package_json" ]; then + echo "Error: package.json not found at $package_json" + exit 1 +fi + +# Extract values from package.json +app_name=$(node -p "try { require('$package_json').name || 'node-app' } catch(e) { 'node-app' }") +main_script=$(node -p "try { require('$package_json').main || 'index.js' } catch(e) { 'index.js' }") +start_script=$(node -p "try { const scripts = require('$package_json').scripts; if (scripts && scripts.start) { scripts.start.replace(/^node\s+/, '') } else { null } } catch(e) { null }") + +# Use start script if available, otherwise use main field +if [ "$start_script" != "null" ] && [ -n "$start_script" ]; then + script_file="$start_script" +else + script_file="$main_script" +fi + +# Clean up the script file name (remove any node command prefix) +script_file=$(echo "$script_file" | sed 's/^node\s\+//') + +# Generate ecosystem.config.js +cat > "$ecosystem_config" << EOF +module.exports = { + apps: [{ + name: '${app_name}', + script: '${script_file}', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '256M', + kill_timeout: 3000, + wait_ready: true, + listen_timeout: 3000, + env: { + NODE_ENV: 'development', + PORT: 3000, + NODE_OPTIONS: '--max-old-space-size=200' + }, + env_production: { + NODE_ENV: 'production', + PORT: 3000, + NODE_OPTIONS: '--max-old-space-size=200' + }, + log_file: '/home/${user}/logs/nodejs/app.log', + error_file: '/home/${user}/logs/nodejs/error.log', + out_file: '/home/${user}/logs/nodejs/out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + log_type: 'json', + merge_logs: true, + max_restarts: 5, + min_uptime: '10s' + }] +}; +EOF + +echo "Generated ecosystem.config.js for app: $app_name (script: $script_file)" \ No newline at end of file