django-helpdesk on Ubuntu 24.04 on Azure User Guide
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-helpdeskgunicorn 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
/healthzendpoint for Azure Load Balancer health probes postgresql.service,django-helpdesk.service(gunicorn) andnginx.serviceas 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

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'

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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
helpdeskadminpassword after your first sign in, and create a named account per support agent rather than sharing one. - Restrict
22/tcpto your management network in the NSG, and consider restricting80/tcpand443/tcpto the networks that need the helpdesk. - Terminate TLS with your own certificate and domain, then set
DJANGO_ALLOWED_HOSTS,DJANGO_CSRF_TRUSTED_ORIGINSandDJANGO_SECURE_COOKIES=trueas described in Step 14. - Keep
/root/django-helpdesk-credentials.txtand/opt/django-helpdesk/helpdesk.envas 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.