Applications Azure

django-helpdesk on Ubuntu 24.04 on Azure User Guide

| Product: django-helpdesk on Ubuntu 24.04 LTS on Azure

Overview

django-helpdesk is a mature open source ticket tracker and helpdesk built on the Django web framework. Requests arrive into queues that carry their own priorities and escalation rules, and anyone can raise a ticket and follow its progress from a public web portal without being given an account. Support staff sign in to a dashboard, filter and search the ticket list, take ownership of work, add public or private follow ups, record time spent and resolve tickets, while a searchable knowledge base answers the questions that come up again and again.

django-helpdesk is distributed as a Django application rather than a finished server, so the cloudimg image supplies the production project around it: django-helpdesk 2.3.2 is installed at a pinned upstream release into a dedicated Python virtual environment, hosted by a small purpose built Django project, served by gunicorn and reverse proxied behind nginx, and backed by PostgreSQL 16 on the same instance. gunicorn is bound to loopback 127.0.0.1:8020 and is never exposed directly; nginx on port 80 is the only way in, plus an unauthenticated /healthz endpoint for load balancer probes.

The image is secure by default. No credential of any kind ships, and nothing is listening on the public port in the image at all, so there is no window in which a freshly launched VM is reachable with a known password. On the first boot of every VM a fresh Django secret key, a fresh PostgreSQL password and a unique randomly generated staff password are created, written to a root only file, and only then is the public site brought up. Backed by 24/7 cloudimg support.

What is included:

  • django-helpdesk 2.3.2 installed in a dedicated Python virtual environment and running as the django-helpdesk gunicorn systemd service
  • Django 5.2 LTS on Python 3.12, a combination covered by the django-helpdesk project's own test matrix
  • The public web portal and the staff interface on :80, fronted by nginx with gunicorn bound to loopback only
  • A PostgreSQL 16 database on loopback holding the tickets, with a per VM database password generated on first boot
  • Two ready made queues, IT Support and General Enquiries, plus a Getting Started knowledge base article, so the portal accepts tickets the moment it boots
  • No default account and no shipped secret: the staff user and the Django secret key are both created on first boot with unique random values
  • Email is entirely optional. No mail server is required, no mailbox is polled, and the helpdesk is fully usable without either
  • Host settings configured so sign in works on the public IP address or a custom domain, with no baked in hostname
  • An unauthenticated /healthz endpoint for Azure Load Balancer health probes
  • postgresql.service, django-helpdesk.service (gunicorn) and nginx.service as systemd units
  • 24/7 cloudimg support

Key facts

Item Value
Platform Ubuntu 24.04 LTS (Gen2)
Default SSH user azureuser
Application root /opt/django-helpdesk
Django project /opt/django-helpdesk/site
Python virtual environment /opt/django-helpdesk/venv
Service account helpdesk (non login system user)
Per VM credentials /root/django-helpdesk-credentials.txt
Recommended size Standard_B2s (2 vCPU / 4 GiB RAM)

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point. NSG inbound: allow 22/tcp from your management network, 80/tcp for the helpdesk, and 443/tcp if you add TLS. The image serves plain HTTP on port 80; for production use, terminate TLS with your own domain and restrict access to trusted IP ranges (see Security Recommendations).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for django-helpdesk by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create then Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name django-helpdesk \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open port 80 to your network so you can reach the helpdesk:

az vm open-port --resource-group <your-rg> --name django-helpdesk --port 80

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

First boot generates the per VM secrets, creates the staff account and then brings the public site up. It normally completes within a minute of the VM reaching Running. Check all three services:

systemctl is-active postgresql django-helpdesk nginx

All three report active. Confirm the health endpoint answers:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/healthz

It returns 200.

Check that first boot completed. systemctl show reports the state and the outcome separately, which is what you want here: a first boot that ran and succeeded shows ActiveState=active (it is a oneshot unit with RemainAfterExit=yes) together with Result=success.

systemctl show -p ActiveState -p Result -p ExecMainStatus django-helpdesk-firstboot.service

Terminal output showing postgresql, django-helpdesk and nginx all active and the django-helpdesk gunicorn unit running

The application server and the database are deliberately reachable only on the loopback interface. nginx on port 80 is the single public entry point:

ss -tlnp | grep -E ':(80|8020|5432)\b'

Terminal output showing gunicorn on 127.0.0.1:8020 and PostgreSQL on 127.0.0.1:5432, with only nginx bound to port 80

Step 5 - Retrieve your per VM staff password

Every VM generates its own staff password on first boot and writes it to a root only file. Nothing is shared between deployments and no password is baked into the image.

sudo cat /root/django-helpdesk-credentials.txt

The file lists HELPDESK_URL, HELPDESK_ADMIN_USER (which is helpdeskadmin) and HELPDESK_ADMIN_PASSWORD, along with the portal, staff and Django admin addresses. Store the password in your password manager and change it after your first sign in.

The credentials file and the environment file that holds the Django secret key are both restricted, and the appliance ships a checker that proves no secret has leaked into the system journal, the authentication log, /var/backups, shell history or any other log:

sudo ls -l /root/django-helpdesk-credentials.txt /opt/django-helpdesk/helpdesk.env
sudo bash /usr/local/sbin/django-helpdesk-secret-leak-check.sh

The checker prints LEAKCHECK_OK.

Terminal output showing the credentials file at 0600 root root, the environment file at 0640 root helpdesk, and the leak checker reporting LEAKCHECK_OK

Step 6 - Open the public web portal

Browse to http://<vm-public-ip>/. The public portal is the front door: anyone can raise a ticket, track one they have already raised, or search the knowledge base, all without an account.

The django-helpdesk public web portal with options to submit a ticket, view an existing ticket and browse the knowledge base

Step 7 - Raise a ticket from the public portal

Choose Submit a Ticket. Pick the queue that matches the request, give a short summary and a description, and supply an email address so the person raising it can be kept informed. The image ships with IT Support and General Enquiries already configured, so this works immediately.

The public submit a ticket form filled in, with a queue selected, a summary, a description and a contact email address

On submit, django-helpdesk creates the ticket and shows the tracking page for it straight away, with a reference of the form <queue-slug>-<number>. Anyone holding that reference and the email address used can return to View a Ticket later and follow progress without signing in.

The public ticket tracking page showing the newly created ticket, its reference, status and description

Step 8 - Sign in as support staff

Browse to http://<vm-public-ip>/login/ and sign in as helpdeskadmin with the password from Step 5. All Tickets shows every request across every queue, with its status, age, owner and submitter, and the table can be searched, sorted, filtered and saved as a reusable query.

The staff ticket list showing every ticket with status, creation time, owner and submitter, including the ticket raised from the public portal

Step 9 - Work a ticket through to resolution

Open a ticket from the list. The Respond tab is where the work happens: write a follow up, set the new status, decide whether the update is public (public updates are what the person who raised the ticket sees on the tracking page), and optionally record time spent.

The Respond tab of a ticket with a follow up written and the new status set to Resolved

Choose Update This Ticket. The follow up and the status change are both written into the ticket's history, so the full audit trail of who did what and when stays with the ticket.

The ticket after the update, showing the resolution, the status of Resolved and the follow up history including the status change

Step 10 - Dashboard and reports

Dashboard summarises the tickets you own, the unassigned tickets waiting to be picked up, and the age profile of everything open, so a support team can see at a glance what needs attention. Reports breaks tickets down by queue, status, priority and month.

The staff dashboard showing ticket age bands, tickets assigned to the signed in user and unassigned tickets waiting to be picked up

Step 11 - Knowledge base

Knowledgebase holds articles grouped into categories. The image ships one Getting Started article explaining how to raise and track a ticket; add your own so recurring questions are answered before they become tickets. Articles are managed from the Django admin at http://<vm-public-ip>/admin/ under Helpdesk.

The knowledge base showing the Getting Started category and its article

Step 12 - Create your own queues

Queues are how work is separated between teams. Manage them in the Django admin at http://<vm-public-ip>/admin/helpdesk/queue/. For each queue set a title and a slug (the slug becomes the ticket reference prefix), and tick Allow public submission if the queue should appear on the public portal form. You can also list them from the command line:

sudo /usr/local/sbin/helpdesk-manage shell -c "from helpdesk.models import Queue; print([(q.slug, q.title, q.allow_public_submission) for q in Queue.objects.all()])"

Step 13 - Optional: outbound email and email to ticket

Email is optional and is switched off by default. The helpdesk is fully functional without a mail server: tickets are raised and worked through the web portal and the REST API. Out of the box Django's console email backend is used, so notifications are written to the service log rather than sent.

To send notifications through your own SMTP server, add the settings to the environment file and restart the service:

sudo nano /opt/django-helpdesk/helpdesk.env

Add the lines you need, then restart:

EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_HOST_USER=helpdesk@example.com
EMAIL_HOST_PASSWORD=your-smtp-password
EMAIL_USE_TLS=true
DEFAULT_FROM_EMAIL=helpdesk@example.com
sudo systemctl restart django-helpdesk

To collect tickets from a mailbox as well, open the queue in the Django admin, tick Allow email submission, fill in the mailbox type, host, port, user and password, then run the collector on a schedule with a systemd timer or cron:

sudo /usr/local/sbin/helpdesk-manage get_email

Leave both switched off if you do not need them. Nothing else in the appliance depends on either.

Step 14 - Add a domain and TLS

For production, put your own certificate in front of the helpdesk. Point a DNS record at the VM's public IP, then install a certificate with your preferred tool and configure nginx to serve it. The vhost lives at /etc/nginx/sites-available/cloudimg-django-helpdesk.

Once you are serving HTTPS, restrict the hostnames the application answers on and switch the session and CSRF cookies to secure only, by editing the environment file:

DJANGO_ALLOWED_HOSTS=helpdesk.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://helpdesk.example.com
DJANGO_SECURE_COOKIES=true

Then restart the service:

sudo systemctl restart django-helpdesk

Leave DJANGO_SECURE_COOKIES at false while you are still on plain HTTP. A secure only cookie is never returned by the browser over HTTP, so turning it on too early stops sign in from working.

Step 15 - Verify the whole workflow yourself

The appliance ships the same end to end check that the image is built and tested with. It renders the public submit form, submits a ticket through it, confirms the ticket reached the database, confirms the public tracking page shows it, proves a guessed staff password is rejected, signs in with the real one, opens the ticket in the staff interface, adds a follow up and resolves it, re-reads the database to prove the change persisted, and then removes the ticket it created so nothing is left behind:

sudo bash /usr/local/sbin/django-helpdesk-verify-workflow.sh

It prints a single line, WORKFLOW_OK ticket=<id> status=Resolved followups=2. Any failure prints BROKEN-<reason> instead and exits non zero.

Terminal output showing WORKFLOW_OK, the queue and ticket counts, and the installed Django, django-helpdesk, PostgreSQL, nginx and Python versions

You can check the installed versions at any time:

/opt/django-helpdesk/venv/bin/python -c "import django, importlib.metadata as m; print('Django', django.get_version(), '| django-helpdesk', m.version('django-helpdesk'))"

Server Components

Component Version Purpose
django-helpdesk 2.3.2 The ticket tracker: queues, tickets, follow ups, knowledge base, REST API
Django 5.2 LTS The web framework hosting django-helpdesk
Python 3.12 Runtime, in a dedicated virtual environment
gunicorn 26.x WSGI application server, bound to 127.0.0.1:8020
nginx 1.24 Reverse proxy on port 80, serves static files and /healthz
PostgreSQL 16 Ticket database, bound to 127.0.0.1:5432
whitenoise 6.x Static file compression and caching

Filesystem Layout

Path Size Purpose
/ 29 GB Root filesystem
/boot 913 MB Operating system kernel files
/boot/efi 106 MB UEFI boot partition (Gen2 Hyper V)
/mnt 8 GB Azure temporary resource disk (not persistent)

Key directories

Path Purpose
/opt/django-helpdesk Application root, owned by the helpdesk system user
/opt/django-helpdesk/venv Python virtual environment holding django-helpdesk and its dependencies
/opt/django-helpdesk/site The Django project (manage.py, helpdesk_site/)
/opt/django-helpdesk/site/static Collected static files, served by nginx
/opt/django-helpdesk/site/media Ticket attachments uploaded by users
/opt/django-helpdesk/helpdesk.env Per VM environment file: secret key, database password, host settings
/var/lib/postgresql/16/main PostgreSQL data directory
/etc/nginx/sites-available/cloudimg-django-helpdesk The nginx vhost
/root/django-helpdesk-credentials.txt Per VM staff credentials, 0600 root:root

Managing the services

systemctl status django-helpdesk --no-pager --lines=0
sudo systemctl restart django-helpdesk
sudo systemctl restart nginx
sudo systemctl restart postgresql

Follow the application log:

sudo journalctl -u django-helpdesk -f

Run any Django management command with the instance environment already loaded:

sudo /usr/local/sbin/helpdesk-manage showmigrations helpdesk | tail -5

Scripts and Log Files

Path Purpose
/usr/local/sbin/django-helpdesk-firstboot.sh First boot: generates the per VM secrets, creates the staff account, enables the public site
/usr/local/sbin/helpdesk-manage Runs a Django management command with the instance environment
/usr/local/sbin/django-helpdesk-verify-workflow.sh End to end workflow check (Step 15)
/usr/local/sbin/django-helpdesk-secret-leak-check.sh Proves no per VM secret reached the journal, auth log, backups or history
/var/lib/cloudimg/django-helpdesk-firstboot.done First boot sentinel; its presence stops first boot re-running
journalctl -u django-helpdesk Application and gunicorn access log
journalctl -u django-helpdesk-firstboot First boot log
/var/log/nginx/access.log, /var/log/nginx/error.log nginx logs

On Startup

django-helpdesk-firstboot.service runs once, on the first boot of each VM, gated on the sentinel above. It resolves the VM's public address, rotates the PostgreSQL password, generates a unique Django secret key, applies any pending database migrations, creates the helpdeskadmin staff account with a random password, starts gunicorn on loopback, and only then enables the public nginx vhost. It finishes by proving the whole ticket workflow works and that no secret leaked into any log, and writes the sentinel.

Until that unit has completed, nothing is listening on port 80. This is deliberate: it means a newly launched VM is never reachable with a credential that anybody else could know. On later boots the unit is skipped and the normal service units start the helpdesk.

Backups

Back up the ticket database. The dump goes to a root only directory; do not copy the environment file into a backup location, because it holds the Django secret key and the database password.

sudo install -d -o root -g root -m 0700 /var/backups/django-helpdesk
sudo -i bash -c 'set -a; . /opt/django-helpdesk/helpdesk.env; set +a; umask 077; PGPASSWORD="$DB_PASSWORD" pg_dump -h 127.0.0.1 -U "$DB_USER" -d "$DB_NAME" | gzip > /var/backups/django-helpdesk/helpdesk-$(date -u +%Y%m%d).sql.gz'
sudo ls -l /var/backups/django-helpdesk/

The dump is written 0600 root:root inside a 0700 directory. Copy it off the VM to your own backup storage. Ticket attachments live under /opt/django-helpdesk/site/media and should be backed up alongside the database.

Troubleshooting

The helpdesk does not answer on port 80. First boot may still be running. Check its state and its outcome:

systemctl show -p ActiveState -p Result -p ExecMainStatus django-helpdesk-firstboot.service

ActiveState=active with Result=success means first boot finished. Result=exit-code or a non zero ExecMainStatus means it failed; read the log below.

sudo journalctl -u django-helpdesk-firstboot --no-pager | tail -40

The public vhost is enabled by first boot, so if that unit failed, port 80 stays closed by design.

Sign in does not work in a browser but the site loads. Check that DJANGO_SECURE_COOKIES is not set to true while you are still serving plain HTTP. A secure only cookie is never sent back over HTTP, so the session is silently dropped.

A page reports a disallowed host. Add your hostname to DJANGO_ALLOWED_HOSTS and your scheme plus hostname to DJANGO_CSRF_TRUSTED_ORIGINS in /opt/django-helpdesk/helpdesk.env, then restart django-helpdesk.

The staff password is not accepted. Re-read it from /root/django-helpdesk-credentials.txt. If the account has been lost entirely, create a replacement:

sudo DJANGO_SUPERUSER_PASSWORD='<new-password>' /usr/local/sbin/helpdesk-manage createsuperuser --noinput --username newadmin --email you@example.com

The public form offers no queue. At least one queue must have Allow public submission ticked. Check with the command in Step 12.

Checking application errors.

sudo journalctl -u django-helpdesk --no-pager | tail -60

Security Recommendations

  • Change the helpdeskadmin password after your first sign in, and create a named account per support agent rather than sharing one.
  • Restrict 22/tcp to your management network in the NSG, and consider restricting 80/tcp and 443/tcp to the networks that need the helpdesk.
  • Terminate TLS with your own certificate and domain, then set DJANGO_ALLOWED_HOSTS, DJANGO_CSRF_TRUSTED_ORIGINS and DJANGO_SECURE_COOKIES=true as described in Step 14.
  • Keep /root/django-helpdesk-credentials.txt and /opt/django-helpdesk/helpdesk.env as they are shipped. The environment file holds the Django secret key; anyone who reads it can forge sessions.
  • Unattended security upgrades are enabled on the image. Leave them on, and reboot when a kernel update requires it.
  • Ticket attachments are user supplied files. They are served from a dedicated path with directory listing off; keep uploads restricted to trusted users if that matters in your environment.
  • Take regular database backups as described above, and store them off the VM.

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk with your Azure subscription ID and the VM name.

django-helpdesk is open source software distributed under the BSD 3-Clause licence. django-helpdesk is a trademark of its respective owners and Django is a registered trademark of the Django Software Foundation; cloudimg is not affiliated with, endorsed by or sponsored by either project. Screenshots in this guide show demonstration data created for illustration; the image itself ships with no tickets.