From 621ed6d54d04d66e5293a5934743443d3870e300 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Fri, 15 May 2026 08:19:53 -0700 Subject: [PATCH 1/8] initial docker setup --- .gitignore | 4 ++- app/portal/Dockerfile | 36 ++++++++++++++++++++++ app/portal/portal/entrypoint.sh | 31 +++++++++++++++++++ app/portal/providers/forms.py | 48 ++++++++++++++++------------- app/portal/requirements.txt | 1 - docker/docker-compose.yaml | 53 +++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 app/portal/Dockerfile create mode 100644 app/portal/portal/entrypoint.sh create mode 100644 docker/docker-compose.yaml diff --git a/.gitignore b/.gitignore index 9c091a6..67661a7 100644 --- a/.gitignore +++ b/.gitignore @@ -115,4 +115,6 @@ venv.bak/ # vagrant .vagrant/ -.DS_Store \ No newline at end of file +.DS_Store + +.env.dev \ No newline at end of file diff --git a/app/portal/Dockerfile b/app/portal/Dockerfile new file mode 100644 index 0000000..816c40e --- /dev/null +++ b/app/portal/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.12-slim + +# Prevent Python from writing .pyc files and enable unbuffered stdout/stderr +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV PIP_NO_CACHE_DIR=1 + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy system requirements +COPY requirements.txt /app/ + +# Upgrade pip and install dependencies +RUN pip install --upgrade pip setuptools wheel + +# Install dependencies +RUN pip install --upgrade pip && \ + pip install -r requirements.txt + +# Copy application code +COPY . /app/ + +COPY portal/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8000 + +ENV DJANGO_SETTINGS_MODULE=portal.settings + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/app/portal/portal/entrypoint.sh b/app/portal/portal/entrypoint.sh new file mode 100644 index 0000000..e5f0043 --- /dev/null +++ b/app/portal/portal/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# Exit on errors +set -e + +# If a SQL_HOST is provided, wait for Postgres to become available before running +# migrations. This prevents race conditions when using docker-compose where the +# web container starts before the DB is ready. +if [ -n "$SQL_HOST" ]; then + echo "Waiting for database at ${SQL_HOST}:${SQL_PORT:-5432}..." + # pg_isready is available after installing postgresql-client in the image + until pg_isready -h "$SQL_HOST" -p "${SQL_PORT:-5432}" >/dev/null 2>&1; do + echo "Postgres is unavailable - sleeping" + sleep 1 + done + echo "Postgres is up" +fi + +echo "Applying database migrations..." +python manage.py migrate --noinput +echo "Collecting static files..." +python manage.py collectstatic --noinput + +echo "loading initial data fixtures..." +python manage.py loaddata fixtures/providers_20210524.json + +echo "creating superuser if not exists..." +python manage.py createsuperuser + +echo "Starting python development server on :8000" +python manage.py runserver 0.0.0.0:8000 diff --git a/app/portal/providers/forms.py b/app/portal/providers/forms.py index e9e72fb..058c99e 100644 --- a/app/portal/providers/forms.py +++ b/app/portal/providers/forms.py @@ -1,25 +1,8 @@ from django import forms +from providers.models import DeliveryMethod, PoliticalSubregion class FilterForm(forms.Form): - def __init__(self,*args,**kwargs): - try: - product_details_choices = kwargs.pop('product_details') - self.base_fields['product_category'].choices = product_details_choices - self.base_fields['product_category'].initial = [ x[0] for x in product_details_choices ] - except Exception as e: - pass - try: - self.base_fields['capacity'].label = "%s per year" % kwargs.pop('production_capacity_unit') - except Exception as e: - pass - - super(FilterForm,self).__init__(*args,**kwargs) - - from providers.models import DeliveryMethod, PoliticalSubregion - deliveryMethodChoices = [(-1, 'Any')] + [(x.pk, str(x)) for x in DeliveryMethod.objects.all().order_by('name')] - availabilityChoices = [(-1, 'Anywhere')] + [(x.pk, str(x)) for x in PoliticalSubregion.objects.all().order_by('name')] - product_category = forms.MultipleChoiceField( widget=forms.CheckboxSelectMultiple, label='...by Product Details', @@ -35,13 +18,38 @@ def __init__(self,*args,**kwargs): distribution = forms.ChoiceField( initial="Any", label="Method of distribution", - choices=deliveryMethodChoices, + choices=[], required=False, ) availability = forms.ChoiceField( initial="Anywhere", label="Confirmed available in", - choices=availabilityChoices, + choices=[], required=False, ) + + def __init__(self,*args,**kwargs): + try: + product_details_choices = kwargs.pop('product_details') + except KeyError: + product_details_choices = None + try: + production_capacity_unit = kwargs.pop('production_capacity_unit') + except KeyError: + production_capacity_unit = None + + super(FilterForm,self).__init__(*args,**kwargs) + + if product_details_choices is not None: + self.fields['product_category'].choices = product_details_choices + self.fields['product_category'].initial = [x[0] for x in product_details_choices] + if production_capacity_unit is not None: + self.fields['capacity'].label = "%s per year" % production_capacity_unit + + self.fields['distribution'].choices = [(-1, 'Any')] + [ + (x.pk, str(x)) for x in DeliveryMethod.objects.all().order_by('name') + ] + self.fields['availability'].choices = [(-1, 'Anywhere')] + [ + (x.pk, str(x)) for x in PoliticalSubregion.objects.all().order_by('name') + ] diff --git a/app/portal/requirements.txt b/app/portal/requirements.txt index 16cd3e1..4e6f256 100644 --- a/app/portal/requirements.txt +++ b/app/portal/requirements.txt @@ -39,7 +39,6 @@ pexpect==4.6.0 phonenumbers==8.10.4 pickleshare==0.7.5 prompt-toolkit>=3.0.30 -psycopg2==2.9.10 psycopg2-binary==2.9.10 ptyprocess==0.6.0 py-moneyed==0.8.0 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..8d008e2 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,53 @@ +services: + db: + image: postgis/postgis:16-3.5-alpine + restart: always + platform: linux/amd64 + environment: + POSTGRES_DB: ${SQL_DATABASE} + POSTGRES_USER: ${SQL_USER} + POSTGRES_PASSWORD: ${SQL_PASSWORD} + volumes: + - oh4s:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${SQL_USER} -d ${SQL_DATABASE} -h localhost -p ${SQL_PORT}"] + interval: 10s + timeout: 5s + retries: 5 + + web: + build: + context: ../app/portal + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - path: .env.dev + required: true + ports: + - "8000:8000" + healthcheck: + test: ["CMD-SHELL", "python manage.py check --deploy 2>/dev/null || python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/\")' 2>/dev/null"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + environment: + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + DEBUG: ${DEBUG} + SQL_ENGINE: ${SQL_ENGINE} + SQL_HOST: ${SQL_HOST} + SQL_PORT: ${SQL_PORT} + SQL_DATABASE: ${SQL_DATABASE} + SQL_USER: ${SQL_USER} + SQL_PASSWORD: ${SQL_PASSWORD} + SECRET_KEY: ${SECRET_KEY} + depends_on: + db: + condition: service_healthy + volumes: + - ../app/portal:/usr/src/app + +volumes: + oh4s: \ No newline at end of file From 1a0c2b4a8dc759efa178bc22a4c510003375e45f Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Mon, 18 May 2026 10:04:33 -0700 Subject: [PATCH 2/8] add init db script --- .gitignore | 4 +++- docker/docker-compose.yaml | 2 ++ docker/init-db.sh | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100755 docker/init-db.sh diff --git a/.gitignore b/.gitignore index 67661a7..3358e10 100644 --- a/.gitignore +++ b/.gitignore @@ -117,4 +117,6 @@ venv.bak/ .DS_Store -.env.dev \ No newline at end of file +.env.dev + +db_dump.sql \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8d008e2..256d8cd 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -9,6 +9,8 @@ services: POSTGRES_PASSWORD: ${SQL_PASSWORD} volumes: - oh4s:/var/lib/postgresql/data + - ./db_dump.sql:/tmp/db_dump.sql:ro + - ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh ports: - "5432:5432" healthcheck: diff --git a/docker/init-db.sh b/docker/init-db.sh new file mode 100755 index 0000000..5ee9b5d --- /dev/null +++ b/docker/init-db.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# This script runs automatically when the PostgreSQL container is first initialized +# It will only run if the database is empty (first time setup) + +echo "Importing database dump..." + +# Import the SQL dump, ignoring meta-command errors +PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=0 < /tmp/db_dump.sql + +echo "Database import completed!" \ No newline at end of file From d6cba09740aebedb3d4dcf22b139c42de240244b Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Wed, 20 May 2026 09:28:51 -0700 Subject: [PATCH 3/8] update docs for docker local install --- README.md | 14 ++++++++++++++ docker/.env.template | 9 +++++++++ docker/docker-compose.yaml | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docker/.env.template diff --git a/README.md b/README.md index 8387b13..dda39eb 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ Oregon Harvest For Schools Portal git clone https://github.com/Ecotrust/OH4S_Proteins.git cd OH4S_Proteins ``` +## Choose your development environment: Vagrant or Docker + +## Docker + +- In order to run the application with production data, locate a database dump of a production install. Add the dump to the `docker` directory and name it `db_dump.sql`. This is necessary for the `init-db.sh` script to run correctly. + +- Create an copy the `.env.template` file to a `.env` file in the `docker` directory. Add your environment variables to that file. + +- Move to the docker directory: `cd docker` + +- Start the docker containers: `docker compose up` + +- View the app at `http://localhost:8000` + ## Vagrant ``` diff --git a/docker/.env.template b/docker/.env.template new file mode 100644 index 0000000..9dab048 --- /dev/null +++ b/docker/.env.template @@ -0,0 +1,9 @@ +DEBUG= +SECRET_KEY= +ALLOWED_HOSTS= +SQL_ENGINE=django.contrib.gis.db.backends.postgis +SQL_DATABASE= +SQL_USER= +SQL_PASSWORD= +SQL_HOST= +SQL_PORT= \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 256d8cd..db49dac 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -25,7 +25,7 @@ services: dockerfile: Dockerfile restart: unless-stopped env_file: - - path: .env.dev + - path: .env required: true ports: - "8000:8000" From f5947256a9e027463993862988389d9b17bc32b3 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Wed, 20 May 2026 14:30:39 -0700 Subject: [PATCH 4/8] use env vars instead of local_settings.py --- README.md | 2 +- app/portal/portal/entrypoint.sh | 3 --- app/portal/portal/settings.py | 17 +++++++++++------ docker/.env.template | 3 ++- docker/docker-compose.yaml | 4 ++-- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index dda39eb..51c17e4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ cd OH4S_Proteins - In order to run the application with production data, locate a database dump of a production install. Add the dump to the `docker` directory and name it `db_dump.sql`. This is necessary for the `init-db.sh` script to run correctly. -- Create an copy the `.env.template` file to a `.env` file in the `docker` directory. Add your environment variables to that file. +- Create a copy of the `.env.template` file as `.env` in the `docker` directory. Add your environment variables to that file. - Move to the docker directory: `cd docker` diff --git a/app/portal/portal/entrypoint.sh b/app/portal/portal/entrypoint.sh index e5f0043..f64ba5a 100644 --- a/app/portal/portal/entrypoint.sh +++ b/app/portal/portal/entrypoint.sh @@ -24,8 +24,5 @@ python manage.py collectstatic --noinput echo "loading initial data fixtures..." python manage.py loaddata fixtures/providers_20210524.json -echo "creating superuser if not exists..." -python manage.py createsuperuser - echo "Starting python development server on :8000" python manage.py runserver 0.0.0.0:8000 diff --git a/app/portal/portal/settings.py b/app/portal/portal/settings.py index ea67ea0..2bf66ea 100644 --- a/app/portal/portal/settings.py +++ b/app/portal/portal/settings.py @@ -32,12 +32,15 @@ # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 't8(b^k=eh4pu6o6to8px!7pmcf)@x#p$&nyp&ksm!oc00s2s-(' +SECRET_KEY = os.environ.get('SECRET_KEY', default='set in .env file') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.environ.get('DEBUG', 'True') == 'True' ALLOWED_HOSTS = [] +ALLOWED_HOSTS_ENV = os.environ.get("ALLOWED_HOSTS") +if ALLOWED_HOSTS_ENV: + ALLOWED_HOSTS.extend(ALLOWED_HOSTS_ENV.split(",")) # Application definition @@ -113,8 +116,11 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'set_in_local_settings.py', - 'USER': 'set_in_local_settings.py' + 'NAME': os.environ.get('SQL_DATABASE', default='postgres'), + 'USER': os.environ.get('SQL_USER', default='postgres'), + 'PASSWORD': os.environ.get('SQL_PASSWORD', default=None), + 'HOST': os.environ.get('SQL_HOST', default='db'), + 'PORT': os.environ.get('SQL_PORT', default='5432'), } } @@ -175,5 +181,4 @@ MIN_SEARCH_RANK=0.1 MIN_SEARCH_SIMILARITY=0.25 - -from .local_settings import * +MAPBOX_TOKEN = os.environ.get('MAPBOX_TOKEN', default='') \ No newline at end of file diff --git a/docker/.env.template b/docker/.env.template index 9dab048..393b2bd 100644 --- a/docker/.env.template +++ b/docker/.env.template @@ -6,4 +6,5 @@ SQL_DATABASE= SQL_USER= SQL_PASSWORD= SQL_HOST= -SQL_PORT= \ No newline at end of file +SQL_PORT= +MAPBOX_TOKEN= \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index db49dac..c69adb8 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -14,7 +14,7 @@ services: ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${SQL_USER} -d ${SQL_DATABASE} -h localhost -p ${SQL_PORT}"] + test: ["CMD-SHELL", "pg_isready -U ${SQL_USER} -d ${SQL_DATABASE} -h localhost -p ${SQL_PORT:-5432}"] interval: 10s timeout: 5s retries: 5 @@ -49,7 +49,7 @@ services: db: condition: service_healthy volumes: - - ../app/portal:/usr/src/app + - ../app/portal:/app volumes: oh4s: \ No newline at end of file From 259a652bf5d512305fed5d51220818b5670fd0b8 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Wed, 20 May 2026 15:09:48 -0700 Subject: [PATCH 5/8] add checks for db_dump file --- docker/.env.template | 4 ++-- docker/init-db.sh | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docker/.env.template b/docker/.env.template index 393b2bd..9c1d39d 100644 --- a/docker/.env.template +++ b/docker/.env.template @@ -6,5 +6,5 @@ SQL_DATABASE= SQL_USER= SQL_PASSWORD= SQL_HOST= -SQL_PORT= -MAPBOX_TOKEN= \ No newline at end of file +SQL_PORT=db +MAPBOX_TOKEN=5432 \ No newline at end of file diff --git a/docker/init-db.sh b/docker/init-db.sh index 5ee9b5d..fd204c4 100755 --- a/docker/init-db.sh +++ b/docker/init-db.sh @@ -4,9 +4,19 @@ set -e # This script runs automatically when the PostgreSQL container is first initialized # It will only run if the database is empty (first time setup) + DB_DUMP_FILE="/tmp/db_dump.sql" + if [ ! -f "$DB_DUMP_FILE" ]; then + echo "Error: database dump file not found at $DB_DUMP_FILE. Mount or copy the SQL dump before initializing the database." >&2 + exit 1 + fi + if [ ! -r "$DB_DUMP_FILE" ]; then + echo "Error: database dump file is not readable at $DB_DUMP_FILE. Check file permissions before initializing the database." >&2 + exit 1 + fi + echo "Importing database dump..." # Import the SQL dump, ignoring meta-command errors -PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=0 < /tmp/db_dump.sql +PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=0 < "$DB_DUMP_FILE" echo "Database import completed!" \ No newline at end of file From 2be4fe2c1cd7ad3c4c866e5228f4c437c518db20 Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Wed, 20 May 2026 15:24:33 -0700 Subject: [PATCH 6/8] add condition for local_settings import to support vagrant setup --- app/portal/portal/settings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/portal/portal/settings.py b/app/portal/portal/settings.py index 2bf66ea..b2c3037 100644 --- a/app/portal/portal/settings.py +++ b/app/portal/portal/settings.py @@ -181,4 +181,9 @@ MIN_SEARCH_RANK=0.1 MIN_SEARCH_SIMILARITY=0.25 -MAPBOX_TOKEN = os.environ.get('MAPBOX_TOKEN', default='') \ No newline at end of file +MAPBOX_TOKEN = os.environ.get('MAPBOX_TOKEN', default='') + +try: + from .local_settings import * +except ImportError: + pass \ No newline at end of file From 0b66e964edc0e6891491219449e6858be1b76efc Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Thu, 28 May 2026 14:53:00 -0700 Subject: [PATCH 7/8] fix env vars --- app/portal/portal/settings.py | 2 +- docker/.env.template | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/portal/portal/settings.py b/app/portal/portal/settings.py index b2c3037..5b21844 100644 --- a/app/portal/portal/settings.py +++ b/app/portal/portal/settings.py @@ -115,7 +115,7 @@ DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', + 'ENGINE': os.environ.get('SQL_ENGINE', default='django.db.backends.postgresql'), 'NAME': os.environ.get('SQL_DATABASE', default='postgres'), 'USER': os.environ.get('SQL_USER', default='postgres'), 'PASSWORD': os.environ.get('SQL_PASSWORD', default=None), diff --git a/docker/.env.template b/docker/.env.template index 9c1d39d..3b4f7fe 100644 --- a/docker/.env.template +++ b/docker/.env.template @@ -1,10 +1,10 @@ DEBUG= SECRET_KEY= ALLOWED_HOSTS= -SQL_ENGINE=django.contrib.gis.db.backends.postgis +SQL_ENGINE=django.db.backends.postgresql SQL_DATABASE= SQL_USER= SQL_PASSWORD= -SQL_HOST= -SQL_PORT=db -MAPBOX_TOKEN=5432 \ No newline at end of file +SQL_HOST=db +SQL_PORT=5432 +MAPBOX_TOKEN= \ No newline at end of file From 9f6bca15e5feb76faa1e4a29eec5a517fe7162ce Mon Sep 17 00:00:00 2001 From: Paige Williams Date: Thu, 28 May 2026 14:55:03 -0700 Subject: [PATCH 8/8] import fixtures if they do not exist yet --- app/portal/portal/entrypoint.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/portal/portal/entrypoint.sh b/app/portal/portal/entrypoint.sh index f64ba5a..46ee211 100644 --- a/app/portal/portal/entrypoint.sh +++ b/app/portal/portal/entrypoint.sh @@ -21,8 +21,14 @@ python manage.py migrate --noinput echo "Collecting static files..." python manage.py collectstatic --noinput -echo "loading initial data fixtures..." -python manage.py loaddata fixtures/providers_20210524.json +echo "Checking for existing providers..." +if [ "$(python manage.py shell -c 'from providers.models import PoliticalRegion; print(PoliticalRegion.objects.count())' 2>/dev/null | tail -1)" = "0" ]; then + echo "No providers found, loading default providers fixture..." + python manage.py loaddata fixtures/providers_20210524.json +else + echo "number of political regions: $(python manage.py shell -c 'from providers.models import PoliticalRegion; print(PoliticalRegion.objects.count())' 2>/dev/null | tail -1)" + echo "Providers content already exist, skipping fixture load." +fi echo "Starting python development server on :8000" python manage.py runserver 0.0.0.0:8000