Dockerize Django app with dev/beta/prod env config and uv (#6)
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:
2026-07-07 11:23:50 -07:00
parent 7dd5ec3be1
commit 426cc82f04
24 changed files with 1191 additions and 203 deletions
+54 -12
View File
@@ -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
+48
View File
@@ -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}"
+59
View File
@@ -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)."