Deployment guide

Django Oscar 4.1

Overview

Django Oscar is an open-source e-commerce framework for Django. It gives you a storefront (catalogue, search, product reviews, basket, checkout, customer accounts and wish lists) and a staff dashboard for products, categories, stock and partners, orders and fulfilment, customers, offers, vouchers and ranges, content pages and reports.

This SecureAnyCloud AMI runs Django Oscar 4.1 on Ubuntu 24.04 on Amazon EC2. It runs Oscar's own demo store project with Oscar's sample catalogue loaded, so you see a working shop on the first visit and can build your own store from it. It is one version of the SecureAnyCloud e-commerce platforms listing on AWS Marketplace, which you pick as the software version when you subscribe and launch.

What you get:

  • A working store on first boot. Oscar's sample catalogue (209 products in books and clothing categories with their cover images, countries, content pages, ranges and offers) is prepared and indexed for search at build time. The PostgreSQL database itself is created on your instance's first boot and loaded from that catalogue, so no database that existed on a build machine ships in the image.
  • No default login. Oscar's demo project documents the accounts superuser@example.com and staff@example.com with the password testing. Those accounts are never created on this image (their fixture files are removed), and first boot creates one superuser with a password unique to your instance. The demo accounts, a blank password and common weak passwords are refused.
  • Secrets unique to your instance. The admin password, Django's SECRET_KEY and the PostgreSQL password Oscar uses are generated on first boot and written to /root/django-oscar-credentials.txt (root only). The image ships with no key, the database account without a password, and gunicorn and nginx refuse to start until first boot has finished. Django refuses to start without a key of at least 50 characters.
  • HTTPS only, from the first boot. nginx serves the store on port 443 with a certificate generated on your instance; there is no plain HTTP listener, requests for a host name the instance does not serve are dropped, and the session and CSRF cookies are Secure.
  • Host and client address from configuration. Django is handed the configured host name. ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS list exactly the instance's names, and the Django site domain used in password reset links and sitemaps follows the configured name. nginx passes the real client address from the connection and drops any X-Forwarded-Host a client sends.
  • Hardened defaults. gunicorn runs as an unprivileged user on 127.0.0.1 only, with a read-only view of the code. PostgreSQL 16 listens on 127.0.0.1 only, and DEBUG is always off. gunicorn, nginx, PostgreSQL and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
  • Checked dependencies. Oscar 4.1 runs on Django 5.2.18 (the 5.2 long-term support release). Every Python package is installed from a hash locked list and was checked against the OSV and PyPI advisory databases at build time with no known vulnerability. No compiler or build tools are on the image.
  • Separate data volumes. The PostgreSQL database (/var/lib/postgresql) and the application with its uploaded media (/opt/django-oscar) each live on their own 10 GiB EBS volume that you can grow independently of the root volume.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Django Oscar 4.1 with Oscar's demo store project, Django 5.2.18, gunicorn 26.2, nginx 1.24, PostgreSQL 16, Python 3.12, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu
Architecture: x86_64
Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH) from your admin network and TCP 443 (the store and dashboard) from your customers and staff. Never open 5432.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Django Oscar 4.1 on Ubuntu 24.04 as the software version and select your Region, then choose Launch through EC2.

Choose m5.large, your key pair and a security group as described above. The AMI brings three volumes: the 8 GiB root volume, a 10 GiB volume for the database and a 10 GiB volume for the application and its media. You can make any of them larger at launch.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=django-oscar}]'

First boot takes well under a minute after the instance is running.

2

Connect to your instance

Version SSH user

Django Oscar 4.1 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot creates the database and sets the passwords, the key and the certificate. Then gunicorn and nginx start. Check the services:

systemctl is-active postgresql@16-main django-oscar-firstboot django-oscar nginx
sudo test -f /var/lib/sac/data/django-oscar-firstboot.done && echo FIRSTBOOT_DONE

All four units are active and the first boot marker is present:

active active active active FIRSTBOOT_DONE

Only SSH and nginx listen on network ports, and nginx listens on 443 only. gunicorn and PostgreSQL listen on the loopback address. The two :53 entries are the local DNS resolver:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd")) 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) 127.0.0.1:5432 users:(("postgres")) 127.0.0.1:8002 users:(("gunicorn"),("gunicorn"),("gunicorn"),("gunicorn")) 127.0.0.53%lo:53 users:(("systemd-resolve")) 127.0.0.54:53 users:(("systemd-resolve")) [::]:22 users:(("sshd"),("systemd")) [::]:443 users:(("nginx"),("nginx"),("nginx"))

The database and the application each have their own volume:

findmnt -no TARGET,SOURCE,SIZE,FSTYPE /var/lib/postgresql
findmnt -no TARGET,SOURCE,SIZE,FSTYPE /opt/django-oscar
/var/lib/postgresql /dev/nvme1n1 9.7G ext4 /opt/django-oscar /dev/nvme2n1 9.7G ext4
4

Get your URL and passwords

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

The file is readable by root only:

# Django Oscar 4.1 - generated on first boot by django-oscar-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Keep this file secret. OSCAR_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/ OSCAR_DASHBOARD_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/en-gb/dashboard/ OSCAR_DJANGO_ADMIN_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/admin/ OSCAR_ADMIN_EMAIL=admin@ec2-<public-ip-dashed>.compute-1.amazonaws.com OSCAR_ADMIN_USERNAME=admin OSCAR_ADMIN_PASSWORD=<OSCAR_ADMIN_PASSWORD> POSTGRES_DATABASE=oscar POSTGRES_USER=oscar POSTGRES_PASSWORD=<POSTGRES_PASSWORD>
# Django's SECRET_KEY (in /opt/django-oscar/oscar.env); it signs sessions, CSRF tokens and password-reset links. DJANGO_SECRET_KEY=<DJANGO_SECRET_KEY> # Sign in to the dashboard with OSCAR_ADMIN_EMAIL and OSCAR_ADMIN_PASSWORD. The certificate is self-signed # (minted on this instance); the store is served on https (port 443) only. # Change the name the store is served as:
sudo django-oscar-set-hostname <your-dns-name>
# Change the admin password:
sudo oscar-manage changepassword admin
# PostgreSQL listens on 127.0.0.1 only; on-box maintenance:
sudo -u postgres psql oscar
# User guide: https://secureanycloud.com/stacks/django-oscar-4-1/

The URL uses the instance's public DNS name from the EC2 instance metadata. The certificate also names the public and private addresses, so https://<public-ip>/ works too. If you stop and start the instance without an Elastic IP, the boot service picks up the new address, issues a new certificate for it and updates the store URL. To serve the store under your own DNS name, see Step 8.

You need the PostgreSQL password only for your own tools, because Oscar already has it in /opt/django-oscar/oscar.env. Keep DJANGO_SECRET_KEY with your backups.

5

Sign in

Open the OSCAR_URL in your browser. The certificate is self-signed, so your browser asks you to confirm it the first time. You can install your own certificate instead (Step 8). The storefront opens on the catalogue of Oscar's sample products:

Each product has its own page with price, stock and the basket button:

Open the OSCAR_DASHBOARD_URL. Sign in with the OSCAR_ADMIN_EMAIL (or the user name admin) and the OSCAR_ADMIN_PASSWORD from Step 4:

The dashboard shows orders, customers and products at a glance:

The same account opens the Django admin at the OSCAR_DJANGO_ADMIN_URL (user name admin). Oscar's documentation treats the Django admin as a developer convenience, so use the dashboard for day-to-day work.

You can also check the sign-in from the instance. This script signs in through the real storefront and Django admin forms over HTTPS with the instance's password and reaches the dashboard. It then shows that the demo accounts, a blank password and common weak passwords are refused:

sudo bash /usr/local/sbin/django-oscar-cred-roundtrip.sh
OK per-instance admin signs in over HTTPS (dashboard + Django admin); superuser@/staff@example.com:testing, admin@example.com:admin, blank and weak passwords refused

The storefront and the catalogue answer over HTTPS, and the dashboard sends a signed out visitor to its sign in page:

H=$(sudo grep '^OSCAR_URL=' /root/django-oscar-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
curl -sk -o /dev/null -w 'storefront %{http_code} %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'catalogue %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/en-gb/catalogue/"
curl -sk -o /dev/null -w 'dashboard (signed out) %{http_code} %{redirect_url}\n' --resolve "$H:443:127.0.0.1" "https://$H/en-gb/dashboard/"
storefront 302 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/en-gb/ catalogue 200 dashboard (signed out) 302 https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/en-gb/dashboard/login/?next=/en-gb/dashboard/
6

Set up your store

The sample catalogue shows how Oscar works. Work through these areas of the dashboard to make the store your own:

  • Account (top right): open your profile and set your own name and e-mail address, then change the password (or use sudo oscar-manage changepassword admin on the instance).
  • Catalogue: Product types and Categories first, then Products. Create a product of a type, add its images, attributes and a stock record (the partner, SKU, price and quantity Oscar sells it with). You can delete the sample products here or keep them while you test.
  • Fulfilment: orders, partners (your warehouses or suppliers) and their users.
  • Offers: site offers, vouchers and ranges. The sample offers are listed here, and you can switch them off.
  • Content: pages (About, Contact and so on), e-mail templates and reviews. Customers: customer accounts, staff users and stock alerts. Reports: orders, products, users and offers.

Oscar is a framework, so you add payment gateways, shipping methods and tax rules in Python code. The store project is Oscar's demo project in /opt/django-oscar/src/sandbox, with the production settings in settings_sac.py. The Oscar documentation (https://django-oscar.readthedocs.io/) covers forking apps and adding payment integrations.

Until you add a payment method, checkout completes without taking payment, as in Oscar's demo.

7

E-mail (optional)

Oscar e-mails order confirmations, password resets and alerts. Until you configure a relay, messages are written to /opt/django-oscar/mail-outbox (readable by root only) rather than sent.

To send them, add your SMTP settings to /opt/django-oscar/oscar.env (for Amazon SES: EMAIL_HOST=email-smtp.<region>.amazonaws.com, EMAIL_PORT=587, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_USE_TLS=true and DEFAULT_FROM_EMAIL), then restart the store. Allow outbound 587 in your security group or network ACLs if you restrict egress.

sudo nano /opt/django-oscar/oscar.env
sudo systemctl restart django-oscar

Password reset links always use your store's configured host name.

8

Your own DNS name and certificate

Point a DNS record at the instance (an Elastic IP keeps the address stable), then set the name the store is served as. This mints a new self-signed certificate for the name, updates nginx, ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS, the Django site domain and the URLs in /root/django-oscar-credentials.txt, restarts the store, and keeps the name across reboots:

sudo django-oscar-set-hostname <your-domain>

To use a certificate from a public or internal certificate authority instead (for example one issued by Let's Encrypt for <your-domain>), copy it over the generated pair and reload nginx (running django-oscar-set-hostname again replaces it with a self-signed one):

sudo install -m 0644 <your-domain>.crt /etc/ssl/django-oscar/oscar.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/django-oscar/oscar.key
sudo nginx -t && sudo systemctl reload nginx
9

Security checks

nginx serves the application, its static files and uploaded media only. Configuration files are never served, there is no plain HTTP listener (curl reports 000), and a request for a host name the instance does not serve gets no answer:

H=$(sudo grep '^OSCAR_URL=' /root/django-oscar-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#') for p in /.env /oscar.env /settings_sac.py /admin/; do curl -sk -o /dev/null -w "%{http_code} $p\n" --resolve "$H:443:127.0.0.1" "https://$H$p" done curl -s -o /dev/null -w 'http://127.0.0.1/ -> %{http_code}\n' -m 5 http://127.0.0.1/ || true curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" https://evil.example/ || true
404 /.env 404 /oscar.env 404 /settings_sac.py 302 /admin/ http://127.0.0.1/ -> 000 unknown host -> 000

A forged X-Forwarded-Host header does not change the links Oscar builds; the sitemap still names your store:

H=$(sudo grep '^OSCAR_URL=' /root/django-oscar-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
curl -sk -H 'X-Forwarded-Host: evil.example' --resolve "$H:443:127.0.0.1" "https://$H/sitemap.xml" | grep -o '<loc>[^<]*</loc>' | sed -n '1,2p'
<loc>https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/sitemap-static-ar.xml</loc> <loc>https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/sitemap-products-ar.xml</loc>

Django only accepts the instance's own names, and only over HTTPS:

grep -E '^(ALLOWED_HOSTS|CSRF_TRUSTED_ORIGINS)=' /opt/django-oscar/hosts.env
ALLOWED_HOSTS=ec2-<public-ip-dashed>.compute-1.amazonaws.com,<public-ip>,ip-<private-ip-dashed>.ec2.internal,<private-ip>,127.0.0.1,localhost CSRF_TRUSTED_ORIGINS=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com,https://<public-ip>,https://ip-<private-ip-dashed>.ec2.internal,https://<private-ip>,https://127.0.0.1,https://localhost

The versions on the image, and the build's dependency audit:

/opt/django-oscar/venv/bin/python -c 'import oscar, django; print("Django Oscar", oscar.get_version(), "on Django", django.get_version())' head -2 /etc/sac/django-oscar-release.txt grep -E '^(Django|django-oscar|gunicorn|pillow|psycopg)==' /etc/sac/django-oscar-release.txt
Django Oscar 4.1 on Django 5.2.18 sac: Django Oscar 4.1 (BSD-3-Clause) on Django 5.2.18; sandbox project from the upstream 4.1 tag archive (sha256 9192d70a69898b4e49971c423cbad4d4509ae0c7e4fb6abadab5a9a6b0856c1d). Python packages (pip --require-hashes; pip-audit OSV + PyPI: no known vulnerabilities at build): django-oscar==4.1 Django==5.2.18 gunicorn==26.2.0 pillow==13.0.0 psycopg==3.3.6

The certificate was generated on this instance and names its own addresses:

H=$(sudo grep '^OSCAR_URL=' /root/django-oscar-credentials.txt | cut -d= -f2- | sed -E 's#https://([^/]+)/#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.compute-1.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<public-ip>, DNS:ip-<private-ip-dashed>.ec2.internal, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

None of the services can reach the instance metadata service:

systemctl show django-oscar nginx postgresql@16-main django-oscar-firstboot -p Id -p IPAddressDeny
IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=django-oscar.service IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=nginx.service IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=postgresql@16-main.service IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 Id=django-oscar-firstboot.service

nginx hands gunicorn the configured host name and the client address from the connection, never values a client sends, and gunicorn trusts forwarded headers from the local machine only:

grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP|X-Forwarded-Proto|X-Forwarded-Host) ' /etc/nginx/sites-available/sac-django-oscar ps -o args= -C gunicorn | head -1
proxy_set_header Host $oscar_host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host ""; /opt/django-oscar/venv/bin/python3 /opt/django-oscar/venv/bin/gunicorn wsgi:application --bind 127.0.0.1:8002 --workers 3 --timeout 120 --forwarded-allow-ips 127.0.0.1 --error-logfile -

PostgreSQL has exactly one account with a password, the store's own; the postgres superuser signs in over the local socket only:

sudo -u postgres psql -At -c "SELECT rolname, rolpassword IS NOT NULL AS has_password FROM pg_authid WHERE rolname IN ('postgres','oscar') ORDER BY 1"
oscar|t postgres|f

Because the services cannot reach the metadata service, Oscar cannot use the instance's IAM role. If you move media to Amazon S3, give the store an access key for a narrowly scoped IAM user rather than relying on the instance role.

10

Changing passwords

  • Admin password: sudo oscar-manage changepassword admin, or sign in and use Account. Then update or delete OSCAR_ADMIN_PASSWORD in /root/django-oscar-credentials.txt so it does not hold an old password.
  • Give each member of staff their own account: they register on the storefront, then you tick Staff status for them in the Django admin (/admin/, Users). Keep superuser rights to the few people who need them.
  • PostgreSQL password: only change it together with DATABASE_PASSWORD in /opt/django-oscar/oscar.env, then sudo systemctl restart django-oscar.
  • SECRET_KEY: changing it signs every customer and admin out and invalidates outstanding password reset links. To rotate it, put a new random value of at least 50 characters in oscar.env and restart the store.
11

Backup and maintenance

The store database lives in PostgreSQL on its own volume (/var/lib/postgresql). The application, its configuration (with the key) and uploaded media live under /opt/django-oscar on the second volume. The simplest backup is an EBS snapshot of both data volumes. For a file-level copy while the store is running:

sudo -u postgres pg_dump -Fc oscar > <backup-dir>/oscar-$(date +%F).dump
sudo tar -czf <backup-dir>/oscar-files-$(date +%F).tar.gz -C /opt/django-oscar oscar.env src/sandbox/public/media

To grow a data volume, modify the EBS volume in the EC2 console, then extend the file system online (for example sudo resize2fs /dev/nvme2n1 for /opt/django-oscar; check the device with findmnt).

Operating system security updates install automatically. Run sudo apt-get update && sudo apt-get upgrade to apply everything else. nginx, gunicorn and PostgreSQL restart cleanly, and the store keeps all of its data. Python packages are pinned in the store's virtual environment and are updated with a new image version.

Component Path

Store project /opt/django-oscar/src/sandbox (settings settings_sac.py)

Python environment /opt/django-oscar/venv

Configuration /opt/django-oscar/oscar.env (mode 0640) and /opt/django-oscar/hosts.env

Uploaded media and static files /opt/django-oscar/src/sandbox/public/ (served at /media/ and /static/)

Search index /opt/django-oscar/src/sandbox/whoosh_index

Database PostgreSQL 16 database oscar (/var/lib/postgresql)

nginx site /etc/nginx/sites-available/sac-django-oscar

Certificate and key /etc/ssl/django-oscar/

Store service django-oscar.service (gunicorn)

Boot service /usr/local/sbin/django-oscar-firstboot.sh

Host name helper /usr/local/sbin/django-oscar-set-hostname

Management commands sudo oscar-manage <command>

Credentials /root/django-oscar-credentials.txt (mode 0600)

12

Security recommendations

  • Allow port 22 from your admin network only, and port 443 from your customers and staff. Never open 5432.
  • Use your own DNS name and a certificate your browsers trust (Step 8).
  • Change the admin e-mail and password to your own (Step 6), and give each person their own account.
  • Add a real payment integration before taking orders, and remove or replace the sample catalogue and offers.
  • Keep the operating system patched, and snapshot the data volumes regularly.

Support and licensing

Django Oscar is open source under the BSD 3-Clause licence (Copyright Tangent Communications PLC and individual contributors); Django is under the BSD 3-Clause licence too. There is no per store or per order licence fee. The AMI price covers SecureAnyCloud's hardening, first boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on AWS

Launch Django Oscar 4.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS