Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9527cba
Add GitHub Actions workflow for Dokku deploys
VinneyJ Apr 20, 2026
f60cd34
Fix GitHub Actions disk exhaustion during validation
VinneyJ Apr 20, 2026
28e28b8
Deploy Dokku from Docker Hub image
VinneyJ Apr 21, 2026
593a506
Use a single Docker Hub to Dokku deployment flow
VinneyJ Apr 21, 2026
215fe33
Enable deployment workflow test branch
VinneyJ Apr 21, 2026
9858faa
Build deployment image on ARM runner
VinneyJ Apr 21, 2026
21a55c1
Allow deployment test from workflow PR branch
VinneyJ Apr 21, 2026
cfe2f38
Set default Docker Hub image repository
VinneyJ Apr 21, 2026
459d2c0
Add semantic version tags for Docker images
VinneyJ Apr 21, 2026
48045ea
Load Dokku deploy key without ssh-agent
VinneyJ Apr 21, 2026
a535b5f
Validate Dokku deploy key before SSH
VinneyJ Apr 21, 2026
583b81e
Use base64 encoded Dokku deploy key
VinneyJ Apr 21, 2026
333d118
Support base64 or multiline Dokku deploy key
VinneyJ Apr 21, 2026
e10170c
Use Dokku action for ARM image deployment
VinneyJ Apr 21, 2026
d32b506
Simplify Dokku image deploy workflow
VinneyJ Apr 21, 2026
2ea126d
Read runtime settings from environment
VinneyJ Apr 22, 2026
5e0c835
Fix Django template backend setting
VinneyJ Apr 22, 2026
c87dd19
Add whitenoise runtime dependency
VinneyJ Apr 22, 2026
9884535
Use non-manifest static files storage
VinneyJ Apr 22, 2026
e243bda
Collect static files in Docker image
VinneyJ Apr 22, 2026
cd52a20
Address PR review: simplify SECRET_KEY, update actions to latest vers…
VinneyJ May 5, 2026
47c6721
Remove redundant Python setup and compileall steps from workflow
VinneyJ May 5, 2026
0b6eaa2
Fix deploy_docker_image to pass single tag to Dokku
VinneyJ May 5, 2026
06fda2b
Revert deploy_docker_image to use steps.meta.outputs.tags
VinneyJ May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 65 additions & 36 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
VinneyJ marked this conversation as resolved.
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 }}
63 changes: 37 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# Use Debian-based slim image
FROM python:3.11-slim
FROM python:3.11-slim AS builder
Comment thread
maquchizi marked this conversation as resolved.

# 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++ \
Expand All @@ -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"]
29 changes: 11 additions & 18 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
VinneyJ marked this conversation as resolved.

# 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 = [
Expand All @@ -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',
Expand Down Expand Up @@ -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'

Expand All @@ -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',
}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down