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.

Django on Akamai CloudCloud Firewall22, 80, 443 onlyNginxTLS terminationGunicornsystemd serviceManaged Postgrestrusted sourcesKeep the database and uploads off the instance so the compute node can be rebuilt at any time.
  1. Create an Ubuntu LTS instance with an SSH key (no password logins) in a region near your users.
  2. Attach a Cloud Firewall that drops inbound traffic except SSH from your IP, HTTP, and HTTPS.
  3. Create a non-root deploy user, install Python, Nginx, and build dependencies.
  4. Use a managed PostgreSQL cluster with the instance's IP in its trusted sources, or run PostgreSQL locally.
  5. Run Gunicorn under systemd on a Unix socket and proxy to it from Nginx.
  6. Issue certificates with Certbot and set Django's HTTPS settings.
  7. 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
inbound: drop everything elseInbound ruleSource22 SSH✓acceptyour IP addresses only80 HTTP✓acceptanywhere (redirects)443 HTTPS✓acceptanywhere5432 PostgreSQL✕dropnever public8000 Gunicorn✕dropNginx uses the socketDefault inbound policy: drop. Outbound can stay opendjango-deployment.com
The Cloud Firewall filters before traffic reaches the instance, so it also covers a misconfigured ufw.

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.

where should PostgreSQL live?How much database operationsdo you want to own?small, one serverSame instancecheapest, you run backupsgrowing or criticalManaged Databasebackups and failover includedMoving from one to the other later is a dump, a restore, and a settings changedjango-deployment.com
Either way, Django reads the connection details from the environment, not from code.

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"},
}
what gets backed up, and whereLinode Backups enabledwhole-disk, dailyNightly pg_dumpto Object StorageMedia synced off the serveror stored in Object StorageA restore testedon a fresh instanceDisk snapshots alone can catch the database mid-writedjango-deployment.com
A logical dump is consistent by itself; a disk image of a running database may not be.

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

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.

Keep going