diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 764d582..c461e4f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,43 +1,72 @@ -name: Deploy to EC2 +name: VI | Build and Deploy on: push: - branches: [ main ] - pull_request: - branches: [ main ] + branches: + - main + - chore/dokku-github-actions + workflow_dispatch: + inputs: + version: + description: Image version to publish and deploy, for example 1.0.0 + required: false + type: string + +permissions: + contents: read + +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: true + +env: + APP_NAME: ${{ secrets.DOKKU_APP_NAME }} + DOKKU_REMOTE_URL: ssh://dokku@${{ secrets.DOKKU_HOST }}/${{ secrets.DOKKU_APP_NAME }} + IMAGE_NAME: ${{ vars.DOCKERHUB_IMAGE || 'codeforafrica/vulnerability-index-tool' }} jobs: deploy: - runs-on: ubuntu-latest - + name: Build, Push, and Deploy + runs-on: ubuntu-24.04-arm + environment: prod + timeout-minutes: 60 + steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install django psycopg2-binary boto3 requests beautifulsoup4 pandas numpy torch transformers peft scikit-learn joblib langdetect cleanlab python-dotenv - - - name: Run tests - run: | - # Add your test commands here - python -m compileall . - - - name: Deploy to EC2 - uses: appleboy/ssh-action@master - with: - host: ${{ secrets.EC2_HOST }} - username: ${{ secrets.EC2_USERNAME }} - key: ${{ secrets.EC2_PRIVATE_KEY }} - script: | - cd ~/Vulnerability_index_tool - git pull origin main - source venv/bin/activate - pip install -r requirements.txt - python manage.py migrate - sudo systemctl restart gunicorn # or your service name + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + username: ${{ secrets.DOCKER_HUB_USERNAME }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + cache-from: type=gha + cache-to: type=gha,mode=max + context: . + file: ./Dockerfile + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Push to Dokku + uses: dokku/github-action@v1.10.0 + with: + git_remote_url: ${{ env.DOKKU_REMOTE_URL }} + ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + deploy_docker_image: ${{ steps.meta.outputs.tags }} diff --git a/Dockerfile b/Dockerfile index ed9c9c7..20d9fcf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,17 @@ -# Use Debian-based slim image -FROM python:3.11-slim +FROM python:3.11-slim AS builder -# Set environment variables for better Python behavior -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + VIRTUAL_ENV=/opt/venv WORKDIR /app -# Install system dependencies for compilation -# Complete list of required packages for pycairo and other dependencies +RUN python -m venv "$VIRTUAL_ENV" +ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ gcc \ g++ \ @@ -20,32 +21,42 @@ RUN apt-get update && apt-get install -y \ libpq-dev \ && rm -rf /var/lib/apt/lists/* -# Upgrade pip, setuptools, and wheel -RUN pip install --upgrade pip setuptools wheel - -# Copy requirements.txt COPY requirements.txt . -# Install Python requirements using pip -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --upgrade pip setuptools wheel \ + && pip install -r requirements.txt -# Copy the rest of the project files -COPY . . +FROM python:3.11-slim AS runtime -# Expose the port the app runs on -EXPOSE 8000 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_NO_CACHE_DIR=1 \ + VIRTUAL_ENV=/opt/venv \ + PATH="/opt/venv/bin:$PATH" \ + HOME=/home/appuser \ + MPLCONFIGDIR=/home/appuser/.config/matplotlib -# Command to run the application with Gunicorn - -RUN pip install --upgrade pip setuptools wheel +WORKDIR /app -COPY requirements.txt . +RUN apt-get update && apt-get install -y --no-install-recommends \ + libcairo2 \ + libgirepository-1.0-1 \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* \ + && addgroup --system appgroup \ + && adduser --system --ingroup appgroup --home /home/appuser appuser \ + && mkdir -p /home/appuser/.config/matplotlib \ + && chown -R appuser:appgroup /home/appuser + +COPY --from=builder /opt/venv /opt/venv +COPY . . -RUN pip install --no-cache-dir -r requirements.txt +RUN python manage.py collectstatic --noinput \ + && chown -R appuser:appgroup /app -COPY . . +USER appuser EXPOSE 8000 - -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"] +CMD ["sh", "-c", "python manage.py migrate --noinput && python manage.py collectstatic --noinput && exec gunicorn --bind 0.0.0.0:8000 config.wsgi:application"] diff --git a/config/settings.py b/config/settings.py index 51f89e5..65014c0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -4,28 +4,19 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +load_dotenv(BASE_DIR / '.env') # SECURITY WARNING: keep the secret key used in production secret! -# Handle special characters in secret key -try: - # Try to get from environment variable first - SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'fallback-secret-key-for-development') - # If it's the fallback, use the actual key - if SECRET_KEY == 'fallback-secret-key-for-development': - SECRET_KEY = "(@lhxdh^3z1aea9xjny21q^0crno_h48*3!y7en!g#x(5^*zad" -except: - # Fallback if environment variable access fails - SECRET_KEY = "(@lhxdh^3z1aea9xjny21q^0crno_h48*3!y7en!g#x(5^*zad" +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', '') # SECURITY WARNING: Don't run with debug turned on in production! -#DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' -DEBUG = True -# Security: Allow specific hosts only +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' -# Get ALLOWED_HOSTS from environment variable, with fallback to specific IPs -import os - -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = [ + host.strip() + for host in os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') + if host.strip() +] # CSRF_TRUSTED_ORIGINS MUST have the scheme (http://) CSRF_TRUSTED_ORIGINS = [ @@ -46,6 +37,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -102,6 +94,7 @@ STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' @@ -119,7 +112,7 @@ CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', - 'LOCATION': 'redis://127.0.0.1:6379/1', # Adjust if Redis is on a different host/port + 'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'), 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } diff --git a/requirements.txt b/requirements.txt index 5043798..9e3f7cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ nltk # Web & Utilities Django==4.2.7 gunicorn==21.2.0 +whitenoise==6.9.0 boto3>=1.29.0 requests==2.31.0 beautifulsoup4==4.12.2