Deployment guide

django-helpdesk

Overview

django-helpdesk is a mature open-source ticket tracker and helpdesk built on the Django web framework. Requests arrive in queues that carry their own priorities and escalation rules. Anyone can raise a ticket and follow its progress from a public web portal without an account.

Support staff sign in to a dashboard, filter and search the ticket list, and take ownership of work. They add public or private follow-ups, record time spent and resolve tickets. 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 SecureAnyCloud 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. A small, purpose-built Django project hosts it, gunicorn serves it, and nginx reverse proxies it. PostgreSQL 16 on the same instance backs it.

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 listens on the public port in the image, so a freshly launched VM is never reachable with a known password. On the first boot of every VM, the image creates a fresh Django secret key, a fresh PostgreSQL password and a unique, randomly generated staff password. It writes them to a root-only file, and only then brings the public site up.

What you get:

  • 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 that holds 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. First boot creates the staff user and the Django secret key with unique random values.
  • Optional email. No mail server is required and no mailbox is polled, so 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 hostname baked in.
  • An unauthenticated /healthz endpoint for Azure Load Balancer health probes.
  • postgresql.service, django-helpdesk.service (gunicorn) and nginx.service as systemd units.
  • 24/7 SecureAnyCloud support.

Key facts

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 (a non-login system user)

Per-VM credentials: /root/django-helpdesk-credentials.txt

Recommended size: Standard_B2s (2 vCPU / 4 GiB RAM)

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point.

In the network security group, allow inbound 22/tcp from your management network and 80/tcp for the helpdesk. Also allow 443/tcp if you add TLS.

The image serves plain HTTP on port 80. For production, 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 and choose Create a resource. Search the Marketplace for django-helpdesk by SecureAnyCloud, and select Create.

On the Basics tab, 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).

Choose Review + create, and then choose Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:django-helpdesk-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name django-helpdesk \
  --image sac1647283583153:django-helpdesk-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open port 80 to your network so that 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 that 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 together with Result=success. The unit stays active because it is a oneshot unit with RemainAfterExit=yes.

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. Change it after your first sign-in.

The credentials file and the environment file that holds the Django secret key are both restricted. The appliance also 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 a ticket 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, and give a short summary and a description. Supply an email address so the person raising the ticket 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 its tracking page straight away. The ticket has a reference of the form <queue-slug>-<number>. Anyone who holds 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. You can search, sort and filter the table, and save a filter 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, and optionally record time spent. Decide whether the update is public. Public updates are what the person who raised the ticket sees on the tracking page.

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 summarizes the tickets you own, the unassigned tickets waiting to be picked up, and the age profile of everything open. 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 that explains how to raise and track a ticket. Add your own articles so that recurring questions are answered before they become tickets.

Manage articles from the Django admin at http://<vm-public-ip>/admin/ under Helpdesk.

Step 12 - Create your own queues

Queues separate work 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. Tick Allow public submission if the queue should appear on the public portal form. You can also list the queues 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. You raise and work tickets 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=<set-at-first-boot> 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 and 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. Install a certificate with your preferred tool, and configure nginx to serve it. The vhost lives at /etc/nginx/sites-available/sac-django-helpdesk.

Once you are serving HTTPS, edit the environment file. Restrict the hostnames that the application answers on, and switch the session and CSRF cookies to secure-only:

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. The browser never returns a secure-only cookie 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. The check works through these actions:

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

Run the check:

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

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

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

Each line gives the component, its version and its purpose.

django-helpdesk 2.3.2: the ticket tracker, with queues, tickets, follow-ups, knowledge base and REST API

Django 5.2 LTS: the web framework that hosts django-helpdesk

Python 3.12: the runtime, in a dedicated virtual environment

gunicorn 26.x: the WSGI application server, bound to 127.0.0.1:8020

nginx 1.24: the reverse proxy on port 80, which serves static files and /healthz

PostgreSQL 16: the ticket database, bound to 127.0.0.1:5432

whitenoise 6.x: static file compression and caching

Filesystem layout

Each line gives a path, its size where relevant, and its purpose.

/ (29 GB): the root filesystem

/boot (913 MB): operating system kernel files

/boot/efi (106 MB): the UEFI boot partition (Gen2 Hyper-V)

/mnt (8 GB): the Azure temporary resource disk (not persistent)

Key directories

/opt/django-helpdesk: the application root, owned by the helpdesk system user

/opt/django-helpdesk/venv: the Python virtual environment that holds 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: the per-VM environment file, with the secret key, database password and host settings

/var/lib/postgresql/16/main: the PostgreSQL data directory

/etc/nginx/sites-available/sac-django-helpdesk: the nginx vhost

/root/django-helpdesk-credentials.txt: the per-VM staff credentials, mode 0600, owned by 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

Each line gives a script, sentinel or log, and its purpose.

/usr/local/sbin/django-helpdesk-firstboot.sh: the first-boot script. It generates the per-VM secrets, creates the staff account and 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: the end-to-end workflow check (Step 15)

/usr/local/sbin/django-helpdesk-secret-leak-check.sh: proves that no per-VM secret reached the journal, auth log, backups or history

/var/lib/sac/data/django-helpdesk-firstboot.done: the first-boot sentinel. Its presence stops first boot from running again.

journalctl -u django-helpdesk: the application and gunicorn access log

journalctl -u django-helpdesk-firstboot: the first-boot log

/var/log/nginx/access.log and /var/log/nginx/error.log: the 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 and generates a unique Django secret key. It applies any pending database migrations and creates the helpdeskadmin staff account with a random password. It starts gunicorn on loopback, and only then enables the public nginx vhost.

The unit finishes by proving that the whole ticket workflow works and that no secret leaked into any log. It then writes the sentinel.

Until that unit has completed, nothing listens on port 80. This is deliberate, because 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. Back them 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

First boot enables the public vhost. 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. The browser never sends a secure-only cookie back over HTTP, so the session is silently dropped.

A page reports a disallowed host. Add your hostname to DJANGO_ALLOWED_HOSTS. Add 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 is 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.

Check for application errors. Read the application log:

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

Security recommendations

  • Change the helpdeskadmin password after your first sign-in. Create a named account for each support agent rather than sharing one.
  • Restrict 22/tcp to your management network in the network security group. 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, and anyone who reads it can forge sessions.
  • Leave unattended security upgrades on. They are enabled on the image. Reboot when a kernel update requires it.
  • Treat ticket attachments as user-supplied files. They are served from a dedicated path with directory listing off. Restrict uploads to trusted users if that matters in your environment.
  • Take regular database backups as described above, and store them off the VM.

Support

SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com with your Azure subscription ID and the VM name.

django-helpdesk is open-source software distributed under the BSD 3-Clause license. django-helpdesk is a trademark of its respective owners, and Django is a registered trademark of the Django Software Foundation. SecureAnyCloud 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.

Deploy on Azure

Launch django-helpdesk on Ubuntu 24.04 LTS on Azure, with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure