1. Is SQLite Fine for a Django Site?
Yes, for most small and medium sites. Every new Django project starts with SQLite, and you do not have to switch databases just because you deploy. The SQLite team says it handles sites with up to about 100,000 visits a day well, and they call that estimate cautious.
SQLite has real advantages for a small team. There is no database server to install, patch or pay for. Queries skip the network, so simple reads are very fast. Your entire database is one file that you can copy, inspect and restore.
The catch is not the database. The catch is deployment. A Django project that runs perfectly with runserver often breaks in three predictable ways on its first day online. This guide fixes all three, then walks through a full deploy.
2. Three Things That Break on Launch Day
- The database disappears. Many hosts rebuild your app from scratch on every deploy and throw away the old disk. If
db.sqlite3sits next tomanage.py, it goes with it. You lose every user, order and post. - The site has no styles. With
DEBUG = False, Django stops serving CSS, JavaScript and images. The admin looks broken, and so does your site. - Every page returns an error. Django refuses requests for host names that are not in
ALLOWED_HOSTS. Forms fail with a CSRF error if your https address is not inCSRF_TRUSTED_ORIGINS.
Each fix takes a few lines. Make them before your first deploy, and launch day becomes boring in the best way.
3. Production Settings
Read every value that differs between your laptop and your server from environment variables. Then the same settings.py works everywhere, and no secret ends up in Git.
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS if host not in ("localhost", "127.0.0.1")]
# Your host ends https before the request reaches Django
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
Generate a new secret key for production. Never reuse the one Django put in your project, because it is probably in your Git history.
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Set DJANGO_ALLOWED_HOSTS to every address your site answers on, separated by commas. For example, myshop.com,www.myshop.com.
4. Database Settings for SQLite
Two changes make SQLite production-ready. Store the file in a folder your host keeps, and tune SQLite for several workers.
DATA_DIR = Path(os.environ.get("APP_DATA_DIR", BASE_DIR / "data"))
DATA_DIR.mkdir(parents=True, exist_ok=True)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DATA_DIR / "db.sqlite3",
"OPTIONS": {
"timeout": 20,
"transaction_mode": "IMMEDIATE",
"init_command": (
"PRAGMA journal_mode=WAL;"
"PRAGMA synchronous=NORMAL;"
"PRAGMA foreign_keys=ON;"
),
},
}
}
Here is what each option does.
- WAL mode lets readers and the writer work at the same time. Without it, a single write blocks every page view.
- timeout makes a worker wait up to 20 seconds for a lock instead of failing at once with "database is locked."
- IMMEDIATE transactions take the write lock at the start of a transaction. This avoids a common deadlock where two workers both read, then both try to write.
- synchronous=NORMAL speeds up writes and stays safe in WAL mode.
The transaction_mode and init_command options need Django 5.1 or later. On older versions, upgrade Django or run the PRAGMA commands from a connection_created signal.
5. Static Files with WhiteNoise
WhiteNoise lets Django serve its own CSS, JavaScript and images efficiently. You do not need a separate web server or storage bucket. Install it and add it to your requirements.
pip install whitenoise
pip freeze > requirements.txt
Add the middleware directly after Django's security middleware, then tell Django where to collect files.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ...the rest of your middleware
]
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
collectstatic copies every static file into STATIC_ROOT. The compressed manifest storage adds a fingerprint to each file name, so browsers can cache files for a long time and still get new versions after a deploy. Run collectstatic when the app starts, as shown in the deploy steps below.
6. User Uploads
Files that users upload, such as avatars and attachments, are not static files. They must live in the folder your host keeps, next to the database.
MEDIA_URL = "media/"
MEDIA_ROOT = DATA_DIR / "media"
WhiteNoise does not serve uploaded files. A small site can serve them through Django with a URL pattern.
from django.conf import settings
from django.urls import re_path
from django.views.static import serve
urlpatterns += [
re_path(r"^media/(?P<path>.*)$", serve, {"document_root": settings.MEDIA_ROOT}),
]
Django's documentation calls this approach inefficient, and it is. For a site with a few hundred uploads a day, it works fine. If your site serves large files or heavy traffic, store uploads in an object storage service such as Amazon S3 with the django-storages package.
7. Run Migrations Safely
Run migrate on every deploy, before the new code takes traffic. If a migration fails, the old version should keep serving visitors. Never run migrations by hand on a live server and hope.
On Cloudilio, put the command in the field called "Run before each publish," under the app's advanced settings.
python manage.py migrate --noinput
Cloudilio runs this command against your data folder after it builds the new version and before it switches visitors over. If the command fails, publishing stops, and your current version stays live. The publish log shows the migration output so you can see what went wrong.
Before your first deploy, run Django's own production check on your laptop. It lists missing security settings.
DJANGO_SECRET_KEY=test DJANGO_DEBUG=false python manage.py check --deploy
8. Deploy on Cloudilio Step by Step
With the settings above in place, the deploy itself takes about ten minutes.
- Check your files. Your project folder needs
manage.py, your project package and arequirements.txtthat lists Django, Gunicorn and WhiteNoise. Leavedb.sqlite3,.venvandstaticfilesout of the upload. - Create the app. Upload a zip of the folder or link your GitHub repository. Cloudilio finds
manage.py, detects Django and starts it with Gunicorn. - Add your secrets. On the Secrets page, add
DJANGO_SECRET_KEYandDJANGO_ALLOWED_HOSTS. Include the app's free web address and your own domain in the hosts list. - Set the start command. In advanced settings, set the start command so static files are collected each time the app starts. Replace
mysitewith your project's name.python manage.py collectstatic --noinput && gunicorn mysite.wsgi --bind 0.0.0.0:$PORT --workers 2 - Add the migration command. Put
python manage.py migrate --noinputin "Run before each publish." - Publish. Watch the publish log. When it finishes, open the site and the admin to confirm styles load.
- Create your admin user. Add
DJANGO_SUPERUSER_PASSWORDas a secret. Then open the Tasks page, paste the command below under Run a command and press Run. Delete the secret afterwards.python manage.py createsuperuser --noinput --username admin --email you@example.com
Connect your own domain on the Web address page. Cloudilio sets up https for it automatically.
9. Back Up a Live SQLite Database
Copying a live SQLite file with cp can produce a broken copy, because a write may happen halfway through. In WAL mode, recent changes may also sit in a separate -wal file. Use SQLite's own backup method instead. VACUUM INTO writes a clean, compact copy and works anywhere Python runs.
import sqlite3
conn = sqlite3.connect("/app/data/db.sqlite3")
conn.execute("VACUUM INTO '/app/data/backup.sqlite3'")
conn.close()
On a server with the sqlite3 command-line tool installed, the .backup command does the same job.
sqlite3 /app/data/db.sqlite3 ".backup '/app/data/backup.sqlite3'"
Cloudilio backs up the whole data folder every day and keeps the backups for three to seven days, depending on your plan. You can restore one with a click or download it. If you need a separate copy elsewhere, save the Python snippet as backup.py, run python backup.py as a nightly scheduled task and upload the result to storage you control.
10. When to Switch to PostgreSQL
SQLite stays the right choice for longer than most people expect. Switch when one of these becomes true.
- You need more than one server. SQLite lives on one machine. If you must run your app on several servers behind a load balancer, you need a database server.
- Writes pile up. SQLite allows one writer at a time. If your logs show frequent lock timeouts even with WAL mode, your site writes too often for SQLite.
- You need PostgreSQL features. Full-text search with ranking, JSON queries with indexes and PostGIS are reasons on their own.
Django makes the switch manageable. Run dumpdata on the old database, point DATABASES at PostgreSQL, run migrate and then loaddata. Test the whole process on a copy first.
11. Questions People Ask
Why does my Django site lose its data after every deploy?
Your host replaces the app's disk on each deploy, and your database file lives on that disk. Move the database into a persistent folder, such as /app/data on Cloudilio, by changing DATABASES["default"]["NAME"].
Why does the Django admin have no styling in production?
Django stops serving static files when DEBUG is False. Install WhiteNoise, set STATIC_ROOT and run collectstatic on each start.
How many Gunicorn workers should I run with SQLite?
Two to four workers suit most small sites. WAL mode lets them all read at once. More workers use more memory and do not speed up writes, because SQLite allows one writer at a time.
Why do my forms fail with a CSRF error after deploying?
Since Django 4.0, Django checks the origin of form posts. Add your full https address, such as https://myshop.com, to CSRF_TRUSTED_ORIGINS.
Cloudilio Engineering Team
Database Systems Group • CloudilioCloudilio hosts Python apps. You publish Flask, Django, FastAPI, Streamlit and plain Python scripts without managing a server, and your app's data stays safe between updates.