A team whose product is written in Python often ends up running its marketing or publication site on a PHP CMS, because that is where the CMSs are, and then has one system in the company nobody can extend. django CMS exists for that team: a full content management system that is also a Django application, so the same developers, the same models and the same deployment pipeline serve the content site too. This post follows a Python developer building one on MassiveGRID PaaS, which is built on Virtuozzo Application Platform, formerly Jelastic.

The team runs a data-journalism publication we will call The Ledger Review, with a Django back end for its data tools and a need for an editorial site that can embed those tools. The django CMS package deploys a Python application server with Django and django CMS behind a production WSGI server, a relational database (PostgreSQL here) with credentials in the Django settings, and persistent storage for static and media files.

What django CMS is, for people who know Django

django CMS is a set of Django applications: a page tree with a placeholder system, front-end editing where editors double-click content on the live page, plugins that render content types, versioning and publishing workflow, multilingual pages, and permissions integrated with Django's auth. Because it is a Django project, the publication's own applications, the data-tool models, the chart renderers, are installed alongside it and exposed as CMS plugins that editors drop into pages. The developer's first afternoon on the package was replacing the sample project with the team's repository and registering two existing Django apps as plugins.

Deploying from Git, with migrations

The Python layer is connected to the team's repository through the Git-Push-Deploy add-on, so a push to main pulls the code and runs the configured build commands. The developer's build step is the standard Django release sequence:

pip install -r requirements.txt
python manage.py migrate --noinput
python manage.py collectstatic --noinput

Migrations run before the WSGI server restarts on the new code, so a schema change and the code that needs it arrive together. Settings come from environment variables on the layer (SECRET_KEY, database URL, allowed hosts), read by django-environ, and nothing secret is in the repository. When the layer later grows to two nodes, deployments run sequentially behind the platform's NGINX balancer, and the developer notes that migrations must then be idempotent and run once, which the deploy hook on the master node ensures.

Static and media files, done properly

Django's two file categories need different homes. Static files (CSS, JavaScript, the admin's assets) are collected at deploy into a directory the web server serves directly, with far-future cache headers; they are regenerable and per release. Media files, the editors' uploaded images, are runtime data and must survive deploys and be visible to every node. The package's persistent storage holds both, and when the layer scales, media moves to a shared storage container mounted at the media path on every node, or to S3-compatible storage through a Django storage backend, which the developer chose for the CDN in front of it. Static files stay local per node, since each node builds its own copy at deploy.

Celery and Redis for the slow parts

A data-journalism site renders charts from datasets, and some renders take seconds; they do not belong in a web request. The developer adds a Redis node from the topology wizard and a second Python layer that runs Celery workers from the same repository, with Redis as the broker and result backend. The CMS plugin that embeds a chart enqueues the render and shows a cached image when it is ready; the worker layer scales vertically when a big dataset is published. Redis also serves as Django's cache backend, which turns django CMS's page rendering into a memory hit for anonymous readers. This is the same web-plus-workers-plus-Redis shape this blog describes for Laravel, in Python.

LayerRunsDeployCloudlets (typical)
Python web (WSGI)Django and django CMSGit push; pip, migrate, collectstatic; graceful restart6 to 14
Python workersCelery from the same repositorySame push; workers restart after current task4 to 16
RedisBroker, results, Django cacheUnaffected2 to 4
PostgreSQLContent and application dataMigrations from the web deploy4 to 10

Editors, versions and the publishing workflow

The editorial team got what a CMS is for: front-end editing on the page they are looking at, a page tree with drafts and published versions, and permissions per section through Django's groups. The developers got what they were for: every content type is a Django model with a migration in Git, every plugin is Python they can test, and the publication site deploys from the same pipeline as the data tools. When an editor asked for a "live data" block that updates from a dataset hourly, it was a plugin, a Celery beat schedule and a pull request, not a search for a third-party module.

Operations and cost

The Database Backup/Restore add-on dumps PostgreSQL nightly; media is on object storage with its own versioning; the platform's snapshots cover the rest. Django upgrades are rehearsed on a clone, which copies the database and the Redis node, and django CMS major versions follow the same route. The whole environment averages about 30 cloudlets an hour (a cloudlet is 128 MiB of RAM plus 400 MHz of CPU), roughly $73 a month before discounts at MassiveGRID's published $0.003372 per cloudlet-hour. The team's summary: one language, one pipeline, one place where every part of the publication lives.

Frequently Asked Questions

django CMS or Wagtail?

Both are Django-based CMSs. django CMS emphasises front-end editing on the live page and a placeholder and plugin model; Wagtail emphasises a structured editing interface with StreamField and is strong for editorial workflows. Either runs on the platform's Python application server the same way; the package installs django CMS.

Which WSGI server does the package use, and can I switch?

The Python application server runs a production WSGI server configured by the platform; you can adjust its worker count through the layer's configuration and switch to an ASGI server such as Uvicorn behind it if your project uses async views, with the process manager settings updated accordingly.

How do I run Django's management commands?

From the platform's Web SSH console on the Python node, in the project directory with the virtual environment active, exactly as on a laptop: createsuperuser, shell, or a custom command. Scheduled commands go in the platform's cron editor or as Celery beat tasks.

A CMS your Python team can extend

The django CMS package deploys Django and django CMS on a Python application server with a database and persistent storage in one click on MassiveGRID PaaS. Add Redis and a worker layer from the topology wizard, deploy from Git, pay per cloudlet-hour. Free 14-day trial.

django CMS on MassiveGRID PaaS

Further Reading