Django Oscar 4.1 on AWS User Guide
Overview
This guide covers launching and using the Django Oscar 4.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg e-commerce platforms listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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. The image 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.
What the cloudimg image adds:
- 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.comandstaff@example.comwith the passwordtesting. 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_KEYand 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_HOSTSandCSRF_TRUSTED_ORIGINSlist 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 anyX-Forwarded-Hosta client sends. - Hardened defaults. gunicorn runs as an unprivileged user on
127.0.0.1only with a read only view of the code; PostgreSQL 16 listens on127.0.0.1only;DEBUGis 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.
Step 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 your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as 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.
Step 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>
Step 3: Check the services
First boot creates the database, sets the passwords, the key and the certificate, then gunicorn and nginx start:
systemctl is-active postgresql@16-main django-oscar-firstboot django-oscar nginx
sudo test -f /var/lib/cloudimg/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, nginx 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
Step 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://www.cloudimg.co.uk/guides/django-oscar-4-1-on-ubuntu-24-04-aws/
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 only need the PostgreSQL password for your own tools; Oscar already has it in /opt/django-oscar/oscar.env. Keep DJANGO_SECRET_KEY with your backups.
Step 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 (or install your own, 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; 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, 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/
Step 6: Set up your store
The sample catalogue is there to show how Oscar works. Work through these 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 adminon 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). The sample products can be deleted here or kept 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 can be switched 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: payment gateways, shipping methods and tax rules are added in Python code. The store project is Oscar's demo project in /opt/django-oscar/src/sandbox, with the production settings in settings_cloudimg.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.
Step 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.
Step 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
Step 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_cloudimg.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_cloudimg.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/cloudimg/django-oscar-release.txt
grep -E '^(Django|django-oscar|gunicorn|pillow|psycopg)==' /etc/cloudimg/django-oscar-release.txt
Django Oscar 4.1 on Django 5.2.18
cloudimg: 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 = cloudimg
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/cloudimg-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.
Step 10: Changing passwords
- Admin password:
sudo oscar-manage changepassword admin, or sign in and use Account. Then update or deleteOSCAR_ADMIN_PASSWORDin/root/django-oscar-credentials.txtso 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_PASSWORDin/opt/django-oscar/oscar.env, thensudo 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.envand restart the store.
Step 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_cloudimg.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/cloudimg-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) |
Step 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 cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA