Dockerize Django app with dev/beta/prod env config and uv (#6)
Unit Tests / test (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
## Summary - Containerize the Django app with Docker and docker-compose (dev + production) - Refactor settings into `dev` / `beta` / `prod` environments driven by environment variables - Connect to PostgreSQL via `DATABASE_URL` or `DB_*` vars - Migrate package management from pip to uv (`pyproject.toml`, `uv.lock`) - Split Gitea workflows: PRs run unit tests only; pushes to `master` run tests, Docker validation, and deploy - Update deploy script to rsync code, preserve server `.env`, validate config, and run Docker compose Closes #4 ## Test plan - [x] `uv run python manage.py test` passes locally (10/10) - [x] `DJANGO_ENV=beta` and `DJANGO_ENV=prod` load with correct logging levels - [x] `scripts/validate-env.sh` rejects missing production variables - [ ] `docker compose up --build` starts app + Postgres locally - [ ] Containerized unit tests pass in CI Docker job - [ ] Server `.env` created from `.env.prod.example` before first production deploy - [ ] CI workflow runs on this PR (tests only, no deploy) Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
+54
-12
@@ -2,10 +2,9 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="/home/westfarn/Documents/django_live_sites/Company_Site"
|
||||
APP_DIR="$REPO_ROOT/company_site"
|
||||
VENV="$REPO_ROOT/venv"
|
||||
SETTINGS="$APP_DIR/company_site/settings.py"
|
||||
LOCAL_SETTINGS="$APP_DIR/company_site/local_settings.py"
|
||||
ENV_FILE="$REPO_ROOT/.env"
|
||||
COMPOSE_FILE="$REPO_ROOT/docker-compose.prod.yml"
|
||||
VALIDATE_SCRIPT="$REPO_ROOT/scripts/validate-env.sh"
|
||||
|
||||
if [[ $# -lt 1 || -z "${1:-}" ]]; then
|
||||
echo "Usage: $0 <checkout-directory>" >&2
|
||||
@@ -13,24 +12,67 @@ if [[ $# -lt 1 || -z "${1:-}" ]]; then
|
||||
fi
|
||||
|
||||
CHECKOUT="$1"
|
||||
DEPLOY_MODE="${DEPLOY_MODE:-docker}"
|
||||
|
||||
cp "$SETTINGS" /tmp/company_site_settings.py.bak
|
||||
if [[ "$DEPLOY_MODE" == "docker" ]]; then
|
||||
echo "Syncing application files to $REPO_ROOT (preserving server .env)..."
|
||||
rsync -a --delete \
|
||||
--exclude .git/ \
|
||||
--exclude .venv/ \
|
||||
--exclude .env \
|
||||
--exclude db.sqlite3 \
|
||||
--exclude staticfiles/ \
|
||||
"$CHECKOUT/" "$REPO_ROOT/"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Missing $ENV_FILE on the server." >&2
|
||||
echo "Create it from .env.prod.example and configure production values." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$VALIDATE_SCRIPT" "$ENV_FILE"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "Building Docker images..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build
|
||||
|
||||
echo "Starting containers..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --remove-orphans
|
||||
|
||||
echo "Running database migrations..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T web \
|
||||
uv run python manage.py migrate --noinput
|
||||
|
||||
echo "Deploy complete."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" ps
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Legacy venv/systemd deploy (DEPLOY_MODE=legacy)
|
||||
APP_DIR="$REPO_ROOT/company_site"
|
||||
VENV="$REPO_ROOT/.venv"
|
||||
SETTINGS_DIR="$APP_DIR/company_site/settings"
|
||||
LOCAL_SETTINGS="$APP_DIR/company_site/local_settings.py"
|
||||
|
||||
cp -r "$SETTINGS_DIR" /tmp/company_site_settings.bak
|
||||
cp "$LOCAL_SETTINGS" /tmp/company_site_local_settings.py.bak 2>/dev/null || true
|
||||
|
||||
rsync -a --delete \
|
||||
--exclude venv/ \
|
||||
--exclude .venv/ \
|
||||
--exclude .git/ \
|
||||
--exclude 'company_site/company_site/settings.py' \
|
||||
--exclude .env \
|
||||
--exclude 'company_site/company_site/settings/' \
|
||||
--exclude 'company_site/company_site/local_settings.py' \
|
||||
"$CHECKOUT/" "$REPO_ROOT/"
|
||||
|
||||
cp /tmp/company_site_settings.py.bak "$SETTINGS"
|
||||
rm -rf "$SETTINGS_DIR"
|
||||
cp -r /tmp/company_site_settings.bak "$SETTINGS_DIR"
|
||||
cp /tmp/company_site_local_settings.py.bak "$LOCAL_SETTINGS" 2>/dev/null || true
|
||||
|
||||
source "$VENV/bin/activate"
|
||||
pip install -r "$REPO_ROOT/requirements.txt"
|
||||
uv sync --frozen --directory "$REPO_ROOT"
|
||||
cd "$APP_DIR"
|
||||
python manage.py migrate --noinput
|
||||
python manage.py collectstatic --noinput
|
||||
uv run python manage.py migrate --noinput
|
||||
uv run python manage.py collectstatic --noinput
|
||||
|
||||
sudo systemctl restart company
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd /app/company_site
|
||||
|
||||
wait_for_database() {
|
||||
if [[ -z "${DATABASE_URL:-}" && -z "${DB_HOST:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Waiting for database..."
|
||||
for _ in $(seq 1 30); do
|
||||
if uv run python - <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "company_site.settings")
|
||||
|
||||
import django
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
django.setup()
|
||||
|
||||
try:
|
||||
connections["default"].ensure_connection()
|
||||
except OperationalError:
|
||||
sys.exit(1)
|
||||
PY
|
||||
then
|
||||
echo "Database is ready."
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Database did not become ready in time." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_database
|
||||
|
||||
uv run python manage.py migrate --noinput
|
||||
uv run python manage.py collectstatic --noinput
|
||||
|
||||
exec uv run gunicorn company_site.wsgi:application \
|
||||
--bind "${GUNICORN_BIND:-0.0.0.0:8000}" \
|
||||
--workers "${GUNICORN_WORKERS:-2}"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${1:?Usage: $0 <env-file>}"
|
||||
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
echo "Environment file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
DJANGO_ENV="${DJANGO_ENV:-prod}"
|
||||
|
||||
required_vars=(
|
||||
DJANGO_SECRET_KEY
|
||||
DJANGO_ALLOWED_HOSTS
|
||||
DB_PASSWORD
|
||||
DB_NAME
|
||||
DB_USER
|
||||
DATABASE_URL
|
||||
)
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" || "$DJANGO_ENV" == "beta" ]]; then
|
||||
required_vars+=(
|
||||
RECAPTCHA_PUBLIC_KEY
|
||||
RECAPTCHA_PRIVATE_KEY
|
||||
EMAIL_HOST_USER
|
||||
EMAIL_HOST_PASSWORD
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" ]]; then
|
||||
required_vars+=(TIANJI_WEBSITE_ID)
|
||||
fi
|
||||
|
||||
missing=()
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var:-}" ]]; then
|
||||
missing+=("$var")
|
||||
fi
|
||||
done
|
||||
|
||||
if ((${#missing[@]} > 0)); then
|
||||
echo "Missing required environment variables in $ENV_FILE:" >&2
|
||||
printf ' - %s\n' "${missing[@]}" >&2
|
||||
echo "Copy .env.prod.example to .env on the server and set production values." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$DJANGO_ENV" == "prod" && "$DJANGO_SECRET_KEY" == change-me* ]]; then
|
||||
echo "DJANGO_SECRET_KEY must be changed from the example value for production." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Environment validation passed (DJANGO_ENV=$DJANGO_ENV)."
|
||||
Reference in New Issue
Block a user