diff --git a/.env b/.env index 06f57c520..72dc04c1b 100644 --- a/.env +++ b/.env @@ -1 +1,14 @@ +# This file specifies the environment variables that will be set when using the +# `heroku local` command locally during development. It has no effect on apps +# running on Heroku - to set env vars for those, see: +# https://devcenter.heroku.com/articles/config-vars + +# This is used by gunicorn.conf.py and Django's settings.py to set appropriate +# configuration for development vs production. +ENVIRONMENT="development" + +# Prevent log buffering when using using `heroku local` with Django management commands. +PYTHONUNBUFFERED=1 + +# An example env var used in the tutorial. TIMES=2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..a139d887f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# Note: Delete this file if you are copying the code in this repository into your own project. + +# Default to requesting pull request reviews from the Heroku Languages team. +#ECCN:Open Source +#GUSINFO:Languages,Heroku Python Platform +* @heroku/languages + +# However, request review from the Heroku language owner for files that are updated +# by Dependabot, to reduce team review request noise. +requirements.txt @edmorley diff --git a/.github/PULL_REQUEST_TEMPLATE.MD b/.github/PULL_REQUEST_TEMPLATE.MD new file mode 100644 index 000000000..eb8203925 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.MD @@ -0,0 +1,10 @@ + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..55dbe8564 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" diff --git a/.gitignore b/.gitignore index f95d164a6..2f694d7b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -venv -*.pyc -staticfiles -.env +# Python files +__pycache__/ +.venv/ +venv/ + +# Django files +staticfiles/ db.sqlite3 + +# macOS files +.DS_Store diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..a45d8a607 --- /dev/null +++ b/.python-version @@ -0,0 +1,2 @@ +# The Python version to use for the project. +3.14 diff --git a/Procfile b/Procfile index 47a27c07f..62e39fbe0 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,7 @@ -web: gunicorn gettingstarted.wsgi --log-file - +web: gunicorn --config gunicorn.conf.py gettingstarted.wsgi + +# Uncomment this `release` process if you are using a database, so that Django's model +# migrations are run as part of app deployment, using Heroku's Release Phase feature: +# https://docs.djangoproject.com/en/6.0/topics/migrations/ +# https://devcenter.heroku.com/articles/release-phase +#release: ./manage.py migrate --no-input diff --git a/Procfile.windows b/Procfile.windows index 69789e554..f593965a1 100644 --- a/Procfile.windows +++ b/Procfile.windows @@ -1 +1 @@ -web: python manage.py runserver 0.0.0.0:5000 +web: python manage.py runserver %PORT% diff --git a/README.md b/README.md index 30cc47c48..0806f3220 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,44 @@ -# python-getting-started +# Python Getting Started -A barebones Python app, which can easily be deployed to Heroku. +A barebones Django app, which can easily be deployed to Heroku. -This application supports the [Getting Started with Python on Heroku](https://devcenter.heroku.com/articles/getting-started-with-python) article - check it out. +This application supports the tutorials for both the [Cedar and Fir generations](https://devcenter.heroku.com/articles/generations) of the Heroku platform. You can check them out here: -## Running Locally +- [Getting Started on Heroku with Python](https://devcenter.heroku.com/articles/getting-started-with-python) +- [Getting Started on Heroku Fir with Python](https://devcenter.heroku.com/articles/getting-started-with-python-fir) -Make sure you have Python [installed properly](http://install.python-guide.org). Also, install the [Heroku Toolbelt](https://toolbelt.heroku.com/) and [Postgres](https://devcenter.heroku.com/articles/heroku-postgresql#local-setup). +## Deploying to Heroku -```sh -$ git clone git@github.com:heroku/python-getting-started.git -$ cd python-getting-started +Using resources for this example app counts towards your usage. [Delete your app](https://devcenter.heroku.com/articles/heroku-cli-commands#heroku-apps-destroy) and [database](https://devcenter.heroku.com/articles/heroku-postgresql#removing-the-add-on) as soon as you are done experimenting to control costs. -$ pip install -r requirements.txt +### Deploy on Heroku [Cedar](https://devcenter.heroku.com/articles/generations#cedar) -$ createdb python_getting_started +By default, apps use Eco dynos if you are subscribed to Eco. Otherwise, it defaults to Basic dynos. The Eco dynos plan is shared across all Eco dynos in your account and is recommended if you plan on deploying many small apps to Heroku. Learn more about our low-cost plans [here](https://blog.heroku.com/new-low-cost-plans). -$ python manage.py migrate -$ python manage.py collectstatic +Eligible students can apply for platform credits through our new [Heroku for GitHub Students program](https://blog.heroku.com/github-student-developer-program). -$ heroku local +```term +$ git clone https://github.com/heroku/python-getting-started +$ cd python-getting-started +$ heroku create +$ git push heroku main +$ heroku open ``` -Your app should now be running on [localhost:5000](http://localhost:5000/). +### Deploy on Heroku [Fir](https://devcenter.heroku.com/articles/generations#fir) -## Deploying to Heroku +By default, apps on [Fir](https://devcenter.heroku.com/articles/generations#fir) use 1X-Classic dynos. To create an app on [Fir](https://devcenter.heroku.com/articles/generations#fir) you'll need to +[create a private space](https://devcenter.heroku.com/articles/working-with-private-spaces#create-a-private-space) +first. -```sh -$ heroku create -$ git push heroku master - -$ heroku run python manage.py migrate +```term +$ git clone https://github.com/heroku/python-getting-started +$ cd python-getting-started +$ heroku create --space +$ git push heroku main +$ heroku ps:wait $ heroku open ``` -or - -[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) - -## Documentation For more information about using Python on Heroku, see these Dev Center articles: diff --git a/app.json b/app.json index 00281c3c7..c093460de 100644 --- a/app.json +++ b/app.json @@ -1,8 +1,20 @@ { "name": "Start on Heroku: Python", "description": "A barebones Python app, which can easily be deployed to Heroku.", - "image": "heroku/python", "repository": "https://github.com/heroku/python-getting-started", - "keywords": ["python", "django" ], - "addons": [ "heroku-postgresql" ] + "website": "https://devcenter.heroku.com/articles/getting-started-with-python", + "keywords": ["python", "django"], + "env": { + "DJANGO_SECRET_KEY": { + "description": "The secret key for the Django application.", + "generator": "secret" + } + }, + "environments": { + "test": { + "scripts": { + "test": "./manage.py test --debug-mode" + } + } + } } diff --git a/gettingstarted/asgi.py b/gettingstarted/asgi.py new file mode 100644 index 000000000..4c319693f --- /dev/null +++ b/gettingstarted/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for gettingstarted project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gettingstarted.settings") + +application = get_asgi_application() diff --git a/gettingstarted/settings.py b/gettingstarted/settings.py index 15a865023..a4d5cf55b 100644 --- a/gettingstarted/settings.py +++ b/gettingstarted/settings.py @@ -1,138 +1,247 @@ """ -Django settings for gettingstarted project, on Heroku. For more info, see: -https://github.com/heroku/heroku-django-template +Django settings for gettingstarted project. + +Generated by 'django-admin startproject' using Django 6.0. For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ +https://docs.djangoproject.com/en/6.0/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ +https://docs.djangoproject.com/en/6.0/ref/settings/ """ import os -import dj_database_url - - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) - +import secrets +from pathlib import Path -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ +import dj_database_url -# SECURITY WARNING: change this before deploying to production! -SECRET_KEY = 'i+acxn5(akgsn!sr4^qgf(^m&*@+g1@u^t@=8s@axc41ml*f=s' +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Before using your Heroku app in production, make sure to review Django's deployment checklist: +# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ + +# Django requires a unique secret key for each Django app, that is used by several of its +# security features. To simplify initial setup (without hardcoding the secret in the source +# code) we set this to a random value every time the app starts. However, this will mean many +# Django features break whenever an app restarts (for example, sessions will be logged out). +# In your production Heroku apps you should set the `DJANGO_SECRET_KEY` config var explicitly. +# Make sure to use a long unique value, like you would for a password. See: +# https://docs.djangoproject.com/en/6.0/ref/settings/#std-setting-SECRET_KEY +# https://devcenter.heroku.com/articles/config-vars +# SECURITY WARNING: Keep the secret key used in production secret! +SECRET_KEY = os.environ.get( + "DJANGO_SECRET_KEY", + default=secrets.token_urlsafe(nbytes=64), +) -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +# Django has a debug mode which shows more detailed error messages and also means static assets +# can be served without having to run the production `collectstatic` command. However, this +# debug mode *must only be enabled in development* for security and performance reasons: +# https://docs.djangoproject.com/en/6.0/ref/settings/#std-setting-DEBUG +# Debug mode will be automatically enabled when the project is run via `heroku local` (which +# loads the environment variables set in the `.env` file, where `ENVIRONMENT=development`). +# SECURITY WARNING: Don't run with debug turned on in production! +DEBUG = os.environ.get("ENVIRONMENT") == "development" + +# The `DYNO` env var is set on Heroku CI, but it's not a real Heroku app, so we have to +# also explicitly exclude CI: +# https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables +IS_HEROKU_APP = "DYNO" in os.environ and "CI" not in os.environ + +if IS_HEROKU_APP: + # On Heroku, it's safe to use a wildcard for `ALLOWED_HOSTS`, since the Heroku router performs + # validation of the Host header in the incoming HTTP request. On other platforms you may need to + # list the expected hostnames explicitly in production to prevent HTTP Host header attacks. See: + # https://docs.djangoproject.com/en/6.0/ref/settings/#std-setting-ALLOWED_HOSTS + ALLOWED_HOSTS = ["*"] + + # Redirect all non-HTTPS requests to HTTPS. This requires that: + # 1. Your app has a TLS/SSL certificate, which all `*.herokuapp.com` domains do by default. + # When using a custom domain, you must configure one. See: + # https://devcenter.heroku.com/articles/automated-certificate-management + # 2. Your app's WSGI web server is configured to use the `X-Forwarded-Proto` headers set by + # the Heroku Router (otherwise you may encounter infinite HTTP 301 redirects). See this + # app's `gunicorn.conf.py` for how this is done when using gunicorn. + # + # For maximum security, consider enabling HTTP Strict Transport Security (HSTS) headers too: + # https://docs.djangoproject.com/en/6.0/ref/middleware/#http-strict-transport-security + SECURE_SSL_REDIRECT = True +else: + ALLOWED_HOSTS = [".localhost", "127.0.0.1", "[::1]", "0.0.0.0", "[::]"] # Application definition -INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'hello' -) +# Several optional Django features that are present in the default `startproject` template have +# been disabled since they are not used by this example app. To use them, uncomment the relevant +# entries in `INSTALLED_APPS`, `MIDDLEWARE`, `TEMPLATES` and `urls.py`. See: +# https://docs.djangoproject.com/en/6.0/ref/contrib/admin/ +# https://docs.djangoproject.com/en/6.0/topics/auth/ +# https://docs.djangoproject.com/en/6.0/ref/contrib/contenttypes/ +# https://docs.djangoproject.com/en/6.0/topics/http/sessions/ +# https://docs.djangoproject.com/en/6.0/ref/contrib/messages/ +INSTALLED_APPS = [ + # Use WhiteNoise's runserver implementation instead of the Django default, for dev-prod parity. + "whitenoise.runserver_nostatic", + # "django.contrib.admin", + # "django.contrib.auth", + # "django.contrib.contenttypes", + # "django.contrib.sessions", + # "django.contrib.messages", + "django.contrib.staticfiles", + "hello", +] -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + # Django doesn't support serving static assets in a production-ready way, so we use the + # excellent WhiteNoise package to do so instead. The WhiteNoise middleware must be listed + # after Django's `SecurityMiddleware` so that security redirects are still performed. + # See: https://whitenoise.readthedocs.io + "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", +] -ROOT_URLCONF = 'gettingstarted.urls' +ROOT_URLCONF = "gettingstarted.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'debug': True, - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + # "django.contrib.auth.context_processors.auth", + # "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'gettingstarted.wsgi.application' +WSGI_APPLICATION = "gettingstarted.wsgi.application" # Database -# https://docs.djangoproject.com/en/1.9/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +if IS_HEROKU_APP: + # In production on Heroku the database configuration is derived from the `DATABASE_URL` + # environment variable by the dj-database-url package. `DATABASE_URL` will be set + # automatically by Heroku when a database addon is attached to your Heroku app. See: + # https://devcenter.heroku.com/articles/provisioning-heroku-postgres#application-config-vars + # https://github.com/jazzband/dj-database-url + DATABASES = { + "default": dj_database_url.config( + env="DATABASE_URL", + conn_max_age=600, + conn_health_checks=True, + ssl_require=True, + ), } -} +else: + # When running locally in development or in CI, a sqlite database file will be used instead + # to simplify initial setup. Longer term it's recommended to use Postgres locally too. + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } + # Password validation -# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] + # Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ +# https://docs.djangoproject.com/en/6.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_L10N = True -USE_TZ = True +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" -# Update database configuration with $DATABASE_URL. -db_from_env = dj_database_url.config(conn_max_age=500) -DATABASES['default'].update(db_from_env) +USE_I18N = True -# Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +USE_TZ = True -# Allow all host headers -ALLOWED_HOSTS = ['*'] # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles') -STATIC_URL = '/static/' +# https://docs.djangoproject.com/en/6.0/howto/static-files/ -# Extra places for collectstatic to find static files. -STATICFILES_DIRS = ( - os.path.join(PROJECT_ROOT, 'static'), -) +STATIC_ROOT = BASE_DIR / "staticfiles" +STATIC_URL = "static/" -# Simplified static file serving. -# https://warehouse.python.org/project/whitenoise/ -STATICFILES_STORAGE = 'whitenoise.django.GzipManifestStaticFilesStorage' +STORAGES = { + # Enable WhiteNoise's GZip (and Brotli, if installed) compression of static assets: + # https://whitenoise.readthedocs.io/en/latest/django.html#add-compression-and-caching-support + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} +# Don't store the original (un-hashed filename) version of static files, to reduce slug size: +# https://whitenoise.readthedocs.io/en/latest/django.html#WHITENOISE_KEEP_ONLY_HASHED_FILES +WHITENOISE_KEEP_ONLY_HASHED_FILES = True + +# Customise the default logging config, since by default full Django logs are only emitted when +# `DEBUG=True` (which otherwise makes diagnosing errors much harder in production): +# https://docs.djangoproject.com/en/6.0/ref/logging/#default-logging-configuration +# For more advanced logging you may want to try: https://django-structlog.readthedocs.io +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "[{levelname}] {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "simple", + }, + }, + # Fallback for anything not configured via `loggers`. + "root": { + "handlers": ["console"], + "level": "INFO", + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + # Prevent double logging due to the root logger. + "propagate": False, + }, + "django.request": { + # Suppress the WARNINGS from any HTTP 4xx responses (in particular for 404s caused by + # web crawlers), but still show any ERRORs from HTTP 5xx responses/exceptions. + "level": "ERROR", + }, + }, +} diff --git a/gettingstarted/static/humans.txt b/gettingstarted/static/humans.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/gettingstarted/urls.py b/gettingstarted/urls.py index 608147ae5..f1f285138 100644 --- a/gettingstarted/urls.py +++ b/gettingstarted/urls.py @@ -1,16 +1,29 @@ -from django.conf.urls import include, url +""" +URL configuration for gettingstarted project. -from django.contrib import admin -admin.autodiscover() +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" -import hello.views +# from django.contrib import admin +from django.urls import path -# Examples: -# url(r'^$', 'gettingstarted.views.home', name='home'), -# url(r'^blog/', include('blog.urls')), +import hello.views urlpatterns = [ - url(r'^$', hello.views.index, name='index'), - url(r'^db', hello.views.db, name='db'), - url(r'^admin/', include(admin.site.urls)), + path("", hello.views.index, name="index"), + path("db/", hello.views.db, name="db"), + # Uncomment this and the entry in `INSTALLED_APPS` if you wish to use the Django admin feature: + # https://docs.djangoproject.com/en/6.0/ref/contrib/admin/ + # path("admin/", admin.site.urls), ] diff --git a/gettingstarted/wsgi.py b/gettingstarted/wsgi.py index 620b37498..f3eaa28d8 100644 --- a/gettingstarted/wsgi.py +++ b/gettingstarted/wsgi.py @@ -4,14 +4,13 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gettingstarted.settings") from django.core.wsgi import get_wsgi_application -from whitenoise.django import DjangoWhiteNoise + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gettingstarted.settings") application = get_wsgi_application() -application = DjangoWhiteNoise(application) \ No newline at end of file diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 000000000..c20e84407 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,86 @@ +# Gunicorn configuration file: +# https://docs.gunicorn.org/en/stable/configure.html +# https://docs.gunicorn.org/en/stable/settings.html +# +# Note: The classic Python buildpack currently sets a few gunicorn settings automatically via +# the `GUNICORN_CMD_ARGS` env var (which take priority over the settings in this file): +# https://github.com/heroku/heroku-buildpack-python/blob/main/vendor/python.gunicorn.sh + +import os + +# On Heroku, web dynos must bind to the port number specified via the `PORT` env var. This +# env var is set automatically for web dynos and also when using `heroku local` locally: +# https://devcenter.heroku.com/articles/dyno-startup-behavior#port-binding-of-web-dynos +# https://devcenter.heroku.com/articles/heroku-local +# Gunicorn will automatically use the `PORT` env var, however, by default it will bind to the +# port using the IPv4 interface (`0.0.0.0`). We configure the binding manually to make it bind +# to the IPv6 interface (`::`) instead, so that the app works in IPv6-only environments too. +# (IPv4 connections will still work so long as `IPV6_V6ONLY` hasn't been enabled.) +bind = ["[::]:{}".format(os.environ.get("PORT", 5006))] + +# The default `sync` worker is more suited to CPU/network-bandwidth bound workloads, so we +# instead use the thread based worker type for improved support of blocking I/O workloads: +# https://docs.gunicorn.org/en/stable/design.html#server-model +# +# If you need to further improve the performance of blocking I/O workloads, you may want to +# try the `gevent` worker type, though you will need to disable `preload_app`, enable DB +# connecting pooling, and be aware that gevent's monkey patching can break some packages. +# +# Note: When changing the number of dynos/workers/threads you will want to make sure you +# do not exceed the maximum number of connections to external services such as DBs: +# https://devcenter.heroku.com/articles/python-concurrency-and-database-connections +worker_class = "gthread" + +# gunicorn will start this many worker processes. The Python buildpack automatically sets a +# default for WEB_CONCURRENCY at dyno boot, based on the number of CPUs and available RAM: +# https://devcenter.heroku.com/articles/python-concurrency +workers = os.environ.get("WEB_CONCURRENCY", 1) + +# Each `gthread` worker process will use a pool of this many threads. +threads = 5 + +# Workers silent for more than this many seconds are killed and restarted. +# Note: This only affects the maximum request time when using the `sync` worker. +# For all other worker types it acts only as a worker heartbeat timeout. +timeout = 20 + +# After receiving a restart signal, workers have this much time to finish serving requests. +# This should be set to a value less than the 30 second Heroku dyno shutdown timeout: +# https://devcenter.heroku.com/articles/dyno-shutdown-behavior +graceful_timeout = 20 + +# The number of seconds an idle Keep-Alive connection is kept open. This should be greater than +# the Heroku Router's Keep-Alive idle timeout of 90 seconds, to ensure that the closing of idle +# connections is always initiated by the router and not gunicorn, to prevent a race condition +# if the router sends a request to the app just as gunicorn is closing the connection: +# https://devcenter.heroku.com/articles/http-routing#keepalives +keepalive = 95 + +# Enable logging of incoming requests to stdout. +accesslog = "-" + +# Adjust which fields are included in the access log, and make it use the Heroku logfmt +# style. The `X-Request-Id` and `X-Forwarded-For` headers are set by the Heroku Router: +# https://devcenter.heroku.com/articles/http-routing#heroku-headers +access_log_format = 'gunicorn method=%(m)s path="%(U)s" status=%(s)s duration=%(M)sms request_id=%({x-request-id}i)s fwd="%({x-forwarded-for}i)s" user_agent="%(a)s"' + +if os.environ.get("ENVIRONMENT") == "development": + # Automatically restart gunicorn when the app source changes in development. + reload = True +else: + # Load the app before the worker processes are forked, to reduce memory usage and boot times. + # We don't enable this in development, since it's incompatible with `reload = True`. + preload_app = True + + # Use `SO_REUSEPORT` on the listening socket, which allows for more even request + # distribution between workers. See: https://lwn.net/Articles/542629/ + # We don't enable this in development, since it makes it harder to notice when + # duplicate gunicorn processes have accidentally been launched (eg in different + # terminals), since the "address already in use" error no longer occurs. + reuse_port = True + + # Trust the `X-Forwarded-Proto` header set by the Heroku Router during TLS termination, + # (https://devcenter.heroku.com/articles/http-routing#heroku-headers) so that HTTPS requests + # are correctly marked as secure. This allows the WSGI app (in our case, Django) to distinguish + # between HTTP and HTTPS requests for features like HTTP->HTTPS URL redirection. + forwarded_allow_ips = "*" diff --git a/hello/apps.py b/hello/apps.py new file mode 100644 index 000000000..6b541fb46 --- /dev/null +++ b/hello/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HelloConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "hello" diff --git a/hello/migrations/0001_initial.py b/hello/migrations/0001_initial.py index 99c4a82a5..a502ed715 100644 --- a/hello/migrations/0001_initial.py +++ b/hello/migrations/0001_initial.py @@ -1,23 +1,32 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.1 on 2016-01-27 21:54 -from __future__ import unicode_literals +# Generated by Django 6.0 on 2025-12-08 13:11 from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Greeting', + name="Greeting", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('when', models.DateTimeField(auto_now_add=True, verbose_name=b'date created')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "when", + models.DateTimeField( + auto_now_add=True, verbose_name="date created" + ), + ), ], ), ] diff --git a/hello/models.py b/hello/models.py index 89d76fd1b..974d64f7e 100644 --- a/hello/models.py +++ b/hello/models.py @@ -1,5 +1,7 @@ from django.db import models # Create your models here. + + class Greeting(models.Model): - when = models.DateTimeField('date created', auto_now_add=True) + when = models.DateTimeField("date created", auto_now_add=True) diff --git a/hello/templates/base.html b/hello/templates/base.html index 97ec07386..4a9450c99 100644 --- a/hello/templates/base.html +++ b/hello/templates/base.html @@ -1,7 +1,8 @@ -Python Getting Started on Heroku + Python Getting Started on Heroku + @@ -54,20 +55,38 @@ How Heroku Works +