How to Deploy Django on Railway
If you want to deploy Django on Railway, the main challenge is not just getting a container to start.
Problem statement
If you want to deploy Django on Railway, the main challenge is not just getting a container to start. The real job is making your app run safely in production with the right process manager, database configuration, static files, environment variables, proxy-aware security settings, and verification steps.
Common Django Railway deployment failures are predictable:
- Gunicorn is missing, so the web process never starts
ALLOWED_HOSTSdoes not include the Railway domain- static files are not collected as part of build/deploy
- migrations are not applied in a controlled release step
DEBUG=Trueis left enabled- secrets are committed into Git instead of stored as environment variables
Railway can be a fast way to host Django, but it does not remove the need for production-safe Django settings.
Quick answer
To deploy Django on Railway safely:
- prepare production Django settings
- add Gunicorn, PostgreSQL URL parsing, and static file handling
- provision PostgreSQL in Railway
- connect your GitHub repo to Railway
- set environment variables such as
DJANGO_SECRET_KEY,DATABASE_URL,ALLOWED_HOSTS, andCSRF_TRUSTED_ORIGINS - set a Gunicorn start command
- run
collectstaticduring the build/deploy process - run migrations as a controlled release step
- verify the site, admin, database access, and static assets
Railway is a good fit for straightforward Django deployments and smaller teams, but you still need to handle production settings correctly.
Step-by-step solution
1) Prepare your Django app for Railway deployment
Add production dependencies
Install the packages most Railway Django deployments need:
pip install gunicorn whitenoise dj-database-url psycopg[binary]
pip freeze > requirements.txt
If you use a different dependency workflow, export a valid requirements.txt before deploy.
What these packages do:
gunicorn: production WSGI serverdj-database-url: parsesDATABASE_URLwhitenoise: serves static files from Django when you are not using external object storage or a CDNpsycopg[binary]: PostgreSQL driver
Update Django production settings
Use environment-driven settings. A minimal example:
import os
from pathlib import Path
import dj_database_url
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = [
host.strip() for host in os.environ.get("ALLOWED_HOSTS", "").split(",") if host.strip()
]
CSRF_TRUSTED_ORIGINS = [
origin.strip()
for origin in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
if origin.strip()
]
DATABASES = {
"default": dj_database_url.parse(
os.environ["DATABASE_URL"],
conn_max_age=600,
ssl_require=True,
)
}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SECURE_SSL_REDIRECT = not DEBUG
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
If your domain is fully HTTPS-only, add HSTS:
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = False
Also run a deployment check locally before pushing:
python manage.py check --deploy
Verification check:
DEBUGmust beFalsein RailwaySECRET_KEYmust come from env varsDATABASE_URLmust point to PostgreSQLALLOWED_HOSTSmust include your Railway domain and custom domain if usedCSRF_TRUSTED_ORIGINSmust include fullhttps://origins
Configure static files for production
If Django will serve static files directly, configure Whitenoise:
INSTALLED_APPS = [
# ...
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ...
]
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
}
}
Then test locally:
python manage.py collectstatic --noinput
Run collectstatic during the build/deploy process, not only as a one-off command after the app starts. Do not rely on the app’s local filesystem for persistent runtime-generated assets.
Verification check:
staticfiles/is created during build or pre-release preparation- Django admin CSS loads with
DEBUG=False - no missing static file errors during
collectstatic
For user-uploaded media, do not rely on app-local storage for long-term production use. Use object storage for persistent media.
Define the app start command
Railway needs a web start command. For a typical Django project:
gunicorn myproject.wsgi:application --bind 0.0.0.0:$PORT
Replace myproject with your actual Django project module.
You can also keep this in a Procfile:
web: gunicorn myproject.wsgi:application --bind 0.0.0.0:$PORT
Verification check:
- the WSGI module path is correct
gunicornis present inrequirements.txt
2) Create the Railway project and services
Create a new Railway project
In Railway:
- create a new project
- choose the GitHub deployment flow
- connect the repository with your Django app
Railway should detect it as a Python app if your repo includes the expected Python project files.
Add a PostgreSQL service
Provision a PostgreSQL service in the Railway project. Railway commonly exposes the connection details through environment variables. Confirm that your app service receives a valid DATABASE_URL. If not, copy the value manually into your app service variables.
Operational note: a hosted database is not a backup strategy by itself. Before risky schema changes, make sure you know how backups or restore points are handled for your environment.
Set required environment variables
At minimum, configure:
DJANGO_SECRET_KEYDEBUG=FalseALLOWED_HOSTS=your-app-domain.up.railway.appCSRF_TRUSTED_ORIGINS=https://your-app-domain.up.railway.app
Add any other app-specific secrets:
- email credentials
- Redis URLs
- third-party API keys
- Sentry DSN
Do not commit these values into Git.
3) Configure Railway build and deploy settings
Set the build behavior
Make sure Railway can install dependencies from requirements.txt.
If you need a specific Python version, pin it using the Python version mechanism supported by your Railway build setup, and verify it in build logs.
The main requirement is consistency between local development and the Railway build environment.
Set the start command
If Railway does not pick it up automatically, set the service start command to:
gunicorn myproject.wsgi:application --bind 0.0.0.0:$PORT
You can tune worker count later, but start simple and verify stability first.
Make static collection part of deploy
Your deployment must include:
python manage.py collectstatic --noinput
This should happen during build or as part of the deploy/release process so the running service starts with static assets ready. Do not treat a manual post-deploy collectstatic run as the normal production pattern.
Add a migration step
Run migrations as a controlled release step for the deployed version:
python manage.py migrate
Do not treat the deployment as healthy until migrations complete and database-backed pages have been verified.
Then verify migration state:
python manage.py showmigrations
If your deployment process supports a pre-deploy or release command, you can move migrations there later. Be careful with destructive migrations. Code rollback alone does not always fix a failed schema change.
4) Deploy Django on Railway step by step
Push the code and trigger the first deployment
Commit your changes and push to the branch connected to Railway:
git add .
git commit -m "Prepare Django for Railway deployment"
git push origin main
Watch the build logs for:
- dependency installation success
collectstaticsuccess- Gunicorn startup without import errors
Run database migrations safely
Once the new version is built and ready for release, run:
python manage.py migrate
For higher-risk production changes, confirm you have a database recovery plan before applying schema changes.
Verification check:
python manage.py showmigrationsshows expected migrations as applied- database-backed pages load without errors
Verify static files
Verification check:
- homepage CSS loads
- admin CSS loads
- browser dev tools show no obvious static 404s
If static assets are missing after deploy, fix the build/deploy sequence rather than relying on ad hoc runtime filesystem changes.
Create an admin user if needed
If this is a fresh environment:
python manage.py createsuperuser
Avoid bootstrap scripts with hardcoded admin credentials. Use a one-time secure creation flow.
5) Verify the Railway deployment
Check application health
Test:
- the homepage
- a database-backed page
- the Django admin login
- static assets
- Railway logs for startup or runtime errors
A simple command-level check is still useful before deployment:
python manage.py check --deploy
Confirm production security basics
Verify that:
DEBUG=FalseSECRET_KEYis not hardcoded- HTTPS works on the Railway domain or custom domain
SECURE_SSL_REDIRECTis active for HTTPS-only deploymentsSESSION_COOKIE_SECUREandCSRF_COOKIE_SECUREare enabled- HSTS is enabled only after the domain is fully HTTPS-only
Test a restart and config persistence
Restart or redeploy the service and confirm:
- environment variables are still present
- the app reconnects to PostgreSQL
- no startup-only assumptions were hiding config problems
6) Add a custom domain and production hardening
Attach a custom domain in Railway
Add your domain in Railway and update your DNS records as instructed there. Wait for domain verification and HTTPS issuance.
Update Django host and CSRF settings
When the custom domain is active, update:
ALLOWED_HOSTS=your-app-domain.up.railway.app,example.com,www.example.com
CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com
CSRF_TRUSTED_ORIGINS must include the scheme.
Review baseline hardening
At minimum, review:
- secure cookies
- HSTS policy if the domain is fully HTTPS-only
- logging level
- error monitoring integration
- trusted origins and proxy/TLS settings
Explanation
This setup works because it covers the parts Railway does not solve for you automatically: the Django app server, production settings, static file handling, proxy-aware security settings, and database configuration.
Gunicorn runs the Django app as a proper production web process. dj-database-url keeps database config environment-driven, which fits Railway well. Whitenoise is a practical default for smaller Django apps because it removes the need for a separate static file server. For larger apps or high static/media volume, object storage and a CDN are usually a better next step.
The important Railway-specific detail is that you should not depend on the service filesystem as durable application storage. That affects both media handling and any attempt to generate production assets after the app is already running.
Railway is a good choice when you want a simpler deployment path than managing your own VM, Nginx, process supervision, and TLS termination. If you need deeper network control, custom reverse proxy behavior, or stricter infrastructure ownership, a VPS or container platform may be a better fit.
Edge cases / notes
Static files vs media files
Whitenoise is suitable for static assets collected at build or deploy time. It is not a full solution for user-uploaded media. Store media in persistent object storage.
On Railway, do not assume the service filesystem is durable across deploys or restarts.
Proxy and TLS headers
Because TLS is terminated upstream, Django commonly needs:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
Without that, secure redirects, absolute URL generation, and host handling may be wrong.
Migration risk
If a deploy fails after a destructive migration, rolling back code may not restore compatibility. Prefer backward-compatible migration sequences and know your database restore path before major releases.
Process sizing and timeouts
A single default Gunicorn command is enough to start, but you may need to tune workers and timeout behavior later based on memory use and request patterns.
When manual Railway deployment becomes repetitive
If you deploy multiple Django apps on Railway, the same work repeats: production settings scaffolding, env var validation, Gunicorn setup, static file config, and migration checks. Those steps are good candidates for reusable templates or deploy scripts. The first automation to add is usually preflight validation plus post-deploy smoke tests.
Internal links
Before going deeper, review this Django production settings checklist to confirm your app is ready for any host.
If you want to understand the application server and reverse proxy layer outside a managed platform, see Deploy Django with Gunicorn and Nginx on Ubuntu.
If your project uses ASGI features such as websockets or long-lived connections, see Deploy Django ASGI with Uvicorn and Nginx.
Before launch, use this Django Deployment Checklist for Production to verify rollback readiness and operational checks.
FAQ
Do I need Gunicorn to deploy Django on Railway?
Yes, for a standard Django production deployment, Gunicorn is the usual choice. Railway needs a web process that serves your Django app, and Gunicorn handles that reliably.
How do I configure static files for Django on Railway?
Set STATIC_ROOT, run collectstatic, and use Whitenoise if Django will serve static files itself. The important part is sequencing: collectstatic should happen during build or deploy, not only as a one-off runtime command after the app is already serving traffic.
How do I run migrations during a Railway deployment?
Run python manage.py migrate as a controlled release step for the deployed version. Do not treat the deploy as healthy until migrations complete and database-backed pages have been verified.
Why is my Django app returning DisallowedHost on Railway?
Your Railway domain is probably missing from ALLOWED_HOSTS. Add the Railway-generated domain and any custom domain to the environment variable, then redeploy.
What should I do if a Railway deploy succeeds but the app fails after migration?
Check whether the issue is code, schema, or configuration. Rolling back app code may be enough for a safe migration, but not for destructive schema changes. For risky migrations, use backups or restore points and prefer backward-compatible release patterns.