Add docker compose stack for test deployments

Runs postgres, redis, the web app and a celery worker. Dev defaults live
in .env.docker with an optional .env override. The entrypoint generates
and persists the Django secret key and waits for postgres; the web
service migrates and creates a Site row for every configured domain.
SQL dumps placed in db-init/ are restored on first start and are
git-ignored.
This commit is contained in:
sajeyks 2026-09-30 14:54:38 +03:00
commit 619a20394e
7 changed files with 154 additions and 8 deletions

View file

@ -1,2 +1,5 @@
.git
.env
*.pyc
__pycache__
dynamicweb/settings/dynamic.py

41
.env.docker Normal file
View file

@ -0,0 +1,41 @@
# Dev defaults for docker compose. Override any value in a local ".env" file.
# DEBUG=True makes settings load local.py (ALLOWED_HOSTS=*, console email).
DEBUG=True
ENABLE_LOGGING=False
LOG_LEVEL=DEBUG
# Leave empty: the entrypoint generates and persists one in the "appdata" volume
DJANGO_SECRET_KEY=
POSTGRES_DB=app
POSTGRES_USER=app
POSTGRES_PASSWORD=app
POSTGRES_HOST=db
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# Host -> urlconf. Add the VPS IP / domain here if you access it that way.
UNGLEICH_SITE_CONFIGS={"ungleich.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"blog.ungleich.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"comic.ungleich.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"digitalglarus.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"datacenterlight.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"rails-hosting.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"django-hosting.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"node-hosting.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"devuanhosting.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"devuanhosting.com":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"ipv6onlyhosting.com":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"digitalezukunft.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"hack4glarus.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"xn--nglarus-n2a.ch":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"localhost:8000":{"MULTISITE_CMS_URL":"dynamicweb.urls"},"127.0.0.1:8000":{"MULTISITE_CMS_URL":"dynamicweb.urls"}}
UNGLEICH_BLOG_SITE_ID=1
MULTISITE_CMS_FALLBACK=
EMAIL_HOST=localhost
DCL_ERROR_EMAILS_TO=dev@example.com
ADMIN_EMAIL=dev@example.com
DCL_SUPPORT_FROM_ADDRESS=dev@example.com
WEBHOOK_SECRET=dev
RECAPTCHA_PUBLIC_KEY=dev
RECAPTCHA_PRIVATE_KEY=dev
STRIPE_API_PRIVATE_KEY=
STRIPE_API_PUBLIC_KEY=
STRIPE_API_PRIVATE_KEY_TEST=
# OpenNebula / LDAP are not available in the test env
OPENNEBULA_USERNAME=oneadmin
OPENNEBULA_PASSWORD=opennebula
OPENNEBULA_PROTOCOL=http
# "test_domain" skips the import-time VMTemplate query (tables do not exist before migrate)
OPENNEBULA_DOMAIN=test_domain
OPENNEBULA_PORT=2633
OPENNEBULA_ENDPOINT=/RPC2

7
.gitignore vendored
View file

@ -47,3 +47,10 @@ secret-key
2024-*
2025-*
2023-*
.env
dynamicweb/settings/dynamic.py
# database dumps are kept out of git (personal data)
db-init/*.sql
db-init/*.sql.gz

1
db-init/README.md Normal file
View file

@ -0,0 +1 @@
# Put a Postgres dump here (*.sql or *.sql.gz); it is restored on first start.

42
docker-compose.yml Normal file
View file

@ -0,0 +1,42 @@
x-app: &app
build: .
env_file:
- .env.docker
- path: .env # optional local overrides
required: false
volumes:
- appdata:/data # generated secret key
- media:/usr/src/app/media
- logs:/usr/src/app/logs
depends_on: [db, redis]
services:
db:
image: postgres:12-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- pgdata:/var/lib/postgresql/data
# any *.sql / *.sql.gz here is restored on the first start (empty pgdata)
- ./db-init:/docker-entrypoint-initdb.d:ro
redis:
image: redis:6-alpine
web:
<<: *app
command: sh start-web.sh
ports:
- "8000:8000"
celery:
<<: *app
command: celery -A dynamicweb worker -l info
volumes:
pgdata:
appdata:
media:
logs:

View file

@ -1,19 +1,50 @@
#!/bin/sh
set -uex
set -ue
cd /usr/src/app/
cat > dynamicweb/settings/dynamic.py <<EOF
# Persist a generated Django secret key in a shared volume so that every
# container (web, celery) uses the same one across restarts.
if [ -z "${DJANGO_SECRET_KEY:-}" ]; then
KEY_FILE="${SECRET_KEY_FILE:-/data/secret-key}"
mkdir -p "$(dirname "$KEY_FILE")"
if [ ! -s "$KEY_FILE" ]; then
TMP_KEY="$KEY_FILE.$$"
python -c 'import random,string;print("".join(random.SystemRandom().choice(string.ascii_letters+string.digits) for _ in range(64)))' > "$TMP_KEY"
# hard link is atomic: only one container wins the race
ln "$TMP_KEY" "$KEY_FILE" 2>/dev/null || true
rm -f "$TMP_KEY"
fi
DJANGO_SECRET_KEY="$(cat "$KEY_FILE")"
export DJANGO_SECRET_KEY
fi
cat > dynamicweb/settings/dynamic.py <<PYEOF
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': '${POSTGRES_DB}',
'USER': '${POSTGRES_USER}',
'PASSWORD': '${POSTGRES_PASSWORD}',
'HOST': '${POSTGRES_HOST}',
'PORT': '5432',
'NAME': '${POSTGRES_DB:-app}',
'USER': '${POSTGRES_USER:-app}',
'PASSWORD': '${POSTGRES_PASSWORD:-app}',
'HOST': '${POSTGRES_HOST:-db}',
'PORT': '${POSTGRES_PORT:-5432}',
}
}
EOF
PYEOF
# Wait for postgres to accept connections
python - <<'PYEOF'
import os, socket, sys, time
host = os.environ.get('POSTGRES_HOST', 'db')
port = int(os.environ.get('POSTGRES_PORT', 5432))
for _ in range(60):
try:
socket.create_connection((host, port), 2).close()
sys.exit(0)
except OSError:
time.sleep(1)
sys.exit("postgres at %s:%s not reachable" % (host, port))
PYEOF
exec "$@"

21
start-web.sh Normal file
View file

@ -0,0 +1,21 @@
#!/bin/sh
# Container start-up for the web service: migrate, make sure every configured
# site exists in the DB, then serve.
set -ue
cd /usr/src/app
export DJANGO_SETTINGS_MODULE=dynamicweb.settings
python - <<'PYEOF'
import json, os
import django
django.setup()
from django.core.management import call_command
# system checks import urls.py, which queries the DB, so skip them here
call_command('migrate', interactive=False, skip_checks=True)
from django.contrib.sites.models import Site
for domain in json.loads(os.environ.get('UNGLEICH_SITE_CONFIGS') or '{}'):
Site.objects.get_or_create(domain=domain, defaults={'name': domain})
PYEOF
exec python manage.py runserver 0.0.0.0:8000