Tutorial

Django with PostgreSQL on a Free Tier in 2026: What Actually Works

SnapDeploy Team 2026-10-07 Updated 2026-10-07 7 min read
djangopythonpostgresqldockerfree-tier

Django is easy to run locally and surprisingly easy to run badly in production: the dev server left on, static files served by Python, a SQLite file that vanishes on the next deploy. This guide takes a standard Django project to a live URL on a free container tier, with a real PostgreSQL database, and is honest about where "free" stops.

Everything below was tested with Django 5.2 LTS (supported until April 2028) on Python 3.12. Django 6.1 is the current release; nothing here is version-specific.

What "production-ready" means for a small Django app

  • Gunicorn serves the app, not manage.py runserver.
  • WhiteNoise serves static files from the container, so you do not need a CDN or a second service for CSS and JavaScript.
  • Settings come from environment variables: SECRET_KEY, DEBUG, ALLOWED_HOSTS, DATABASE_URL.
  • PostgreSQL, because the container filesystem is ephemeral. A SQLite file works for a demo, but every deploy and every wake-up starts from a fresh filesystem, so the data is gone.

Step 1: the four files

requirements.txt (pin your own versions; these are the packages that matter):

django>=5.2,<5.3
gunicorn
whitenoise
dj-database-url
psycopg[binary]

settings.py, the parts that change:

import os
import dj_database_url

SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS if h]

DATABASES = {
    "default": dj_database_url.config(
        default="sqlite:///db.sqlite3", conn_max_age=600
    )
}

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ...the rest of your middleware
]

STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

Dockerfile. SnapDeploy generates one when it finds manage.py, but writing your own gives you control over the Python version and the start command:

FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN SECRET_KEY=build python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["sh", "-c", "python manage.py migrate --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 2 --timeout 60"]

Replace config.wsgi with your project's WSGI module. The collectstatic step runs at build time with a throwaway secret so the image already contains the compressed static files. Migrations run at start; with a single container that is safe, and it means a deploy that adds a column never leaves the app pointing at an old schema.

.dockerignore:

.git
.venv
venv
__pycache__
*.pyc
db.sqlite3
.env
staticfiles

Step 2: a PostgreSQL database

No free tier we know of gives you a permanent, always-on Postgres for nothing, so pick the trade-off that fits:

Option Cost The catch
SnapDeploy DB Sprint Pack$1 for 12 hours (PostgreSQL 16, 1 GB RAM, 5 GB storage)A trial, not a home. Data is kept 24 hours after expiry; convert to Mini ($29/month) to keep it.
SnapDeploy PostgreSQL Mini$29/monthNot free. Same dashboard as the container, connection string injected, daily backups.
Neon (free plan)Free0.5 GB of storage and 100 compute-hours a month; the database suspends when idle and resumes on the next query.
Supabase (free plan)Free500 MB, two projects, paused after seven days without activity, no automated backups.
Render (free Postgres)Free1 GB; expires 30 days after creation, then a 14-day grace period before deletion.

Whichever you choose, the app only needs one thing: a DATABASE_URL in the form postgresql://user:password@host:5432/dbname. With a SnapDeploy add-on that variable is injected into the container for you; with an external provider you paste it into the container's environment variables.

Step 3: deploy

  1. Push the project to GitHub.
  2. In the SnapDeploy dashboard, open Containers and choose Deploy from GitHub. Connect the repository. If it has a Dockerfile, that is used; otherwise the generated Python Dockerfile installs requirements.txt and runs Gunicorn against the WSGI module it finds.
  3. Set the environment variables: SECRET_KEY (a long random string), DEBUG=false, ALLOWED_HOSTS with your container's hostname, and DATABASE_URL unless a SnapDeploy database is attached.
  4. Deploy. The first build takes a few minutes. The URL is on containers.snapdeploy.app.

Two things people forget: ALLOWED_HOSTS must contain the exact hostname, or every request is a 400; and CSRF_TRUSTED_ORIGINS must include the https:// origin, or every form post is a 403 behind the proxy.

Where free stops

  • The free container is 512 MB and 0.25 vCPU. Two Gunicorn workers are plenty; four will swap.
  • Free containers sleep after 15 minutes without traffic and wake in about 60 seconds. Django's first request after a wake also warms its own caches, so budget for a slow first page.
  • 100 container-hours a month. An app that is visited a few times a day uses a fraction of that; an app you keep pinging to stay awake burns through it.
  • Custom domains need an Always-On container ($12/month), which also removes the sleeping.
  • Media uploads: the filesystem is ephemeral. Put user uploads in object storage (S3 or compatible), not on disk.

Health check and logs

Add a tiny view that returns 200 at /health and, if you like, runs SELECT 1 against the database. It costs nothing and it is the first thing you will look at when something breaks. Gunicorn's access log goes to stdout, which is what the container logs show.

from django.db import connection
from django.http import JsonResponse

def health(request):
    with connection.cursor() as cur:
        cur.execute("SELECT 1")
    return JsonResponse({"status": "ok"})

Test the image before you push

Most "it works locally but not in the container" problems are visible in thirty seconds on your own machine. Build and run the same image the platform will run:

docker build -t myapp .
docker run --rm -p 8000:8000 -e SECRET_KEY=dev -e DEBUG=false -e ALLOWED_HOSTS=localhost myapp
curl -i http://localhost:8000/health

If this returns 200 with SQLite, the only thing the container host adds is the real DATABASE_URL. If it returns 400, the hostname is not in ALLOWED_HOSTS; if the static files are missing, collectstatic did not run in the build.

Environment variables at a glance

Variable Value If you forget it
SECRET_KEY50+ random characters, never the one from the repositoryThe app refuses to start (our settings read it without a default, on purpose).
DEBUGfalseTracebacks with settings values are shown to visitors.
ALLOWED_HOSTSThe container hostname, comma-separated if severalEvery request is a 400.
DATABASE_URLInjected by a SnapDeploy add-on, or pasted from Neon, Supabase or RenderThe app silently uses SQLite and loses data on the next deploy.

Background work on a container that sleeps

A free container sleeps after 15 minutes without HTTP traffic, so a management command you run "every hour" from inside the app will not run while the container is asleep, and a second container dedicated to a worker has no HTTP traffic at all, so it is treated as idle and slept too. For a free-tier Django app the honest options are: do periodic work when a request arrives (a lightweight check at the top of a view), trigger it from outside with a scheduled HTTP call from a service that runs elsewhere, or move to Always-On ($12/month), where the container never sleeps and a Celery worker or a cron loop behaves as you expect.

Try it: the Django deployment page has the same steps in short form. The free tier is 4 containers, 100 container-hours a month, no card; a $1 DB Sprint Pack gives you a full PostgreSQL for 12 hours when you want to test the whole stack.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles