diff --git a/.env.dev b/.env.dev index edb695aa..20e927f3 100644 --- a/.env.dev +++ b/.env.dev @@ -4,11 +4,11 @@ SECRET_KEY=w(s0&(_eb058wvmg@44_repv8)r9@5p8fx*g_@c)1dm&d*ew^u DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] # External DB (you mentioned production uses an external DB) -DATABASE_HOST=db-postgresql-lon1-05515-jan-22-backup-do-user-8165014-0.c.db.ondigitalocean.com -DATABASE_PORT=25060 -DATABASE_NAME=testing -DATABASE_USER=testing -DATABASE_PASSWORD=AVNS_ykXO4RxAq0zeJTI-1Nl +DB_HOST=db-postgresql-lon1-05515-jan-22-backup-do-user-8165014-0.c.db.ondigitalocean.com +DB_PORT=25060 +DB_NAME=testing +DB_USER=django +DB_PASSWORD=AVNS_NG_s4i7SMMobWLO # Redis REDIS_URL=redis://redis:6379/0 @@ -16,3 +16,7 @@ REDIS_URL=redis://redis:6379/0 # Gunicorn tuning GUNICORN_WORKERS=3 GUNICORN_LOGLEVEL=info + +# Nginx host ports for local development (override prod defaults) +NGINX_HTTP_PORT=8000 +NGINX_HTTPS_PORT=8443 diff --git a/deploy/nginx/dev.conf b/deploy/nginx/dev.conf new file mode 100644 index 00000000..495c7e57 --- /dev/null +++ b/deploy/nginx/dev.conf @@ -0,0 +1,30 @@ +upstream app_server { + server web:8000; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + client_max_body_size 100M; + + location /static/ { + alias /usr/src/app/static/; + expires 30d; + add_header Cache-Control "public, max-age=2592000"; + } + + location /media { + alias /usr/src/app/media; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://app_server; + } +} diff --git a/deploy/nginx/prod.conf b/deploy/nginx/prod.conf index a4f0de41..d08ca5cc 100644 --- a/deploy/nginx/prod.conf +++ b/deploy/nginx/prod.conf @@ -97,7 +97,6 @@ server { } # Default dev / health server block; keep 443 managed outside of docker unless you mount certs - listen 80; } # A minimal HTTPS server block that expects certs to be mounted at /etc/letsencrypt diff --git a/deploy/settings_local.py b/deploy/settings_local.py new file mode 100644 index 00000000..e1dd6ec5 --- /dev/null +++ b/deploy/settings_local.py @@ -0,0 +1,78 @@ +""" +Example `settings_local.py` derived from the production server file. +This file is a template only — DO NOT commit real secrets. Copy it to +`rad/deploy/settings_local.py` on your server (or provide your own) and +ensure the real file is excluded from git. +""" +import os + +DEBUG = True + +INTERNAL_IPS = ["82.69.88.125", "217.155.198.96", "localhost"] + +# Paths inside the nginx container / host-mounted deploy directories. +# On the server, these paths are mounted into nginx as described in the +# deployment README. Adjust as needed. +STATIC_ROOT = '/usr/src/app/static/' + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + +if not DEBUG: + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt': "%d/%b/%Y %H:%M:%S", + }, + 'simple': { + 'format': '%(levelname)s %(message)s', + }, + }, + 'handlers': { + 'file': { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': 'log.txt', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'django': { + 'handlers': ['file'], + 'propagate': True, + 'level': 'DEBUG', + }, + 'atlas': { + 'handlers': ['file'], + 'level': 'DEBUG', + }, + } + } + +# External service credentials (placeholders) +CIMAR_USERNAME = "your.username@example.com" +CIMAR_PASSWORD = "" + +# Celery settings — when running in docker the redis service is +# reachable at the `redis` hostname provided by docker compose. +CELERY_BROKER_URL = "redis://redis:6379" +CELERY_RESULT_BACKEND = "redis://redis:6379" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ.get("DB_NAME", "django"), + "USER": os.environ.get("DB_USER", "django"), + "PASSWORD": os.environ.get("DB_PASSWORD", "AVNS_NG_s4i7SMMobWLO"), + #"HOST": os.environ.get("DB_HOST", "db-postgresql-lon1-05515-do-user-8165014-0.b.db.ondigitalocean.com"), + "HOST": os.environ.get("DB_HOST", "db-postgresql-lon1-05515-jan-22-backup-do-user-8165014-0.c.db.ondigitalocean.com"), + "PORT": os.environ.get("DB_PORT", "25060"), + #"NAME": os.environ.get("DB_NAME", "django"), + #"USER": os.environ.get("DB_USER", "django"), + #"PASSWORD": os.environ.get("DB_PASSWORD", "f7bf31dc9bda1256ea827953480d1917"), + #"HOST": os.environ.get("DB_HOST", "161.35.163.87"), + #"PORT": os.environ.get("DB_PORT", "5432"), + } +} \ No newline at end of file diff --git a/docker/.env.dev.local b/docker/.env.dev.local new file mode 100644 index 00000000..73f8fd7f --- /dev/null +++ b/docker/.env.dev.local @@ -0,0 +1,23 @@ +DEBUG=1 +SECRET_KEY=w(s0&(_eb058wvmg@44_repv8)r9@5p8fx*g_@c)1dm&d*ew^u +DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1] + +DB_NAME=rad +DB_HOST=db +DB_USER=django +DB_PASSWORD=postgres +DB_PORT=5432 + +MEMCACHE_HOST=cache + +# DEV-specific vars used by rad/settings_local.py +DEV_USE_POSTGRES=1 +DEV_DB_NAME=${DB_NAME} +DEV_DB_USER=${DB_USER} +DEV_DB_PASSWORD=${DB_PASSWORD} +DEV_DB_HOST=${DB_HOST} +DEV_DB_PORT=${DB_PORT} + +# pgAdmin creds +PGADMIN_DEFAULT_EMAIL=admin@example.com +PGADMIN_DEFAULT_PASSWORD=admin \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 90d8cf54..9ae4f4c8 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -5,8 +5,10 @@ services: web: env_file: - ../.env.dev - nginx: - ports: - - "8080:80" - - "8443:443" + # Development nginx override: mount a simplified config that does not + # reference LetsEncrypt certs so the container can start without real + # certificates present on the host. + volumes: + - ../deploy/nginx/dev.conf:/etc/nginx/conf.d/default.conf:ro + diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 1dadf42c..da5b859d 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -21,7 +21,7 @@ services: env_file: - ../.env.${COMPOSE_ENV:-prod} environment: - - DJANGO_SETTINGS_MODULE=rad.settings.production + - DJANGO_SETTINGS_MODULE=rad.settings depends_on: redis: condition: service_healthy @@ -39,8 +39,8 @@ services: image: nginx:stable-alpine restart: always ports: - - "80:80" - - "443:443" + - "${NGINX_HTTP_PORT:-80}:80" + - "${NGINX_HTTPS_PORT:-443}:443" depends_on: - web volumes: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ca9f6bb4..f73ef645 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -29,7 +29,7 @@ services: - 8000:8000 - 3459:3459 env_file: - - ./.env.dev + - ./.env.dev.local db: image: postgres:14.2-alpine volumes: diff --git a/entrypoint.sh b/entrypoint.sh old mode 100755 new mode 100644 diff --git a/rad/Dockerfile.prod b/rad/Dockerfile.prod index bd08e295..c25ef6b9 100644 --- a/rad/Dockerfile.prod +++ b/rad/Dockerfile.prod @@ -29,17 +29,45 @@ WORKDIR /usr/src/app # Install python deps into a uv-managed venv for reproducible builds COPY requirements.txt /usr/src/app/ +# Prebuild wheels into /wheels so compiled artifacts are cached in a layer. +# This speeds rebuilds when `requirements.txt` hasn't changed. The first build +# still needs to compile any packages without prebuilt wheels. +RUN mkdir -p /wheels \ + && python -m venv /tmp/venvbuild \ + && /tmp/venvbuild/bin/pip install --upgrade pip setuptools wheel \ + && /tmp/venvbuild/bin/pip wheel --wheel-dir=/wheels -r requirements.txt \ + && rm -rf /tmp/venvbuild + +# Create the final uv-managed venv and install from the cached wheels where +# possible. Using `--no-index --find-links=/wheels` makes pip prefer the +# prebuilt wheels, avoiding recompilation. RUN uv venv /opt/venv \ && export VIRTUAL_ENV=/opt/venv \ && export PATH="/opt/venv/bin:$PATH" \ && uv pip install --upgrade pip setuptools wheel \ - && uv pip install --no-cache-dir -r requirements.txt + && uv pip install --no-index --find-links=/wheels -r requirements.txt -# Copy project files into image -COPY . /usr/src/app +# Ensure the runtime environment uses the uv-managed venv by default. +# This persists the venv into image ENV so `python` and `gunicorn` point +# to the venv-installed binaries at container start. +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="/opt/venv/bin:$PATH" -# Create non-root user -RUN useradd -m appuser && chown -R appuser /usr/src/app /opt/venv +# Create non-root user before copying files so we can use Docker's +# `--chown` flag during COPY and avoid an expensive recursive `chown -R`. +RUN useradd -m appuser + +# Copy project files into image and set ownership during copy (faster than +# a separate `chown -R` step). This keeps layer reuse efficient. +COPY --chown=appuser:appuser . /usr/src/app + +# Ensure the venv is usable by the non-root user; chown only the venv path +# (much smaller than the whole project tree) to avoid a long recursive chown. +RUN chown -R appuser:appuser /opt/venv + +# Make the entrypoint executable (some files in the repo may not have the +# executable bit set). Do this as root before switching to the app user. +RUN [ -f /usr/src/app/entrypoint.sh ] && chmod +x /usr/src/app/entrypoint.sh || true USER appuser ENTRYPOINT ["/usr/src/app/entrypoint.sh"] diff --git a/scripts/local-up.sh b/scripts/local-up.sh index d471fde0..c8275443 100755 --- a/scripts/local-up.sh +++ b/scripts/local-up.sh @@ -19,4 +19,26 @@ fi COMPOSE_ENV=${COMPOSE_ENV:-dev} echo "Starting compose with COMPOSE_ENV=$COMPOSE_ENV (using .env.$COMPOSE_ENV)" -COMPOSE_ENV=$COMPOSE_ENV docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.dev.yml up -d --build +# Load variables from .env. into the environment so Compose variable +# substitution (e.g. ${NGINX_HTTP_PORT}) works. We export all variables from +# the file for the duration of the command. +ENV_FILE="$REPO_ROOT/.env.$COMPOSE_ENV" +if [ -f "$ENV_FILE" ]; then + # Export variables from the .env file safely without sourcing it. Some + # values include characters (parentheses, ampersands) that break POSIX + # shell parsing if the file is sourced directly. Read line-by-line, + # ignore comments/empty lines and export KEY=VALUE pairs. + while IFS= read -r _line || [ -n "$_line" ]; do + line="$_line" + case "$line" in + ''|\#*) continue ;; + esac + # Split on first '=' into key and value + key=${line%%=*} + val=${line#*=} + # Export directly (preserves special characters in the value) + export "$key=$val" + done < "$ENV_FILE" +fi + +COMPOSE_ENV=$COMPOSE_ENV docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.dev.yml up --build