My suggestion would be keeping one codebase and one server running(or multiple server of the same Django application without any customization based on clients) for this. Main reason is to maintenance easier. You do not want to make changes multiple times to provide a feature to multiple clients.
As you already have a Django application, I think it is best to utilize that code to accommodate the approach given above with minimal change of code. Meaning, you need some way to handle multiple clients connecting to multiple DB. I would suggesting is to use a middleware and database router. Like this:(codes based on this snippet).
import threading
request_cfg = threading.local()
class RouterMiddleware (object):
def process_view( self, request, view_func, args, kwargs ):
if 'client' in kwargs:
request_cfg.client = kwargs['client']
request.client = client
# Here, we are adding client info with request object.
# It will make the implementation bit easier because
# then you can access client information anywhere in the view/template.
def process_response( self, request, response ):
if hasattr( request_cfg, 'client' ):
del request_cfg.client
return response
class DatabaseRouter (object):
def _default_db( self ):
from django.conf import settings
if hasattr( request_cfg, 'client' ) and request_cfg.client in settings.DATABASES:
return request_cfg.client
else:
return None
def db_for_read( self, model, **hints ):
return self._default_db()
def db_for_write( self, model, **hints ):
return self._default_db()
Then add those to settings.py:
DATABASES = {
'default': {
'NAME': 'user',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'postgres_user',
'PASSWORD': 's3krit'
},
'client1': {
'NAME': 'client1',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'postgres_user',
'PASSWORD': 's3krit'
},
'client2': {
'NAME': 'client2',
'ENGINE': 'django.db.backends.postgresql',
'USER': '_user',
'PASSWORD': 'priv4te'
}
}
DATABASE_ROUTERS = [
'path.to.DatabaseRouter',
]
MIDDLEWARE = [
# middlewares
'path.to.RouterMiddleware'
]
Finally update the urls.py:
urlpatterns = [
path('<str:client>/admin/', admin.site.urls),
path('<str:client>/', include('client_app.urls')),
# and so on
]
Advantage of this approach is that you do not have to configure anything for new clients, all you need to do is add a new database in settings and run the migrations as per described in the documentation. No need to configure reverse proxy server or anything else.
Now, when it comes to handling tasks in celery, you can provide which database you will be using to run the the queries(reference to docs). Here is an example:
@app.task
def some_task():
logger.info("-"*25)
for db_name in settings.DATABASES.keys():
Model.objects.using(db_name).filter(some_condition=True)
logger.info("-"*25)