From 0736bebd63205b7484fc543f2f1cdf92fe7a39c7 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Sun, 29 Mar 2026 00:15:19 +0100 Subject: [PATCH] feat: add deployment host and domain configuration guide --- .env.dev.example | 2 + .env.prod.example | 2 + .env.test.example | 2 + backend/config/settings.py | 23 +++++- backend/workflows/admin_config_views.py | 3 + .../templates/workflows/deployment_hosts.html | 78 +++++++++++++++++++ .../workflows/developer_handbook.html | 27 ++++++- .../templates/workflows/handbook.html | 15 ++++ backend/workflows/urls.py | 1 + backend/workflows/views.py | 4 + 10 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 backend/workflows/templates/workflows/deployment_hosts.html diff --git a/.env.dev.example b/.env.dev.example index ab3ea14..c2eab77 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -1,3 +1,5 @@ +APP_DOMAIN= +APP_BASE_URL= DJANGO_SECRET_KEY=change-me DJANGO_DEBUG=1 DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/.env.prod.example b/.env.prod.example index 284485c..5604b8a 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -1,3 +1,5 @@ +APP_DOMAIN=workdock.example.com +APP_BASE_URL=https://workdock.example.com DJANGO_SECRET_KEY=change-me-long-random-value DJANGO_DEBUG=0 DJANGO_ALLOWED_HOSTS=workdock.example.com diff --git a/.env.test.example b/.env.test.example index e1c216c..b021ffb 100644 --- a/.env.test.example +++ b/.env.test.example @@ -1,3 +1,5 @@ +APP_DOMAIN= +APP_BASE_URL= DJANGO_SECRET_KEY=change-me-long-random-value DJANGO_DEBUG=1 DJANGO_ALLOWED_HOSTS=192.168.2.55,localhost,127.0.0.1 diff --git a/backend/config/settings.py b/backend/config/settings.py index 045551c..a3c70d9 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -1,13 +1,32 @@ import os import sys from pathlib import Path +from urllib.parse import urlsplit BASE_DIR = Path(__file__).resolve().parent.parent +def _split_csv_env(name: str, default: str = ''): + return [item.strip() for item in os.getenv(name, default).split(',') if item.strip()] + +def _append_unique(items, value): + if value and value not in items: + items.append(value) + +def _hostname_from_url(url: str) -> str: + try: + return (urlsplit(url).hostname or '').strip() + except ValueError: + return '' + SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'unsafe-dev-key') DEBUG = os.getenv('DJANGO_DEBUG', '0') == '1' -ALLOWED_HOSTS = [h.strip() for h in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if h.strip()] -CSRF_TRUSTED_ORIGINS = [o.strip() for o in os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS', '').split(',') if o.strip()] +APP_DOMAIN = os.getenv('APP_DOMAIN', '').strip() +APP_BASE_URL = os.getenv('APP_BASE_URL', '').strip().rstrip('/') +ALLOWED_HOSTS = _split_csv_env('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1') +CSRF_TRUSTED_ORIGINS = _split_csv_env('DJANGO_CSRF_TRUSTED_ORIGINS', '') +_append_unique(ALLOWED_HOSTS, APP_DOMAIN) +_append_unique(ALLOWED_HOSTS, _hostname_from_url(APP_BASE_URL)) +_append_unique(CSRF_TRUSTED_ORIGINS, APP_BASE_URL) # Security hardening SESSION_COOKIE_HTTPONLY = True diff --git a/backend/workflows/admin_config_views.py b/backend/workflows/admin_config_views.py index 91b4ef6..71284b0 100644 --- a/backend/workflows/admin_config_views.py +++ b/backend/workflows/admin_config_views.py @@ -406,5 +406,8 @@ def project_wiki_page_impl(request): def developer_handbook_page_impl(request): return render(request, 'workflows/developer_handbook.html') +def deployment_hosts_page_impl(request): + return render(request, 'workflows/deployment_hosts.html') + def release_checklist_page_impl(request): return render(request, 'workflows/release_checklist.html') diff --git a/backend/workflows/templates/workflows/deployment_hosts.html b/backend/workflows/templates/workflows/deployment_hosts.html new file mode 100644 index 0000000..0ed0232 --- /dev/null +++ b/backend/workflows/templates/workflows/deployment_hosts.html @@ -0,0 +1,78 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Host & Domain Setup" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} + {% include 'workflows/includes/app_header.html' with header_show_home=1 header_inside_shell=1 %} +
+
+

{% trans "Host & Domain Setup" %}

+ +
+

{% trans "Reference for configuring hostnames and origins correctly in Django deployments, including how to fix Invalid HTTP_HOST errors." %}

+ +
+

{% trans "Why this error happens" %}

+

{% trans "Django rejects requests whose Host header is not present in ALLOWED_HOSTS. This validation runs before normal page routing, so a broken hostname cannot show a friendly in-app error page on that same host." %}

+

{% trans "Use this guide from a working hostname or IP address to correct the environment configuration." %}

+
+ +
+

{% trans "Recommended environment variables" %}

+
    +
  • APP_DOMAIN: {% trans "canonical hostname without scheme, for example" %} workdock.bostame.de
  • +
  • APP_BASE_URL: {% trans "canonical external URL with scheme, for example" %} https://workdock.bostame.de
  • +
  • DJANGO_ALLOWED_HOSTS: {% trans "comma-separated hostnames and IPs allowed to reach the app" %}
  • +
  • DJANGO_CSRF_TRUSTED_ORIGINS: {% trans "comma-separated origins with scheme for POST and CSRF-safe requests" %}
  • +
+

{% trans "The application automatically adds APP_DOMAIN and the hostname from APP_BASE_URL to the effective host allow-list. APP_BASE_URL is also added to trusted CSRF origins." %}

+
+ +
+

{% trans "Current test deployment example" %}

+
APP_DOMAIN=workdock.bostame.de
+APP_BASE_URL=https://workdock.bostame.de
+DJANGO_ALLOWED_HOSTS=192.168.2.55,localhost,127.0.0.1
+DJANGO_CSRF_TRUSTED_ORIGINS=http://192.168.2.55:8088,https://workdock.bostame.de
+
+ +
+

{% trans "Production example" %}

+
APP_DOMAIN=workdock.example.com
+APP_BASE_URL=https://workdock.example.com
+DJANGO_ALLOWED_HOSTS=workdock.example.com
+DJANGO_CSRF_TRUSTED_ORIGINS=https://workdock.example.com
+

{% trans "Production should run with HTTPS, DEBUG disabled, secure cookies enabled, and SSL redirect enabled." %}

+
+ +
+

{% trans "How to fix a live server" %}

+
    +
  1. {% trans "Log into the server and edit the active env file, for example" %} /opt/workdock/.env.test
  2. +
  3. {% trans "Set APP_DOMAIN and APP_BASE_URL to the real external hostname." %}
  4. +
  5. {% trans "Keep the local IP in DJANGO_ALLOWED_HOSTS if you still use direct IP access." %}
  6. +
  7. {% trans "Restart the stack or rerun the deployment script." %}
  8. +
+
cd /opt/workdock
+nano .env.test
+RUN_DJANGO_CHECK=0 DEPLOY_HEALTH_URL="http://127.0.0.1:8088/healthz/" ./scripts/deploy_stack.sh .env.test docker-compose.prod.yml
+
+ +
+

{% trans "Important rules" %}

+
    +
  • DJANGO_ALLOWED_HOSTS: {% trans "hostnames only, no scheme" %}
  • +
  • DJANGO_CSRF_TRUSTED_ORIGINS: {% trans "must include scheme, for example" %} https://workdock.bostame.de
  • +
  • {% trans "If you use both IP and domain access, keep both in the configuration." %}
  • +
  • {% trans "A broken hostname setup cannot self-heal via the same broken host. Use a working host or IP to access this guide." %}
  • +
+
+
+{% endblock %} diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index c0b0a39..b91aae4 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -33,6 +33,7 @@ Builders Testing Backup + Hosts & Domains CI/CD Deployment Troubleshooting @@ -267,7 +268,25 @@ make backup-verify BACKUP_DIR=backups/backup_YYYYmmdd_HHMMSS
  • The staff UI uses the shared action-progress overlay for backup creation and verification so long-running actions present one standard app behavior.
  • -

    13) CI/CD

    +

    13) Host and Domain Configuration

    + + +

    14) CI/CD