Deploy Django on Akamai Cloud (Linode)
Deploy Django on an Akamai Cloud (Linode) compute instance with a Cloud Firewall, Gunicorn, Nginx, managed PostgreSQL, Object Storage for media, and backups.
Problem statement
Akamai Cloud, still widely known as Linode, offers predictable-price Linux instances plus the pieces a Django deployment needs: Cloud Firewalls, managed PostgreSQL databases, S3-compatible Object Storage, NodeBalancers, and an instance backup service. It is a common choice for freelancers and small teams who want a VPS without a hyperscaler's complexity.
The instance is a blank Linux machine. You are responsible for locking it down, running Django reliably, and deciding where data lives. This guide covers the Linode-specific setup and a production-ready application layer.
- Create an Ubuntu LTS instance with an SSH key (no password logins) in a region near your users.
- Attach a Cloud Firewall that drops inbound traffic except SSH from your IP, HTTP, and HTTPS.
- Create a non-root deploy user, install Python, Nginx, and build dependencies.
- Use a managed PostgreSQL cluster with the instance's IP in its trusted sources, or run PostgreSQL locally.
- Run Gunicorn under systemd on a Unix socket and proxy to it from Nginx.
- Issue certificates with Certbot and set Django's HTTPS settings.
- Enable the Backup Service and store uploads in Object Storage.
Step-by-step solution
1. Create the instance
In Cloud Manager, create a Linode with the latest Ubuntu LTS image and add your SSH public key. With the Linode CLI, look up current identifiers first:
linode-cli regions list
linode-cli linodes types --format id,label,memory,vcpus,price.monthly
linode-cli images list --format id,label | grep -i ubuntu
linode-cli linodes create \
--label django-prod --region eu-central --type g6-standard-1 \
--image linode/ubuntu24.04 \
--root_pass "$(openssl rand -base64 32)" \
--authorized_keys "$(cat ~/.ssh/id_ed25519.pub)"
A shared-CPU plan with 2 GB of memory handles a typical small Django site with PostgreSQL elsewhere. Dedicated-CPU plans suit CPU-heavy apps where noisy neighbours would hurt latency.
2. Attach a Cloud Firewall
Cloud Firewalls filter traffic before it reaches the instance. Create one with a default-drop inbound policy:
linode-cli firewalls create --label django-prod-fw \
--rules.inbound_policy DROP --rules.outbound_policy ACCEPT \
--rules.inbound '[
{"label":"ssh","action":"ACCEPT","protocol":"TCP","ports":"22","addresses":{"ipv4":["203.0.113.10/32"]}},
{"label":"web","action":"ACCEPT","protocol":"TCP","ports":"80,443","addresses":{"ipv4":["0.0.0.0/0"],"ipv6":["::/0"]}}
]'
linode-cli firewalls device-create <firewall-id> --id <linode-id> --type linode
Keep a host firewall (UFW) as a second layer if you prefer, but make sure both allow the same ports; a mismatch is a common cause of "site unreachable" after setup.
3. Prepare the server
ssh root@<instance-ip>
adduser deploy && usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Then, as deploy, disable root and password SSH logins (PermitRootLogin no, PasswordAuthentication no in /etc/ssh/sshd_config.d/), and install packages:
sudo apt update && sudo apt -y upgrade
sudo apt -y install python3-venv python3-dev build-essential libpq-dev nginx git
sudo mkdir -p /srv/myapp && sudo chown deploy:www-data /srv/myapp
git clone git@github.com:example/myapp.git /srv/myapp/app
python3 -m venv /srv/myapp/venv
/srv/myapp/venv/bin/pip install -r /srv/myapp/app/requirements.txt gunicorn
4. Set up the database
Managed PostgreSQL. Create a database cluster in the same region. Add the instance's public IPv4 address (or its VPC address if both are in a VPC) to the cluster's trusted sources; connections from anywhere else are refused. Use the connection details from Cloud Manager and require TLS. Managed clusters present a certificate you can download; set sslmode=verify-full with that CA file when you want full verification.
Local PostgreSQL. Install postgresql, create a role and database, and keep it listening on localhost only. You then own backups and upgrades.
Store configuration in an environment file:
sudo install -o root -g www-data -m 640 /dev/null /etc/myapp.env
DJANGO_SETTINGS_MODULE=config.settings.production
DJANGO_SECRET_KEY=replace-me
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgres://myapp:pw@<cluster-host>:<port>/myapp?sslmode=require
5. Run Gunicorn with a socket-activated service
# /etc/systemd/system/myapp.socket
[Socket]
ListenStream=/run/myapp.sock
SocketUser=www-data
[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp Gunicorn
Requires=myapp.socket
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp.env
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application --workers 3 --access-logfile -
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
With socket activation, systemd owns the socket and passes it to Gunicorn, so no --bind is needed and Nginx connections queue during restarts.
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.socket myapp.service
Run migrate and collectstatic with the environment file loaded before starting the service for the first time.
6. Configure Nginx and TLS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
client_max_body_size 20m;
location /static/ {
alias /srv/myapp/app/staticfiles/;
expires 30d;
}
location / {
proxy_pass http://unix:/run/myapp.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo nginx -t && sudo systemctl reload nginx
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Point A and AAAA records at the instance's IPv4 and IPv6 addresses before running Certbot. Set SECURE_PROXY_SSL_HEADER, SECURE_SSL_REDIRECT, secure cookies, and CSRF_TRUSTED_ORIGINS in Django.
7. Media and backups
Object Storage is S3-compatible, so django-storages' S3 backend works with a custom endpoint:
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "myapp-media",
"endpoint_url": "https://<region-cluster>.linodeobjects.com",
"access_key": os.environ["OBJ_ACCESS_KEY"],
"secret_key": os.environ["OBJ_SECRET_KEY"],
"default_acl": "private",
"querystring_auth": True,
},
},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
Use the endpoint shown for your bucket's region in Cloud Manager. Enable the Backup Service on the instance for whole-machine recovery, and keep independent database dumps for point recovery.
Verification check:
curl -sI https://example.com/ | head -1
sudo systemctl status myapp --no-pager
sudo nginx -t
Explanation
The Cloud Firewall and trusted sources give network isolation outside the operating system, so a misconfigured service on the instance is not automatically exposed. Socket activation keeps the app reachable during restarts, and moving the database and media to managed services makes the instance itself replaceable, which is what makes rebuilds and scaling to a second instance behind a NodeBalancer straightforward later.
Edge cases / notes
Adding a NodeBalancer
A NodeBalancer can terminate TLS or pass TCP through. If it terminates TLS, it forwards X-Forwarded-Proto; keep SECURE_PROXY_SSL_HEADER and restrict the instance firewall so web ports accept traffic only from the NodeBalancer's private range.
Rollback
Deploy releases into versioned directories or tags and switch with a symlink or git checkout, followed by systemctl reload myapp. Instance backups are for disaster recovery, not routine rollbacks.
When to automate this
The firewall, instance, and database cluster can be declared in Terraform with the Linode provider; server configuration and releases fit a deploy script or Ansible. Automate first the parts you repeat on every release: pull, install, migrate, collectstatic, reload.
Next steps
- See the complete Linux walkthrough in Deploy Django with Gunicorn and Nginx on Ubuntu.
- Configure S3-compatible storage in depth with How to Store Django Media Files on Amazon S3.
- Lock the server down with the Linux Server Hardening Checklist for Django.
- If Nginx cannot reach the app, follow Fix Nginx Not Connecting to Gunicorn (Connection Refused).
FAQ
Is Linode the same as Akamai Cloud?
Yes. Akamai acquired Linode and the compute platform is now marketed as Akamai Cloud. The CLI, API, and Cloud Manager still use Linode naming.
Why can't my Django app connect to the Linode managed database?
The instance's IP is probably not in the cluster's trusted sources, or the connection is not using TLS. Add the IP and include sslmode=require in the connection string.
Do I need both a Cloud Firewall and UFW?
No, one firewall is enough if it is correctly configured. Many teams keep both for defence in depth; the important part is that both allow the same ports.
Can I use Linode Object Storage with django-storages?
Yes. It is S3-compatible; use the S3 backend with the bucket's regional endpoint URL and access keys.