Repository: fasouto/django-starter-template
Branch: master
Commit: 6a8b9b8a35ba
Files: 33
Total size: 26.4 KB
Directory structure:
gitextract__ab4pzx6/
├── .dockerignore
├── .editorconfig
├── .gitignore
├── .python-version
├── Dockerfile.dev
├── LICENSE
├── apps/
│ ├── __init__.py
│ └── base/
│ ├── __init__.py
│ ├── models.py
│ ├── templates/
│ │ └── base/
│ │ └── home.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── config/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── static/
│ │ ├── css/
│ │ │ └── base.css
│ │ └── js/
│ │ └── app.js
│ ├── templates/
│ │ ├── 403.html
│ │ ├── 404.html
│ │ ├── 500.html
│ │ ├── base.html
│ │ └── robots.txt
│ ├── urls.py
│ └── wsgi.py
├── docker-compose.yml
├── manage.py
├── pyproject.toml
├── railway.toml
└── readme.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.venv
__pycache__
*.pyc
.env
db.sqlite3
*.sqlite3
staticfiles
public/static
public/media
.git
================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
max_line_length = 120
[*.{vue,js,json,html,yml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
# Minified JavaScript files shouldn't be changed
[**.min.js]
indent_style = ignore
insert_final_newline = ignore
================================================
FILE: .gitignore
================================================
## Python
*.py[co]
__pycache__/
*.egg*
.coverage
## Virtual environment
.venv/
## Environment
.env
## Project
staticfiles/
public/static
public/media
## SQLite3
*.sqlite3
## OS
.DS_Store
._*
Thumbs.db
Desktop.ini
## Logs
*.log
## Editors and IDEs
*.sublime-project
*.sublime-workspace
*.swp
*.swo
.idea/
.vscode/
*~
\#*\#
================================================
FILE: .python-version
================================================
3.12
================================================
FILE: Dockerfile.dev
================================================
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --dev
COPY . .
================================================
FILE: LICENSE
================================================
Copyright (c) 2017, Fabio Souto
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: apps/__init__.py
================================================
================================================
FILE: apps/base/__init__.py
================================================
"""Application base, containing global templates."""
================================================
FILE: apps/base/models.py
================================================
"""Base models"""
================================================
FILE: apps/base/templates/base/home.html
================================================
{% extends 'base.html' %}
{% load static %}
{% block title %}Django Starter Template{% endblock %}
{% block content %}
Status
To get started, edit apps/base/templates/base/home.html
{% endblock %}
================================================
FILE: apps/base/tests.py
================================================
import pytest
from django.test import Client
@pytest.mark.django_db
def test_home_page_returns_200():
client = Client()
response = client.get("/")
assert response.status_code == 200
@pytest.mark.django_db
def test_health_check_returns_200():
client = Client()
response = client.get("/health/")
assert response.status_code == 200
assert response.json()["status"] == "ok"
================================================
FILE: apps/base/urls.py
================================================
from django.urls import path
from .views import home
app_name = "base"
urlpatterns = [
path("", home, name="home"),
]
================================================
FILE: apps/base/views.py
================================================
import sys
import django
from django.conf import settings
from django.db import connection
from django.http import JsonResponse
from django.shortcuts import render
def home(request):
db_ok = False
db_is_postgres = False
try:
connection.ensure_connection()
db_ok = True
db_is_postgres = connection.vendor == "postgresql"
except Exception:
pass
secret_key = settings.SECRET_KEY
secret_changed = secret_key not in ("django-insecure-dev-key-change-me", "build-time-placeholder", "change-me-to-a-random-string")
rv = 'Railway variables '
checklist = [
("Database", "Set DATABASE_URL in .env or {}".format(rv), db_ok and (db_is_postgres or settings.DEBUG)),
("SECRET_KEY changed", "Set a unique key in .env or {}".format(rv), secret_changed),
("DEBUG is off", "Set DEBUG=False in production", not settings.DEBUG),
("ALLOWED_HOSTS set", "Add your domain to ALLOWED_HOSTS in {}".format(rv), not settings.DEBUG and len(settings.ALLOWED_HOSTS) > 0),
]
return render(
request,
"base/home.html",
{
"django_version": django.get_version(),
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
"db_status": db_ok,
"checklist": checklist,
},
)
def health_check(request):
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return JsonResponse({"status": "ok", "database": "ok"})
except Exception as e:
return JsonResponse({"status": "error", "database": str(e)}, status=500)
================================================
FILE: config/__init__.py
================================================
================================================
FILE: config/asgi.py
================================================
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
application = get_asgi_application()
================================================
FILE: config/settings/__init__.py
================================================
================================================
FILE: config/settings/base.py
================================================
"""
Base settings shared across all environments.
"""
import sys
from pathlib import Path
import environ
env = environ.Env()
# PATHS
BASE_DIR = Path(__file__).resolve().parent.parent # config/
PROJECT_ROOT = BASE_DIR.parent # project root
sys.path.append(str(PROJECT_ROOT / "apps"))
# GENERAL
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
ALLOWED_HOSTS = []
# INSTALLED APPS
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.admindocs",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.humanize",
"django.contrib.staticfiles",
# Local apps
"base",
]
# PASSWORD HASHING
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.ScryptPasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]
# DEBUG
DEBUG = False
INTERNAL_IPS = ["127.0.0.1"]
# LOCALE
TIME_ZONE = "UTC"
LANGUAGE_CODE = "en-us"
USE_I18N = True
USE_TZ = True
# MEDIA
MEDIA_ROOT = PROJECT_ROOT / "public" / "media"
MEDIA_URL = "/media/"
# STATIC FILES
STATIC_ROOT = PROJECT_ROOT / "staticfiles"
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# TEMPLATES
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# MIDDLEWARE
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# LOGGING
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
================================================
FILE: config/settings/development.py
================================================
from .base import * # noqa: F403
from .base import env
DEBUG = True
SECRET_KEY = env("SECRET_KEY", default="django-insecure-dev-key-change-me")
DATABASES = {
"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3"),
}
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}
}
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Use simple static files storage in development (no collectstatic needed)
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# DJANGO DEBUG TOOLBAR
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405
INSTALLED_APPS += ["debug_toolbar"] # noqa: F405
def show_toolbar(request):
from django.conf import settings
return settings.DEBUG
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": "config.settings.development.show_toolbar",
}
================================================
FILE: config/settings/production.py
================================================
from .base import * # noqa: F403
from .base import env
DEBUG = False
# SECRET_KEY has a build-time fallback so collectstatic can run during
# the Docker/Railpack build phase (before env vars are injected).
# At runtime the real key is always required via the environment variable.
SECRET_KEY = env("SECRET_KEY", default="build-time-placeholder")
DATABASES = {
"default": env.db("DATABASE_URL", default="sqlite:///placeholder"),
}
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[".railway.app"])
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
# SECURITY
# Railway terminates SSL at the proxy; internal traffic is HTTP.
# Let the proxy handle HTTPS redirection, not Django.
SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=False)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
================================================
FILE: config/static/css/base.css
================================================
:root {
--color-bg: #f4f7f4;
--color-surface: #fff;
--color-text: #1b2e1b;
--color-muted: #5a7a5a;
--color-accent: #092e20;
--color-green: #44b78b;
--color-green-light: #e8f5e9;
--color-ok: #2e7d32;
--color-err: #c62828;
--color-border: #d5e3d5;
--radius: 8px;
--max-width: 640px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
min-height: 100vh;
}
a { color: var(--color-accent); transition: color 0.15s; }
a:hover { color: var(--color-green); }
.container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; }
/* Nav */
nav {
background: var(--color-accent);
padding: 0.6rem 0;
border-bottom: 2px solid var(--color-green);
}
nav .container { display: flex; align-items: center; justify-content: space-between; }
.nav-brand { color: #fff; text-decoration: none; font-weight: 700; font-size: 0.95rem; letter-spacing: 0.01em; }
.nav-links a { color: rgba(255,255,255,0.65); text-decoration: none; margin-left: 1.25rem; font-size: 0.8rem; font-weight: 500; transition: color 0.15s; }
.nav-links a:hover { color: var(--color-green); }
/* Main */
main { padding: 3rem 0 4rem; }
/* Hero */
.hero {
text-align: center;
margin-bottom: 2rem;
}
.hero-logo {
width: 72px;
height: 72px;
object-fit: contain;
margin: 1rem 0;
}
.hero h1 {
font-size: 1.4rem;
font-weight: 700;
color: var(--color-accent);
margin-bottom: 0.35rem;
letter-spacing: -0.01em;
}
.hero-subtitle {
color: var(--color-muted);
font-size: 0.9rem;
}
.hero-subtitle a {
color: var(--color-green);
text-decoration: none;
font-weight: 600;
}
.hero-subtitle a:hover { text-decoration: underline; }
/* Checklist */
.checklist {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.25rem 1.5rem;
margin-bottom: 1.75rem;
}
.checklist h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-muted);
font-weight: 600;
margin-bottom: 0.75rem;
}
.checklist ul {
list-style: none;
padding: 0;
}
.checklist li {
display: flex;
align-items: flex-start;
gap: 0.6rem;
padding: 0.65rem 0;
color: var(--color-muted);
}
.checklist li.done { color: var(--color-text); }
.checklist li + li { border-top: 1px solid rgba(0,0,0,0.04); }
.check-label { display: block; font-size: 0.88rem; font-weight: 500; }
.check-hint { display: block; font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; }
.check-hint a { color: var(--color-green); text-decoration: underline; text-underline-offset: 2px; }
.check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.2rem;
border-radius: 50%;
background: #e0e0e0;
font-size: 0.6rem;
font-weight: 700;
color: #999;
flex-shrink: 0;
margin-top: 0.15rem;
}
.done .check {
background: var(--color-ok);
color: #fff;
}
/* Hint */
.hint {
font-size: 0.72rem;
color: var(--color-muted);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border);
}
.hint code {
background: var(--color-green-light);
padding: 0.1em 0.35em;
border-radius: 3px;
}
/* Links */
.quick-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
margin-bottom: 2rem;
}
.quick-links a {
display: inline-block;
padding: 0.4rem 0.9rem;
background: var(--color-accent);
color: #fff;
text-decoration: none;
border: 1px solid var(--color-accent);
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
transition: background 0.15s, border-color 0.15s;
}
.quick-links a:hover { background: #0d3b2a; border-color: #0d3b2a; color: #fff; }
.quick-links a.outline {
background: transparent;
color: var(--color-accent);
border: 1px solid var(--color-border);
}
.quick-links a.outline:hover { border-color: var(--color-accent); background: var(--color-green-light); }
================================================
FILE: config/static/js/app.js
================================================
================================================
FILE: config/templates/403.html
================================================
403 Forbidden
403
You are not authorized to view this page.
Return home
================================================
FILE: config/templates/404.html
================================================
404 Not Found
================================================
FILE: config/templates/500.html
================================================
500 Server Error
================================================
FILE: config/templates/base.html
================================================
{% load static %}
{% block title %}Django Starter{% endblock %}
{% block extra_head %}{% endblock %}
{% block content %}{% endblock %}
{% block extra_js %}{% endblock %}
================================================
FILE: config/templates/robots.txt
================================================
User-agent: *
Disallow: /admin
================================================
FILE: config/urls.py
================================================
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views.generic.base import TemplateView
from base.views import health_check
urlpatterns = [
path("", include("base.urls")),
path("admin/doc/", include("django.contrib.admindocs.urls")),
path("admin/", admin.site.urls),
path("health/", health_check),
path(
"robots.txt",
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
),
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns += debug_toolbar_urls()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
================================================
FILE: config/wsgi.py
================================================
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
application = get_wsgi_application()
================================================
FILE: docker-compose.yml
================================================
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
command: uv run python manage.py runserver 0.0.0.0:8000
volumes:
- .:/app
ports:
- "8000:8000"
env_file: .env
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/postgres
- DJANGO_SETTINGS_MODULE=config.settings.development
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
================================================
FILE: manage.py
================================================
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
# Read .env file before setting defaults so local DJANGO_SETTINGS_MODULE is respected
try:
import environ
environ.Env.read_env(os.path.join(os.path.dirname(__file__), ".env"))
except (ImportError, FileNotFoundError):
pass
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
================================================
FILE: pyproject.toml
================================================
[project]
name = "django-starter"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"django>=5.2.13,<5.3",
"gunicorn>=22.0",
"psycopg[binary,pool]>=3.1",
"whitenoise[brotli]>=6.7",
"django-environ>=0.11",
"argon2-cffi>=23.1",
]
[dependency-groups]
dev = [
"django-debug-toolbar>=4.4",
"ruff>=0.6",
"pytest>=8.0",
"pytest-django>=4.8",
"coverage>=7.6",
]
[tool.ruff]
line-length = 120
target-version = "py312"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.development"
python_files = ["tests.py", "test_*.py"]
================================================
FILE: railway.toml
================================================
[build]
builder = "RAILPACK"
buildCommand = "python manage.py collectstatic --noinput"
[deploy]
preDeployCommand = "python manage.py migrate --noinput"
startCommand = "gunicorn config.wsgi --bind 0.0.0.0:$PORT"
healthcheckPath = "/health/"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
================================================
FILE: readme.md
================================================
Django Starter Template
A production-ready Django 5.2 LTS starter template for [Railway](https://railway.com).
[](https://railway.com/deploy/django-starter-template?referralCode=iZa9TM&utm_medium=integration&utm_source=template&utm_campaign=generic)
## Deploy to Railway
Click the button above to deploy this template. Railway will:
1. Create a new Django web service
2. Provision a PostgreSQL database
3. Set `DATABASE_URL` and `SECRET_KEY` automatically
4. Run migrations on deploy
5. Start the application with gunicorn
Your app will be live in under a minute.
### Environment variables
These are set automatically by Railway. Override them in your service settings if needed:
| Variable | Description | Default |
|----------|-------------|---------|
| `SECRET_KEY` | Django secret key | Auto-generated |
| `DATABASE_URL` | PostgreSQL connection string | Provided by Railway |
| `ALLOWED_HOSTS` | Comma-separated hostnames | `.railway.app` |
| `CSRF_TRUSTED_ORIGINS` | Full URLs for POST requests (e.g. `https://myapp.up.railway.app`) | `[]` |
| `DJANGO_SETTINGS_MODULE` | Settings module | `config.settings.production` |
**Important:** If you add a custom domain, add it to both `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` (with `https://` prefix). Without `CSRF_TRUSTED_ORIGINS`, POST requests (login, admin, forms) will return 403.
## Local Development
### Option A: uv (recommended)
Prerequisites: [Python 3.12+](https://python.org), [uv](https://docs.astral.sh/uv/getting-started/installation/)
```bash
git clone https://github.com/fasouto/django-starter-template.git
cd django-starter-template
# Install dependencies
uv sync --dev
# Set up environment
cp .env.example .env
# Run migrations and create admin user
uv run python manage.py migrate
uv run python manage.py createsuperuser
# Start development server
uv run python manage.py runserver
```
Open [http://localhost:8000](http://localhost:8000). The admin panel is at [http://localhost:8000/admin/](http://localhost:8000/admin/).
```bash
# Run tests
uv run pytest
# Lint and format
uv run ruff check .
uv run ruff format .
```
### Option B: Docker Compose
Prerequisites: [Docker](https://docs.docker.com/get-docker/)
```bash
git clone https://github.com/fasouto/django-starter-template.git
cd django-starter-template
cp .env.example .env
# Start Django + PostgreSQL
docker compose up
# In another terminal:
docker compose exec web uv run python manage.py migrate
docker compose exec web uv run python manage.py createsuperuser
```
Open [http://localhost:8000](http://localhost:8000). Code changes reload automatically.
## Project Structure
```
.
├── apps/
│ └── base/ # Default app (home page, health check, tests)
│ ├── templates/base/ # App templates
│ ├── tests.py # Example tests
│ ├── urls.py
│ └── views.py
├── config/ # Django project package
│ ├── settings/
│ │ ├── base.py # Shared settings
│ │ ├── development.py # Dev settings (DEBUG=True, SQLite)
│ │ └── production.py # Production settings (Postgres, security)
│ ├── static/ # Project-level static files
│ │ └── css/base.css
│ ├── templates/ # Project-level templates (base.html, error pages)
│ ├── asgi.py
│ ├── urls.py
│ └── wsgi.py
├── docker-compose.yml # Local dev with Docker (Django + Postgres)
├── Dockerfile.dev # Dev container
├── pyproject.toml # Dependencies and tool config
├── railway.toml # Railway deployment config
├── uv.lock # Locked dependencies
└── manage.py
```
## What's Included
- **[Django 5.2 LTS](https://docs.djangoproject.com/en/5.2/)**: supported until April 2028
- **[PostgreSQL](https://www.postgresql.org/)** via psycopg3, modern async-capable adapter
- **[WhiteNoise](https://whitenoise.readthedocs.io/)**: serve static files without nginx, with brotli compression
- **[django-environ](https://django-environ.readthedocs.io/)**: configure via environment variables and `.env` files
- **[Argon2](https://docs.djangoproject.com/en/5.2/topics/auth/passwords/#using-argon2-with-django)** password hashing (winner of the Password Hashing Competition)
- **Split settings** for separate development and production configurations
- **Health check** at `/health/`, returns JSON for Railway monitoring
- **[django-debug-toolbar](https://django-debug-toolbar.readthedocs.io/)**: SQL queries, templates, cache inspection (dev only)
- **[ruff](https://docs.astral.sh/ruff/)** for linting and formatting
- **[pytest](https://docs.pytest.org/) + [pytest-django](https://pytest-django.readthedocs.io/)** for testing
## Customization
### Adding a new app
```bash
mkdir apps/myapp
uv run python manage.py startapp myapp apps/myapp
```
Then add `"myapp"` to `INSTALLED_APPS` in `config/settings/base.py`.
### Replacing the CSS
The included `config/static/css/base.css` is minimal and framework-free. Replace it with Bootstrap, Tailwind, or any CSS framework you prefer.
### Adding Celery
Add `celery[redis]` to your dependencies, create `config/celery.py`, and add a Redis service to your Railway project or `docker-compose.yml`.
## License
MIT. See [LICENSE](LICENSE).