From fee6fdec2bad704813f772dedec1da5f6a6f4f0d Mon Sep 17 00:00:00 2001 From: a2nr Date: Tue, 6 Jan 2026 21:38:54 +0700 Subject: [PATCH] update example --- Dockerfile | 15 ++- README.md | 185 ++++++++++++++++++++++++++++++- access_container.sh | 2 +- app.py | 29 ++++- example.env.production | 38 +++++++ example_config/sinau-c-tail.json | 19 ++++ gunicorn.conf.py | 38 +++++++ podman-compose.yml | 29 ++++- requirements.txt | 3 +- start.sh | 18 +-- test/test_production.sh | 101 +++++++++++++++++ 11 files changed, 449 insertions(+), 28 deletions(-) create mode 100644 example.env.production create mode 100644 example_config/sinau-c-tail.json create mode 100644 gunicorn.conf.py create mode 100755 test/test_production.sh diff --git a/Dockerfile b/Dockerfile index 0450b47..43be8f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,11 @@ FROM python:3.11-slim -# Install gcc compiler for C code compilation +# Create a non-root user for security +RUN useradd --create-home --shell /bin/bash app + +# Install gcc compiler for C code compilation and other production dependencies RUN apt-get update && \ - apt-get install -y gcc build-essential && \ + apt-get install -y gcc build-essential curl && \ rm -rf /var/lib/apt/lists/* # Set working directory @@ -15,8 +18,12 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . +# Change ownership to the app user +RUN chown -R app:app /app +USER app + # Expose port 5000 EXPOSE 5000 -# Run the application with Gunicorn -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "1", "app:app"] \ No newline at end of file +# Run the application with Gunicorn in production mode using config file +CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"] \ No newline at end of file diff --git a/README.md b/README.md index 03e6fa1..dc468de 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,38 @@ The system uses Markdown files for content management. There are two main types The content directory is located at the parent directory level, outside of the `elemes/` directory. This allows for easy linking and sharing of content between different instances of the LMS. +### Environment Configuration + +The application uses environment variables for configuration. Create a `.env` file at the parent directory level (same level as the `elemes/` directory) to configure the application. The `podman-compose.yml` file is configured to read environment variables from `../.env`. + +Example `.env` file: +``` +# Production Environment Configuration + +# Flask Configuration +FLASK_ENV=production +FLASK_DEBUG=0 +SECRET_KEY=your-super-secret-key-here-change-this-in-production + +# Application Configuration +CONTENT_DIR=content +STATIC_DIR=static +TEMPLATES_DIR=templates +TOKENS_FILE=tokens.csv + +# Server Configuration +HOST=0.0.0.0 +PORT=5000 + +# Security Configuration +MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size +WTF_CSRF_ENABLED=true + +# Logging Configuration +LOG_LEVEL=INFO +LOG_FILE=/var/log/lms-c/app.log +``` + ### Home Page (`home.md`) The home page serves as the main landing page and lesson directory. Here's the template structure: @@ -392,13 +424,121 @@ This LMS can be used as a submodule in another repository. To set it up: 3. Add your lesson files to the content directory -4. Run the LMS from the elemes directory: +4. Create an environment configuration file at the root level: + ```bash + touch .env + ``` + +5. Run the LMS from the elemes directory: ```bash cd elemes podman-compose -f podman-compose.yml up --build ``` -The content directory at the root level will be automatically mounted to the application container. +The content directory and environment configuration at the root level will be automatically mounted to the application container. + +## Production Deployment + +For production deployment, additional considerations and configurations are required to ensure security, performance, and reliability. + +### Production Environment Setup + +1. **Environment Variables**: Use the following environment variables for production: + - `FLASK_ENV=production` - Sets the environment to production + - `FLASK_DEBUG=0` - Disables debug mode + - `PORT=5000` - Specifies the port to run the application on + +2. **Security Considerations**: + - Run the container as a non-root user (already configured in Dockerfile) + - Limit resource usage with podman/docker resource constraints + - Use a reverse proxy (like Nginx) in front of the application + - Implement proper HTTPS with SSL/TLS certificates + +3. **Performance Optimizations**: + - Use the multi-worker Gunicorn configuration (already configured) + - Set appropriate worker processes based on your server's CPU cores + - Configure proper caching mechanisms if needed + +### Production Deployment with Podman + +To deploy in a production environment: + +1. Build the production image: + ```bash + podman build -t lms-c:prod . + ``` + +2. Run with production settings: + ```bash + podman run -d \ + --name lms-c-prod \ + -p 80:5000 \ + -v /path/to/production/content:/app/content \ + -v /path/to/production/static:/app/static \ + -v /path/to/production/tokens.csv:/app/tokens.csv \ + --restart=unless-stopped \ + --memory=512m \ + --cpus=1 \ + lms-c:prod + ``` + +### Production Deployment with Podman Compose + +Create a production-specific compose file (`podman-compose.prod.yml`): + +```yaml +version: '3.8' + +services: + lms-c: + build: . + ports: + - "80:5000" + volumes: + - /path/to/production/content:/app/content + - /path/to/production/static:/app/static + - /path/to/production/templates:/app/templates + - /path/to/production/tokens.csv:/app/tokens.csv + env_file: + - /path/to/production/.env + restart: unless-stopped + mem_limit: 512m + cpus: 1.0 +``` + +Run with: +```bash +podman-compose -f podman-compose.prod.yml up -d --build +``` + +### Reverse Proxy Configuration + +For production, it's recommended to put a reverse proxy like Nginx in front of the application: + +Example Nginx configuration: +``` +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:5000; + 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; + } +} +``` + +### SSL/HTTPS Setup + +For HTTPS, you can use Let's Encrypt with Certbot: +```bash +sudo certbot --nginx -d your-domain.com +``` + +This will automatically configure SSL certificates for your domain. ## Security Considerations @@ -416,8 +556,12 @@ The content directory at the root level will be automatically mounted to the app - `tokens.csv`: Student progress tracking file - `generate_tokens.py`: Script to generate tokens CSV file - `Dockerfile`: Container configuration -- `podman-compose.yml`: Podman Compose configuration +- `podman-compose.yml`: Podman Compose configuration with env_file support - `requirements.txt`: Python dependencies +- `test/`: Directory containing test scripts and load testing tools +- `test/test_production.sh`: Production environment test script +- `test/locustfile.py`: Load testing script using Locust +- `test/podman-compose.locust.yml`: Podman Compose configuration for load testing ## Development @@ -427,12 +571,27 @@ To access the running container for development: podman exec -it /bin/bash ``` +## Testing + +The project includes comprehensive test scripts: + +1. **Production Environment Tests**: Run the production test script to verify all functionality: + ```bash + ./test/test_production.sh + ``` + +2. **Load Testing**: Use the included Locust configuration for load testing: + ```bash + podman-compose -f test/podman-compose.locust.yml up --build + ``` + ## Troubleshooting - If you get permission errors, make sure your user has access to Podman - If the application doesn't start, check that port 5000 is available - If code compilation fails, verify that the gcc compiler is available in the container - If progress tracking isn't working, ensure the tokens.csv file is properly formatted +- If environment variables aren't being loaded, verify that the .env file is in the correct location and properly formatted ## Stopping the Application @@ -562,11 +721,11 @@ The system includes support for load testing using Locust to simulate multiple c 4. **Alternative: Run Locust Directly** - Install Locust on the load testing machine: ```bash - pip install -r locust/requirements.txt + pip install -r test/requirements.txt ``` - Run Locust directly: ```bash - cd locust + cd test locust -f locustfile.py --host http://:5000 ``` @@ -587,3 +746,19 @@ Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load test - Compilation performance under load - Database performance (if one is added in the future) - Container resource limits + +The included `locustfile.py` simulates the following user behaviors: +- Browsing the home page +- Viewing lessons +- Compiling C code +- Validating student tokens +- Logging in with tokens +- Tracking student progress + +### Monitoring Performance + +Monitor the LMS server's resource usage (CPU, memory, disk I/O) during load testing to identify potential bottlenecks. Pay attention to: +- Response times for API requests +- Compilation performance under load +- Database performance (if one is added in the future) +- Container resource limits diff --git a/access_container.sh b/access_container.sh index dab5da9..1635b99 100755 --- a/access_container.sh +++ b/access_container.sh @@ -18,7 +18,7 @@ else podman exec -it lms-c-container /bin/bash else echo "Building and starting container..." - podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv lms-c + podman build -t lms-c . && podman run -d -p 5000:5000 --name lms-c-container -v ../content:/app/content -v ./static:/app/static -v ./templates:/app/templates -v ./tokens.csv:/app/tokens.csv -e FLASK_ENV=production -e FLASK_DEBUG=0 lms-c sleep 5 # Wait for the application to start podman exec -it lms-c-container /bin/bash fi diff --git a/app.py b/app.py index 2d44c12..c96e468 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,7 @@ import subprocess import tempfile import markdown from flask import Flask, render_template, request, jsonify, send_from_directory +from flask_talisman import Talisman import glob import csv import uuid @@ -16,11 +17,29 @@ from datetime import datetime app = Flask(__name__) -# Configuration -CONTENT_DIR = 'content' -STATIC_DIR = 'static' -TEMPLATES_DIR = 'templates' -TOKENS_FILE = 'tokens.csv' +# Load configuration from environment variables with defaults +CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content') +STATIC_DIR = os.environ.get('STATIC_DIR', 'static') +TEMPLATES_DIR = os.environ.get('TEMPLATES_DIR', 'templates') +TOKENS_FILE = os.environ.get('TOKENS_FILE', 'tokens.csv') + +# Security configuration using Talisman +Talisman(app, + force_https=False, # Set to True if using SSL + strict_transport_security=True, + strict_transport_security_max_age=31536000, + frame_options='DENY', + content_security_policy={ + 'default-src': "'self'", + 'script-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com", + 'style-src': "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com", + 'img-src': "'self' data: https:", + 'font-src': "'self' https://cdn.jsdelivr.net", + 'connect-src': "'self'", + 'frame-ancestors': "'none'", + }, + referrer_policy='strict-origin-when-cross-origin' +) def get_lessons(): """Get all lesson files from the content directory""" diff --git a/example.env.production b/example.env.production new file mode 100644 index 0000000..14451c5 --- /dev/null +++ b/example.env.production @@ -0,0 +1,38 @@ +# Production Environment Configuration +# + +# tailscale Configuration +ELEMES_HOST=change-to-lms-topic +TS_AUTHKEY=your-super-secret-key-here-change-this-in-production + +# Flask Configuration +FLASK_ENV=production +FLASK_DEBUG=0 +SECRET_KEY=your-super-secret-key-here-change-this-in-production + +# Application Configuration +CONTENT_DIR=content +STATIC_DIR=static +TEMPLATES_DIR=templates +TOKENS_FILE=tokens.csv + +# Server Configuration +HOST=0.0.0.0 +PORT=5000 + +# Security Configuration +MAX_CONTENT_LENGTH=1024 * 1024 # 1MB max upload size +WTF_CSRF_ENABLED=true + +# Logging Configuration +LOG_LEVEL=INFO +LOG_FILE=/var/log/lms-c/app.log + +# Database Configuration (if using one in future) +# DATABASE_URL=postgresql://user:password@localhost/dbname + +# External Services (if applicable) +# SMTP_SERVER=smtp.example.com +# SMTP_PORT=587 +# SMTP_USERNAME=user@example.com +# SMTP_PASSWORD=password diff --git a/example_config/sinau-c-tail.json b/example_config/sinau-c-tail.json new file mode 100644 index 0000000..00b4384 --- /dev/null +++ b/example_config/sinau-c-tail.json @@ -0,0 +1,19 @@ +{ + "TCP": { + "443": { + "HTTPS": true + } + }, + "Web": { + "${TS_CERT_DOMAIN}:443": { + "Handlers": { + "/": { + "Proxy": "http://elemes:5000" + } + } + } + }, + "AllowFunnel": { + "${TS_CERT_DOMAIN}:443": true + } +} diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..f8cc719 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,38 @@ +# Gunicorn configuration file for production + +# Server socket +bind = "0.0.0.0:5000" +backlog = 2048 + +# Worker processes +workers = 4 +worker_class = "sync" +worker_connections = 1000 +timeout = 120 +keepalive = 5 +max_requests = 1000 +max_requests_jitter = 100 +preload_app = True + +# Security +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# Logging +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' + +# Process naming +proc_name = 'lms-c' + +# Server mechanics +user = 'app' +group = 'app' +tmp_upload_dir = None + +# SSL (uncomment and configure if using SSL) +# keyfile = '/path/to/keyfile' +# certfile = '/path/to/certfile' \ No newline at end of file diff --git a/podman-compose.yml b/podman-compose.yml index 29dde01..c4141d5 100644 --- a/podman-compose.yml +++ b/podman-compose.yml @@ -1,8 +1,9 @@ version: '3.8' services: - lms-c: + elemes: build: . + container_name: elemes ports: - "5000:5000" volumes: @@ -10,6 +11,28 @@ services: - ./static:/app/static - ./templates:/app/templates - ../tokens_siswa.csv:/app/tokens.csv + env_file: + - ../.env + command: gunicorn --config gunicorn.conf.py app:app + elemes-ts: + image: docker.io/tailscale/tailscale:latest + container_name: elemes-ts + hostname: ${ELEMES_HOST} environment: - - FLASK_ENV=development - command: python app.py + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_SERVE_CONFIG=/config/sinau-c-tail.json + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=true + volumes: + - ./state:/var/lib/tailscale + - ../config:/config + - /dev/net/tun:/dev/net/tun + cap_add: + - net_admin + - sys_module + restart: unless-stopped + +networks: + main_network: + drive: bridge + network_mode: service:elemes-ts diff --git a/requirements.txt b/requirements.txt index 3a2b2ec..1260056 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask==2.3.3 markdown==3.5 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +flask-talisman==1.1.0 \ No newline at end of file diff --git a/start.sh b/start.sh index 25c76c6..dec6390 100755 --- a/start.sh +++ b/start.sh @@ -5,19 +5,19 @@ echo "Starting C Programming Learning Management System..." # Check if container is already running -if [ "$(podman ps -q -f name=lms-c-container)" ]; then - echo "Container is already running. Access the application at http://localhost:5000" - exit 0 +if [ "$(podman ps -q -f name=elemes-container)" ]; then + echo "Container is already running. Access the application at http://localhost:5000" + exit 0 fi # Check if container exists but is stopped -if [ "$(podman ps -aq -f name=lms-c-container)" ]; then - echo "Starting existing container..." - podman start lms-c-container +if [ "$(podman ps -aq -f name=elemes-container)" ]; then + echo "Starting existing container..." + podman start elemes-container else - # Build and run the container - echo "Building and starting container..." - podman-compose up --build -d + # Build and run the container + echo "Building and starting container..." + podman-compose up --build -d fi echo "Application is now running. Access at http://localhost:5000" diff --git a/test/test_production.sh b/test/test_production.sh new file mode 100755 index 0000000..8e45ad1 --- /dev/null +++ b/test/test_production.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Test script for LMS-C Production Environment + +echo "Starting LMS-C Production Environment Tests..." + +# Start the application +echo "Starting the application..." +cd .. +./start.sh +cd test + +# Wait for the application to start +echo "Waiting for application to start..." +sleep 10 + +# Test 1: Check if the home page loads and displays lessons +echo "Test 1: Checking home page and lesson display..." +HOME_PAGE_CHECK=$(curl -s http://localhost:5000/ | grep -i "Available Lessons" | wc -l) +if [ $HOME_PAGE_CHECK -gt 0 ]; then + echo "✓ Test 1 PASSED: Home page displays lessons correctly" +else + echo "✗ Test 1 FAILED: Home page does not display lessons" +fi + +# Test 2: Check if specific lesson pages load +echo "Test 2: Checking lesson page loading..." +LESSON_PAGE_CHECK=$(curl -s http://localhost:5000/lesson/welcome.md | grep -i "C Programming Learning System" | wc -l) +if [ $LESSON_PAGE_CHECK -gt 0 ]; then + echo "✓ Test 2 PASSED: Lesson pages load correctly" +else + echo "✗ Test 2 FAILED: Lesson pages do not load" +fi + +# Test 3: Test the code compilation API +echo "Test 3: Testing code compilation API..." +COMPILATION_TEST=$(curl -s -X POST http://localhost:5000/compile \ + -H "Content-Type: application/json" \ + -d '{"code":"#include \n\nint main() {\n printf(\"Hello, World!\\n\");\n return 0;\n}"}') + +# Check if the response contains success: true and the expected output +if echo "$COMPILATION_TEST" | grep -q '"success":true' && echo "$COMPILATION_TEST" | grep -q "Hello, World!"; then + echo "✓ Test 3 PASSED: Code compilation API works correctly" +else + echo "✗ Test 3 FAILED: Code compilation API not working" + echo " Response: $COMPILATION_TEST" +fi + +# Test 4: Test with a more complex C program +echo "Test 4: Testing complex code compilation..." +COMPLEX_CODE='{ + "code": "#include \nint main() {\n int i, sum = 0;\n for (i = 1; i <= 5; i++) {\n sum += i;\n }\n printf(\"Sum of 1 to 5 is: %d\\n\", sum);\n return 0;\n}" +}' + +COMPLEX_TEST=$(curl -s -X POST http://localhost:5000/compile \ + -H "Content-Type: application/json" \ + -d "$COMPLEX_CODE") + +if echo "$COMPLEX_TEST" | grep -q '"success":true' && echo "$COMPLEX_TEST" | grep -q "Sum of 1 to 5"; then + echo "✓ Test 4 PASSED: Complex code compilation works correctly" +else + echo "✗ Test 4 FAILED: Complex code compilation not working" + echo " Response: $COMPLEX_TEST" +fi + +# Test 5: Check if all expected lesson files are accessible +echo "Test 5: Checking lesson accessibility..." +LESSON_FILES=("welcome.md" "hello_world.md" "introduction_to_c.md" "arrays.md") +ALL_LESSONS_ACCESSIBLE=true + +for lesson in "${LESSON_FILES[@]}"; do + lesson_check=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:5000/lesson/$lesson") + if [ "$lesson_check" -ne 200 ]; then + echo "✗ Lesson $lesson returned HTTP $lesson_check" + ALL_LESSONS_ACCESSIBLE=false + fi +done + +if [ "$ALL_LESSONS_ACCESSIBLE" = true ]; then + echo "✓ Test 5 PASSED: All test lessons are accessible" +else + echo "✗ Test 5 FAILED: Some lessons are not accessible" +fi + +# Test 6: Check for errors in container logs +echo "Test 6: Checking container logs for errors..." +LOG_ERRORS=$(podman logs elemes_lms-c_1 2>&1 | grep -i error | wc -l) +if [ $LOG_ERRORS -eq 0 ]; then + echo "✓ Test 6 PASSED: No errors found in container logs" +else + echo "✗ Test 6 FAILED: Errors found in container logs" + podman logs elemes_lms-c_1 | grep -i error +fi + +# Stop the application +echo "Stopping the application..." +cd .. +./stop.sh +cd test + +echo "All tests completed!" \ No newline at end of file