From 9527cba22490f89d95101866ce4a2b191810ce53 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Mon, 20 Apr 2026 14:40:51 +0300 Subject: [PATCH 01/24] Add GitHub Actions workflow for Dokku deploys --- .github/workflows/deploy.yml | 149 +++++++++++++++++++++++++++-------- 1 file changed, 114 insertions(+), 35 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 764d582..de9b377 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,43 +1,122 @@ -name: Deploy to EC2 +name: CI and Dokku Deploy on: push: - branches: [ main ] + branches: + - main pull_request: - branches: [ main ] + workflow_dispatch: + inputs: + environment: + description: Deployment target + required: true + default: prod + type: choice + options: + - dev + - prod + ref: + description: Git ref to deploy + required: false + default: main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - deploy: + validate: + name: Validate Docker image + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build application image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + load: true + tags: vulnerability-index-tool:ci + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke-check Python imports inside image + run: docker run --rm vulnerability-index-tool:ci python -m compileall config dashboard manage.py + + deploy-dev: + name: Deploy to Dokku DEV + runs-on: ubuntu-latest + needs: validate + if: | + (github.event_name == 'push' && github.ref == 'refs/heads/main') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'dev') + environment: dev + concurrency: + group: deploy-dev + cancel-in-progress: false + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + + - name: Trust Dokku host key + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts + + - name: Configure Dokku git remote + run: git remote add dokku "dokku@${{ secrets.DOKKU_HOST }}:${{ secrets.DOKKU_APP_NAME }}" + + - name: Deploy current commit to Dokku DEV + run: git push dokku "HEAD:master" --force + + deploy-prod: + name: Deploy to Dokku PROD runs-on: ubuntu-latest - + needs: validate + if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' + environment: prod + concurrency: + group: deploy-prod + cancel-in-progress: false + 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: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + + - name: Trust Dokku host key + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts + + - name: Configure Dokku git remote + run: git remote add dokku "dokku@${{ secrets.DOKKU_HOST }}:${{ secrets.DOKKU_APP_NAME }}" + + - name: Deploy selected ref to Dokku PROD + run: git push dokku "HEAD:master" --force From f60cd34c3cc50a845c2af54f0c7cbae0d1162db4 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Mon, 20 Apr 2026 15:16:58 +0300 Subject: [PATCH 02/24] Fix GitHub Actions disk exhaustion during validation --- .github/workflows/deploy.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index de9b377..1fc1376 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,6 +39,14 @@ jobs: with: ref: ${{ github.event.inputs.ref || github.ref }} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Smoke-check Python syntax + run: python -m compileall config dashboard manage.py + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -47,14 +55,10 @@ jobs: with: context: . file: ./Dockerfile - load: true - tags: vulnerability-index-tool:ci + push: false cache-from: type=gha cache-to: type=gha,mode=max - - name: Smoke-check Python imports inside image - run: docker run --rm vulnerability-index-tool:ci python -m compileall config dashboard manage.py - deploy-dev: name: Deploy to Dokku DEV runs-on: ubuntu-latest From 28e28b86a3a39c6668c21cb5d052ccd62c7cfa3d Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 10:05:26 +0300 Subject: [PATCH 03/24] Deploy Dokku from Docker Hub image --- .github/workflows/deploy.yml | 74 +++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1fc1376..e76f876 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,15 +23,20 @@ on: permissions: contents: read +env: + IMAGE_REPOSITORY: ${{ vars.DOCKERHUB_IMAGE }} + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - validate: - name: Validate Docker image + build: + name: Build server image runs-on: ubuntu-latest timeout-minutes: 60 + outputs: + image_ref: ${{ steps.image.outputs.image_ref }} steps: - name: Check out repository @@ -50,19 +55,44 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_REPOSITORY }} + tags: | + type=sha,prefix=sha- + type=raw,value=latest,enable={{is_default_branch}} + - name: Build application image + id: build uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile - push: false + pull: true + 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 + - name: Capture immutable image reference + id: image + if: github.event_name != 'pull_request' + run: echo "image_ref=docker.io/${IMAGE_REPOSITORY}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" + deploy-dev: name: Deploy to Dokku DEV runs-on: ubuntu-latest - needs: validate + needs: build if: | (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'dev') @@ -72,11 +102,6 @@ jobs: cancel-in-progress: false steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 with: @@ -87,16 +112,20 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - - name: Configure Dokku git remote - run: git remote add dokku "dokku@${{ secrets.DOKKU_HOST }}:${{ secrets.DOKKU_APP_NAME }}" + - name: Log Dokku app into Docker Hub + run: | + ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - - name: Deploy current commit to Dokku DEV - run: git push dokku "HEAD:master" --force + - name: Release Docker Hub image on Dokku DEV + run: | + ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" deploy-prod: name: Deploy to Dokku PROD runs-on: ubuntu-latest - needs: validate + needs: build if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' environment: prod concurrency: @@ -104,11 +133,6 @@ jobs: cancel-in-progress: false steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - - name: Start SSH agent uses: webfactory/ssh-agent@v0.9.0 with: @@ -119,8 +143,12 @@ jobs: mkdir -p ~/.ssh ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - - name: Configure Dokku git remote - run: git remote add dokku "dokku@${{ secrets.DOKKU_HOST }}:${{ secrets.DOKKU_APP_NAME }}" + - name: Log Dokku app into Docker Hub + run: | + ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - - name: Deploy selected ref to Dokku PROD - run: git push dokku "HEAD:master" --force + - name: Release Docker Hub image on Dokku PROD + run: | + ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" From 593a5065dc112ec9e8b3ba6591aa0a5f690efd0f Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 14:32:04 +0300 Subject: [PATCH 04/24] Use a single Docker Hub to Dokku deployment flow --- .github/workflows/deploy.yml | 47 +++--------------------------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e76f876..bd7da92 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,14 +7,6 @@ on: pull_request: workflow_dispatch: inputs: - environment: - description: Deployment target - required: true - default: prod - type: choice - options: - - dev - - prod ref: description: Git ref to deploy required: false @@ -89,44 +81,11 @@ jobs: if: github.event_name != 'pull_request' run: echo "image_ref=docker.io/${IMAGE_REPOSITORY}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" - deploy-dev: - name: Deploy to Dokku DEV - runs-on: ubuntu-latest - needs: build - if: | - (github.event_name == 'push' && github.ref == 'refs/heads/main') || - (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'dev') - environment: dev - concurrency: - group: deploy-dev - cancel-in-progress: false - - steps: - - name: Start SSH agent - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - - - name: Trust Dokku host key - run: | - mkdir -p ~/.ssh - ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - - - name: Log Dokku app into Docker Hub - run: | - ssh "dokku@${{ secrets.DOKKU_HOST }}" \ - "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - - - name: Release Docker Hub image on Dokku DEV - run: | - ssh "dokku@${{ secrets.DOKKU_HOST }}" \ - "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" - deploy-prod: - name: Deploy to Dokku PROD + name: Deploy to Dokku runs-on: ubuntu-latest needs: build - if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' environment: prod concurrency: group: deploy-prod @@ -148,7 +107,7 @@ jobs: ssh "dokku@${{ secrets.DOKKU_HOST }}" \ "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - - name: Release Docker Hub image on Dokku PROD + - name: Release Docker Hub image on Dokku run: | ssh "dokku@${{ secrets.DOKKU_HOST }}" \ "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" From 215fe335bbf5bf1bc777738aad07b9222b3d4aa0 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 14:37:56 +0300 Subject: [PATCH 05/24] Enable deployment workflow test branch --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bd7da92..ab2c991 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - chore/dokku-github-actions pull_request: workflow_dispatch: inputs: From 9858faaedfa2db00ba31c1368e365af21648f82b Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 14:57:00 +0300 Subject: [PATCH 06/24] Build deployment image on ARM runner --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ab2c991..7af3446 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,7 +26,7 @@ concurrency: jobs: build: name: Build server image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm timeout-minutes: 60 outputs: image_ref: ${{ steps.image.outputs.image_ref }} From 21a55c11b96a443ea5bfa4b47df3038e9fdaf65b Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 15:41:12 +0300 Subject: [PATCH 07/24] Allow deployment test from workflow PR branch --- .github/workflows/deploy.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7af3446..c9044fa 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,7 +49,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} @@ -71,7 +71,7 @@ jobs: context: . file: ./Dockerfile pull: true - push: ${{ github.event_name != 'pull_request' }} + push: ${{ github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha @@ -79,14 +79,17 @@ jobs: - name: Capture immutable image reference id: image - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' run: echo "image_ref=docker.io/${IMAGE_REPOSITORY}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" deploy-prod: name: Deploy to Dokku runs-on: ubuntu-latest needs: build - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: | + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.head_ref == 'chore/dokku-github-actions') environment: prod concurrency: group: deploy-prod From cfe2f38aa2d3f88936486f01b23ba3a325e512f2 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 15:50:42 +0300 Subject: [PATCH 08/24] Set default Docker Hub image repository --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c9044fa..b1b5d85 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - IMAGE_REPOSITORY: ${{ vars.DOCKERHUB_IMAGE }} + IMAGE_REPOSITORY: ${{ vars.DOCKERHUB_IMAGE || 'codeforafrica/vulnerability-index-tool' }} concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} From 459d2c0d9662311be9a39b61758b6cfa0ec56de0 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 15:59:04 +0300 Subject: [PATCH 09/24] Add semantic version tags for Docker images --- .github/workflows/deploy.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1b5d85..c8b5b01 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,6 +12,10 @@ on: description: Git ref to deploy required: false default: main + version: + description: SemVer image version to publish, for example 1.0.0 + required: false + type: string permissions: contents: read @@ -62,6 +66,9 @@ jobs: images: ${{ env.IMAGE_REPOSITORY }} tags: | type=sha,prefix=sha- + type=semver,pattern={{version}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + type=semver,pattern={{major}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} type=raw,value=latest,enable={{is_default_branch}} - name: Build application image From 48045ea04dfd9f132e3aa188d5df30baaf42a368 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 16:10:37 +0300 Subject: [PATCH 10/24] Load Dokku deploy key without ssh-agent --- .github/workflows/deploy.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8b5b01..56bf173 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -103,22 +103,19 @@ jobs: cancel-in-progress: false steps: - - name: Start SSH agent - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - - - name: Trust Dokku host key + - name: Configure SSH run: | mkdir -p ~/.ssh + printf '%s\n' '${{ secrets.DOKKU_SSH_PRIVATE_KEY }}' | sed 's/\\n/\n/g' > ~/.ssh/dokku_deploy_key + chmod 600 ~/.ssh/dokku_deploy_key ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - name: Log Dokku app into Docker Hub run: | - ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + ssh -i ~/.ssh/dokku_deploy_key "dokku@${{ secrets.DOKKU_HOST }}" \ "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - name: Release Docker Hub image on Dokku run: | - ssh "dokku@${{ secrets.DOKKU_HOST }}" \ + ssh -i ~/.ssh/dokku_deploy_key "dokku@${{ secrets.DOKKU_HOST }}" \ "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" From a535b5f807d8dffc24394819d38993666dd5473a Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 16:27:10 +0300 Subject: [PATCH 11/24] Validate Dokku deploy key before SSH --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 56bf173..84becc3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -108,6 +108,7 @@ jobs: mkdir -p ~/.ssh printf '%s\n' '${{ secrets.DOKKU_SSH_PRIVATE_KEY }}' | sed 's/\\n/\n/g' > ~/.ssh/dokku_deploy_key chmod 600 ~/.ssh/dokku_deploy_key + ssh-keygen -y -f ~/.ssh/dokku_deploy_key > ~/.ssh/dokku_deploy_key.pub ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - name: Log Dokku app into Docker Hub From 583b81eb93451c243dcd2c687248165f49844f1a Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 17:01:25 +0300 Subject: [PATCH 12/24] Use base64 encoded Dokku deploy key --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 84becc3..90ac345 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -106,7 +106,7 @@ jobs: - name: Configure SSH run: | mkdir -p ~/.ssh - printf '%s\n' '${{ secrets.DOKKU_SSH_PRIVATE_KEY }}' | sed 's/\\n/\n/g' > ~/.ssh/dokku_deploy_key + printf '%s' '${{ secrets.DOKKU_SSH_PRIVATE_KEY_B64 }}' | base64 -d > ~/.ssh/dokku_deploy_key chmod 600 ~/.ssh/dokku_deploy_key ssh-keygen -y -f ~/.ssh/dokku_deploy_key > ~/.ssh/dokku_deploy_key.pub ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts From 333d1184105ea54b1a2d2f43e9f59dfe1c56e78d Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 22:33:48 +0300 Subject: [PATCH 13/24] Support base64 or multiline Dokku deploy key --- .github/workflows/deploy.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 90ac345..5eb397a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -104,9 +104,19 @@ jobs: steps: - name: Configure SSH + env: + DOKKU_SSH_PRIVATE_KEY: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + DOKKU_SSH_PRIVATE_KEY_B64: ${{ secrets.DOKKU_SSH_PRIVATE_KEY_B64 }} run: | mkdir -p ~/.ssh - printf '%s' '${{ secrets.DOKKU_SSH_PRIVATE_KEY_B64 }}' | base64 -d > ~/.ssh/dokku_deploy_key + if [ -n "$DOKKU_SSH_PRIVATE_KEY_B64" ]; then + printf '%s' "$DOKKU_SSH_PRIVATE_KEY_B64" | tr -d '[:space:]' | base64 -d > ~/.ssh/dokku_deploy_key + elif [ -n "$DOKKU_SSH_PRIVATE_KEY" ]; then + printf '%s\n' "$DOKKU_SSH_PRIVATE_KEY" | sed 's/\\n/\n/g' > ~/.ssh/dokku_deploy_key + else + echo "DOKKU_SSH_PRIVATE_KEY_B64 or DOKKU_SSH_PRIVATE_KEY must be set" + exit 1 + fi chmod 600 ~/.ssh/dokku_deploy_key ssh-keygen -y -f ~/.ssh/dokku_deploy_key > ~/.ssh/dokku_deploy_key.pub ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts From e10170ca2091e05d5dd35fda06fc1437f423b7d6 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 22:46:20 +0300 Subject: [PATCH 14/24] Use Dokku action for ARM image deployment --- .github/workflows/deploy.yml | 131 +++++++++++++++-------------------- 1 file changed, 56 insertions(+), 75 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5eb397a..202eb71 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: CI and Dokku Deploy +name: VI | Deploy | Dokku on: push: @@ -20,25 +20,33 @@ on: permissions: contents: read -env: - IMAGE_REPOSITORY: ${{ vars.DOCKERHUB_IMAGE || 'codeforafrica/vulnerability-index-tool' }} - concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }} @ ${{ github.ref }}" cancel-in-progress: true +env: + APP_NAME: ${{ secrets.DOKKU_APP_NAME }} + DOKKU_REMOTE_BRANCH: "master" + DOKKU_REMOTE_URL: "ssh://dokku@${{ secrets.DOKKU_HOST }}" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_NAME: ${{ vars.DOCKERHUB_IMAGE || 'codeforafrica/vulnerability-index-tool' }} + jobs: - build: - name: Build server image + deploy: + name: Build, Push, and Deploy runs-on: ubuntu-24.04-arm + environment: prod timeout-minutes: 60 - outputs: - image_ref: ${{ steps.image.outputs.image_ref }} + if: | + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.head_ref == 'chore/dokku-github-actions') steps: - - name: Check out repository + - name: Checkout uses: actions/checkout@v4 with: + fetch-depth: 0 ref: ${{ github.event.inputs.ref || github.ref }} - name: Set up Python @@ -49,84 +57,57 @@ jobs: - name: Smoke-check Python syntax run: python -m compileall config dashboard manage.py + - name: Set Docker image version + id: image-version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + else + echo "version=sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + fi + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' - uses: docker/login-action@v3 + - name: Cache Docker layers + uses: actions/cache@v4 with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + key: ${{ runner.os }}-buildx-${{ github.sha }} + path: ${{ runner.temp }}/.buildx-cache + restore-keys: | + ${{ runner.os }}-buildx- - - name: Generate image metadata - id: meta - uses: docker/metadata-action@v5 + - name: Login to DockerHub + uses: docker/login-action@v3 with: - images: ${{ env.IMAGE_REPOSITORY }} - tags: | - type=sha,prefix=sha- - type=semver,pattern={{version}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - type=semver,pattern={{major}}.{{minor}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - type=semver,pattern={{major}},value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} - type=raw,value=latest,enable={{is_default_branch}} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + username: ${{ secrets.DOCKER_HUB_USERNAME }} - - name: Build application image - id: build + - name: Build and push Docker image uses: docker/build-push-action@v6 with: + cache-from: type=local,src=${{ runner.temp }}/.buildx-cache + cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new,mode=max context: . file: ./Dockerfile - pull: true - push: ${{ github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Capture immutable image reference - id: image - if: github.event_name != 'pull_request' || github.head_ref == 'chore/dokku-github-actions' - run: echo "image_ref=docker.io/${IMAGE_REPOSITORY}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT" + platforms: linux/arm64 + push: true + tags: ${{ env.IMAGE_NAME }}:${{ steps.image-version.outputs.version }} - deploy-prod: - name: Deploy to Dokku - runs-on: ubuntu-latest - needs: build - if: | - github.event_name == 'push' || - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.head_ref == 'chore/dokku-github-actions') - environment: prod - concurrency: - group: deploy-prod - cancel-in-progress: false - - steps: - - name: Configure SSH - env: - DOKKU_SSH_PRIVATE_KEY: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - DOKKU_SSH_PRIVATE_KEY_B64: ${{ secrets.DOKKU_SSH_PRIVATE_KEY_B64 }} + - name: Move cache run: | - mkdir -p ~/.ssh - if [ -n "$DOKKU_SSH_PRIVATE_KEY_B64" ]; then - printf '%s' "$DOKKU_SSH_PRIVATE_KEY_B64" | tr -d '[:space:]' | base64 -d > ~/.ssh/dokku_deploy_key - elif [ -n "$DOKKU_SSH_PRIVATE_KEY" ]; then - printf '%s\n' "$DOKKU_SSH_PRIVATE_KEY" | sed 's/\\n/\n/g' > ~/.ssh/dokku_deploy_key + CACHE_DIR="${{ runner.temp }}/.buildx-cache" + NEW_CACHE_DIR="${{ runner.temp }}/.buildx-cache-new" + if [ -d "$NEW_CACHE_DIR" ]; then + rm -rf "$CACHE_DIR" + mv "$NEW_CACHE_DIR" "$CACHE_DIR" else - echo "DOKKU_SSH_PRIVATE_KEY_B64 or DOKKU_SSH_PRIVATE_KEY must be set" - exit 1 + echo "No new cache directory found at $NEW_CACHE_DIR; skipping move." fi - chmod 600 ~/.ssh/dokku_deploy_key - ssh-keygen -y -f ~/.ssh/dokku_deploy_key > ~/.ssh/dokku_deploy_key.pub - ssh-keyscan -H "${{ secrets.DOKKU_HOST }}" >> ~/.ssh/known_hosts - - name: Log Dokku app into Docker Hub - run: | - ssh -i ~/.ssh/dokku_deploy_key "dokku@${{ secrets.DOKKU_HOST }}" \ - "echo '${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}' | registry:login '${{ secrets.DOKKU_APP_NAME }}' --password-stdin docker.io '${{ secrets.DOCKER_HUB_USERNAME }}'" - - - name: Release Docker Hub image on Dokku - run: | - ssh -i ~/.ssh/dokku_deploy_key "dokku@${{ secrets.DOKKU_HOST }}" \ - "git:from-image --force '${{ secrets.DOKKU_APP_NAME }}' '${{ needs.build.outputs.image_ref }}' 'GitHub Actions' 'github-actions@users.noreply.github.com'" + - name: Push to Dokku + uses: dokku/github-action@v1.9.0 + with: + git_remote_url: ${{ env.DOKKU_REMOTE_URL }}/${{ env.APP_NAME }} + ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + deploy_docker_image: ${{ env.IMAGE_NAME }}:${{ steps.image-version.outputs.version }} From d32b506fba8fc7d23cf0df2b1618c89dc21c13db Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 21 Apr 2026 23:37:35 +0300 Subject: [PATCH 15/24] Simplify Dokku image deploy workflow --- .github/workflows/deploy.yml | 58 ++++++++++-------------------------- 1 file changed, 15 insertions(+), 43 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 202eb71..556e074 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,19 +1,14 @@ -name: VI | Deploy | Dokku +name: VI | Build and Deploy on: push: branches: - main - chore/dokku-github-actions - pull_request: workflow_dispatch: inputs: - ref: - description: Git ref to deploy - required: false - default: main version: - description: SemVer image version to publish, for example 1.0.0 + description: Image version to publish and deploy, for example 1.0.0 required: false type: string @@ -26,9 +21,7 @@ concurrency: env: APP_NAME: ${{ secrets.DOKKU_APP_NAME }} - DOKKU_REMOTE_BRANCH: "master" - DOKKU_REMOTE_URL: "ssh://dokku@${{ secrets.DOKKU_HOST }}" - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOKKU_REMOTE_URL: ssh://dokku@${{ secrets.DOKKU_HOST }}/${{ secrets.DOKKU_APP_NAME }} IMAGE_NAME: ${{ vars.DOCKERHUB_IMAGE || 'codeforafrica/vulnerability-index-tool' }} jobs: @@ -37,17 +30,12 @@ jobs: runs-on: ubuntu-24.04-arm environment: prod timeout-minutes: 60 - if: | - github.event_name == 'push' || - github.event_name == 'workflow_dispatch' || - (github.event_name == 'pull_request' && github.head_ref == 'chore/dokku-github-actions') steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ github.event.inputs.ref || github.ref }} - name: Set up Python uses: actions/setup-python@v5 @@ -57,26 +45,21 @@ jobs: - name: Smoke-check Python syntax run: python -m compileall config dashboard manage.py - - name: Set Docker image version - id: image-version + - name: Set image version + id: image run: | if [ -n "${{ github.event.inputs.version }}" ]; then - echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + VERSION="${{ github.event.inputs.version }}" else - echo "version=sha-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + VERSION="sha-$(printf '%s' "$GITHUB_SHA" | cut -c1-7)" fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "image=${IMAGE_NAME}:${VERSION}" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Cache Docker layers - uses: actions/cache@v4 - with: - key: ${{ runner.os }}-buildx-${{ github.sha }} - path: ${{ runner.temp }}/.buildx-cache - restore-keys: | - ${{ runner.os }}-buildx- - - name: Login to DockerHub uses: docker/login-action@v3 with: @@ -86,28 +69,17 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v6 with: - cache-from: type=local,src=${{ runner.temp }}/.buildx-cache - cache-to: type=local,dest=${{ runner.temp }}/.buildx-cache-new,mode=max + cache-from: type=gha + cache-to: type=gha,mode=max context: . file: ./Dockerfile platforms: linux/arm64 push: true - tags: ${{ env.IMAGE_NAME }}:${{ steps.image-version.outputs.version }} - - - name: Move cache - run: | - CACHE_DIR="${{ runner.temp }}/.buildx-cache" - NEW_CACHE_DIR="${{ runner.temp }}/.buildx-cache-new" - if [ -d "$NEW_CACHE_DIR" ]; then - rm -rf "$CACHE_DIR" - mv "$NEW_CACHE_DIR" "$CACHE_DIR" - else - echo "No new cache directory found at $NEW_CACHE_DIR; skipping move." - fi + tags: ${{ steps.image.outputs.image }} - name: Push to Dokku uses: dokku/github-action@v1.9.0 with: - git_remote_url: ${{ env.DOKKU_REMOTE_URL }}/${{ env.APP_NAME }} + git_remote_url: ${{ env.DOKKU_REMOTE_URL }} ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - deploy_docker_image: ${{ env.IMAGE_NAME }}:${{ steps.image-version.outputs.version }} + deploy_docker_image: ${{ steps.image.outputs.image }} From 2ea126d1db510f6899fc3de352020ff09c270de4 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Wed, 22 Apr 2026 22:09:21 +0300 Subject: [PATCH 16/24] Read runtime settings from environment --- config/settings.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/config/settings.py b/config/settings.py index 51f89e5..92c9c6d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -4,6 +4,7 @@ # 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 @@ -18,14 +19,13 @@ SECRET_KEY = "(@lhxdh^3z1aea9xjny21q^0crno_h48*3!y7en!g#x(5^*zad" # 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 +46,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -58,7 +59,7 @@ TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'BACKEND': 'django.template.backends.djangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { @@ -102,6 +103,7 @@ STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' @@ -119,7 +121,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', } From 5e0c835d982ca69c742ab34248ec98d54e349e57 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Wed, 22 Apr 2026 22:10:33 +0300 Subject: [PATCH 17/24] Fix Django template backend setting --- config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index 92c9c6d..faef12e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -59,7 +59,7 @@ TEMPLATES = [ { - 'BACKEND': 'django.template.backends.djangoTemplates', + 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { From c87dd1920aa364329e8b94bb138aef97d8f1257d Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Wed, 22 Apr 2026 22:18:22 +0300 Subject: [PATCH 18/24] Add whitenoise runtime dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) 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 From 9884535628ea756876854e25c518949141fa39cb Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Wed, 22 Apr 2026 22:28:00 +0300 Subject: [PATCH 19/24] Use non-manifest static files storage --- config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index faef12e..0e7b697 100644 --- a/config/settings.py +++ b/config/settings.py @@ -103,7 +103,7 @@ STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' From e243bdafc16429e4929e1b464051bcfbb07477c2 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Wed, 22 Apr 2026 23:19:25 +0300 Subject: [PATCH 20/24] Collect static files in Docker image --- Dockerfile | 63 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 26 deletions(-) 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"] From cd52a20c973ff92be3ae3711ab810fed7853acb2 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 5 May 2026 13:17:58 +0300 Subject: [PATCH 21/24] Address PR review: simplify SECRET_KEY, update actions to latest versions - Simplify SECRET_KEY to use os.getenv with empty string fallback - Replace manual image versioning with docker/metadata-action@v6 - Bump dokku/github-action from v1.9.0 to v1.10.0 - Update all GitHub Actions to latest major versions --- .github/workflows/deploy.yml | 35 +++++++++++++++-------------------- config/settings.py | 11 +---------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 556e074..19edb7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,41 +33,35 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Smoke-check Python syntax run: python -m compileall config dashboard manage.py - - name: Set image version - id: image - run: | - if [ -n "${{ github.event.inputs.version }}" ]; then - VERSION="${{ github.event.inputs.version }}" - else - VERSION="sha-$(printf '%s' "$GITHUB_SHA" | cut -c1-7)" - fi - - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "image=${IMAGE_NAME}:${VERSION}" >> "$GITHUB_OUTPUT" - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v3 + 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@v6 + uses: docker/build-push-action@v7 with: cache-from: type=gha cache-to: type=gha,mode=max @@ -75,11 +69,12 @@ jobs: file: ./Dockerfile platforms: linux/arm64 push: true - tags: ${{ steps.image.outputs.image }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} - name: Push to Dokku - uses: dokku/github-action@v1.9.0 + 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.image.outputs.image }} + deploy_docker_image: ${{ steps.meta.outputs.tags }} diff --git a/config/settings.py b/config/settings.py index 0e7b697..65014c0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -7,16 +7,7 @@ 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' From 47c672196077a7e1568e06ead676225fe21030ef Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 5 May 2026 13:43:36 +0300 Subject: [PATCH 22/24] Remove redundant Python setup and compileall steps from workflow --- .github/workflows/deploy.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 19edb7b..c461e4f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,14 +37,6 @@ jobs: with: fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - - name: Smoke-check Python syntax - run: python -m compileall config dashboard manage.py - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 From 0b6eaa2e6154b9c01d08ed9cb860f84d508a3e1f Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 5 May 2026 13:57:09 +0300 Subject: [PATCH 23/24] Fix deploy_docker_image to pass single tag to Dokku --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c461e4f..5c0a51c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,4 +69,4 @@ jobs: with: git_remote_url: ${{ env.DOKKU_REMOTE_URL }} ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - deploy_docker_image: ${{ steps.meta.outputs.tags }} + deploy_docker_image: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} From 06fda2b483e7d0c9adab26b8000f8e246d4956d5 Mon Sep 17 00:00:00 2001 From: Vincent Otieno Date: Tue, 5 May 2026 14:02:48 +0300 Subject: [PATCH 24/24] Revert deploy_docker_image to use steps.meta.outputs.tags --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5c0a51c..c461e4f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,4 +69,4 @@ jobs: with: git_remote_url: ${{ env.DOKKU_REMOTE_URL }} ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} - deploy_docker_image: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} + deploy_docker_image: ${{ steps.meta.outputs.tags }}