This guide covers deploying Blik to production using various platforms.
Dokploy is a self-hosted PaaS that makes deployment simple. Here's how to deploy Blik:
- A Dokploy instance running on your server
- A domain name pointing to your server
- SSL certificate (Let's Encrypt is automatically handled by Dokploy)
- Log into your Dokploy dashboard
- Click "Create Application"
- Choose "Docker Compose" as the deployment method
- Connect your Git repository or upload the code
In Dokploy's environment variables section, set the following (use .env.production as a template):
# SECRET_KEY is auto-generated if not set (recommended to set for production persistence)
SECRET_KEY=<your-secret-key-here>
# CRITICAL: Disable debug mode in production
DEBUG=False
# REQUIRED: Add your domain(s) here, comma-separated
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
# REQUIRED: CSRF trusted origins (include protocol)
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
# CRITICAL: Set to True when using HTTPS (which you should be!)
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=True
# REQUIRED: Encryption key for sensitive data (SMTP passwords in database)
# Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
ENCRYPTION_KEY=<your-encryption-key-here>Note on SECRET_KEY:
- If not set, a secure random key is auto-generated on first startup
- For production, it's recommended to set a persistent value so sessions remain valid across restarts
- The auto-generated key will be displayed in the logs on first startup
Generate keys manually:
# SECRET_KEY
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
# ENCRYPTION_KEY
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"DATABASE_NAME=blik
DATABASE_USER=blik
DATABASE_PASSWORD=<generate-secure-password>
DATABASE_HOST=db
DATABASE_PORT=5432EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=noreply@yourdomain.comAny EMAIL_* variable you set here is authoritative: it is written to the
organization on every container start, and the matching field on the Settings
page is shown read-only ("Managed by EMAIL_HOST in the environment"). To
configure SMTP from the admin UI instead, leave these unset or empty
(EMAIL_HOST=). See ADMIN_GUIDE.md.
SITE_DOMAIN=feedback.yourdomain.com
SITE_PROTOCOL=httpsAll links in outgoing email — reviewer invitations, self-assessments, report
links — are built from SITE_DOMAIN, never from the incoming request's host.
Behind a reverse proxy the request host is often an internal upstream name, so
deriving links from it would send recipients a URL they cannot open. Set
SITE_DOMAIN to the public hostname your users type in the browser.
ORGANIZATION_NAME=Your Company Name# Internal port for main application container
# Default: 8000 (usually no need to change)
PORT=8000
# Public nginx port (if using multi-container setup)
# Default: 80
NGINX_PORT=80Note: Platforms like Dokploy/Railway handle port mapping automatically. Only change if you have port conflicts.
If you're running Blik as a SaaS product with Stripe billing, configure these:
# Get from Stripe Dashboard > Developers > API keys
STRIPE_PUBLISHABLE_KEY=pk_live_your_key_here
STRIPE_SECRET_KEY=sk_live_your_key_here
# Get from Stripe Dashboard > Developers > Webhooks
# After creating webhook endpoint at: https://yourdomain.com/api/stripe/webhook
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# Create products in Stripe Dashboard > Products
# Copy the Price ID for each product (starts with price_...)
STRIPE_PRICE_ID_SAAS=price_your_saas_price_id_here
STRIPE_PRICE_ID_ENTERPRISE=price_your_enterprise_price_id_hereSee docs/STRIPE_WEBHOOKS.md for complete Stripe setup instructions.
- Click "Deploy" in Dokploy
- Wait for the build and deployment to complete
- Dokploy will automatically:
- Build the Docker images
- Run database migrations
- Collect static files
- Start the application
Option A: Use the Setup Wizard (Recommended)
- Visit
https://yourdomain.com/setup/ - Follow the interactive setup wizard to:
- Create your admin account
- Configure organization details
- Set up email (optional, can be configured later)
- Generate test data if desired
Option B: Auto-Setup (Advanced)
Add these to environment variables before first deployment:
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_EMAIL=admin@yourdomain.com
DJANGO_SUPERUSER_PASSWORD=<secure-password>Dokploy handles SSL automatically:
- In your application settings, enable "SSL/TLS"
- Choose "Let's Encrypt" for automatic certificate
- Add your domain name
- Dokploy will obtain and auto-renew the certificate
SSL Termination Architecture
Your load balancer or reverse proxy handles SSL termination:
- External traffic: HTTPS (encrypted) from users to load balancer
- Internal traffic: HTTP (unencrypted) from load balancer to your container
The container listens on HTTP (port 8000). SSL is terminated upstream.
Security cookie settings:
SESSION_COOKIE_SECURE=True # Safe: Django sees X-Forwarded-Proto: https
CSRF_COOKIE_SECURE=True # Safe: Django sees X-Forwarded-Proto: httpsThese work because the load balancer forwards the X-Forwarded-Proto: https header, telling Django the original request was HTTPS.
❌ Avoid infinite redirect loops:
Do NOT force HTTPS at the application level when SSL is terminated upstream:
- Load balancer terminates SSL, forwards HTTP to container
- If container redirects to HTTPS, load balancer forwards HTTP again
- Infinite loop
✅ Correct configuration:
- SSL termination at load balancer/proxy
- Set
SESSION_COOKIE_SECURE=TrueandCSRF_COOKIE_SECURE=True - Do NOT set
SECURE_SSL_REDIRECT=Truein Django - Do NOT add HTTPS redirects inside the container
For deploying without Dokploy on any server with Docker:
git clone https://github.com/yourusername/blik.git
cd blikcp .env.production .env
# Edit .env with your production settings
nano .envCritical settings to change:
SECRET_KEY- Generate a new one (or leave blank for auto-generation)ENCRYPTION_KEY- Generate with cryptography.fernet (required!)DEBUG=FalseALLOWED_HOSTS- Your domain(s)CSRF_TRUSTED_ORIGINS- Your domain(s) with https:// protocolDATABASE_PASSWORD- Secure passwordEMAIL_*- Your SMTP settings (these override the admin UI — see above)SITE_DOMAIN- Public hostname; every link in outgoing email is built from itSESSION_COOKIE_SECURE=TrueCSRF_COOKIE_SECURE=True
docker compose up -dThe entrypoint script will automatically:
- Wait for PostgreSQL
- Run migrations
- Collect static files
- Load default questionnaires
SSL Termination
SSL should be terminated at the reverse proxy, not in the application container.
Example Nginx Configuration:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # CRITICAL: Tells Django request was HTTPS
}
location /static/ {
alias /path/to/blik/staticfiles/;
}
}Example Caddy Configuration (Simpler - SSL automatic):
yourdomain.com {
reverse_proxy localhost:8000
# Caddy automatically sets X-Forwarded-Proto
}
Required Headers:
The X-Forwarded-Proto header tells Django the original request protocol, enabling secure cookies:
SESSION_COOKIE_SECURE=True
CSRF_COOKIE_SECURE=TrueWithout this header, these settings will break (Django thinks all requests are HTTP).
Do NOT set SECURE_SSL_REDIRECT=True - it causes infinite loops when SSL is terminated upstream.
Visit https://yourdomain.com/setup/ to complete the setup wizard.
- Add
heroku.ymlor use buildpacks - Set environment variables via Heroku dashboard
- Add PostgreSQL addon:
heroku addons:create heroku-postgresql:mini - Deploy:
git push heroku main
- Connect your GitHub repository
- Railway auto-detects Dockerfile
- Set environment variables in Railway dashboard
- Add PostgreSQL service from Railway marketplace
- Deploy automatically on push
DigitalOcean offers two deployment options:
App Platform (Recommended) - Fully managed PaaS with auto-scaling and zero-downtime deployments:
- One-click deploy via "Deploy to DigitalOcean" button in README
- Automatic SSL certificates and GitHub integration
- Cost: ~$20/month (web + database)
Droplet - VPS with full control and lower cost:
- Docker Compose setup with automated deployment script
- Manual SSL setup with Let's Encrypt
- Cost: ~$7-15/month depending on configuration
See the comprehensive DigitalOcean Deployment Guide for detailed instructions on both options, including:
- One-click App Platform deployment
- Droplet setup with automation scripts
- SSL configuration
- Database backups
- Troubleshooting guide
Use the Docker Compose configuration with your platform's container orchestration:
- AWS: ECS with RDS PostgreSQL
- GCP: Cloud Run with Cloud SQL
- Azure: Container Instances with Azure Database for PostgreSQL
- SSL/HTTPS is configured and working
-
DEBUG=Falsein production - Strong
SECRET_KEYis set (or auto-generated) -
ENCRYPTION_KEYis set and unique - Database password is secure
-
ALLOWED_HOSTSincludes your domain(s) -
CSRF_TRUSTED_ORIGINSincludes your domain(s) with https:// - Security cookies enabled (
SESSION_COOKIE_SECURE=True,CSRF_COOKIE_SECURE=True)
- Email is configured and tested
- Admin account created (via /setup/ or env vars)
- Default questionnaires loaded
- Static files are being served correctly
- Database backups configured
- Monitoring/logging set up (optional)
- Port configuration correct (if customized)
- Enable 2-factor authentication on your Google account
- Generate an App Password: https://myaccount.google.com/apppasswords
- Use the app password in
EMAIL_HOST_PASSWORD
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=youremail@gmail.com
EMAIL_HOST_PASSWORD=your-16-char-app-passwordNote: Gmail has sending limits (500 emails/day). For production, use a transactional email service.
- Sign up at https://sendgrid.com
- Create an API key
- Configure:
EMAIL_HOST=smtp.sendgrid.net
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=apikey
EMAIL_HOST_PASSWORD=your-sendgrid-api-keyEMAIL_HOST=email-smtp.us-east-1.amazonaws.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-ses-smtp-username
EMAIL_HOST_PASSWORD=your-ses-smtp-passwordEMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=postmaster@yourdomain.mailgun.org
EMAIL_HOST_PASSWORD=your-mailgun-passwordManual Backup:
docker compose exec db pg_dump -U blik blik > backup_$(date +%Y%m%d).sqlRestore from Backup:
docker compose exec -T db psql -U blik blik < backup_20241024.sqlAutomated Backups: Set up a cron job:
0 2 * * * cd /path/to/blik && docker compose exec -T db pg_dump -U blik blik | gzip > /backups/blik_$(date +\%Y\%m\%d).sql.gz# Pull latest changes
git pull origin main
# Rebuild and restart
docker compose down
docker compose up -d --build
# Migrations run automatically via entrypoint# All logs
docker compose logs -f
# Just the web app
docker compose logs -f web
# Just the database
docker compose logs -f dbdocker compose up -d --scale web=3Symptom: Browser shows "too many redirects" or "redirect loop" error.
Cause: Application is redirecting to HTTPS, but load balancer already terminated SSL and forwards HTTP to the container.
Solution:
- Verify
X-Forwarded-Protoheader is set by your load balancer/proxy - Ensure Django settings do NOT include
SECURE_SSL_REDIRECT = True - Remove any HTTPS redirects in application-level nginx/code
- Keep only:
SESSION_COOKIE_SECURE=True CSRF_COOKIE_SECURE=True
Symptom: "CSRF verification failed" error on forms.
Causes and solutions:
-
Missing CSRF_TRUSTED_ORIGINS:
CSRF_TRUSTED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
Must include the full protocol (https://) and domain.
-
Load balancer not forwarding X-Forwarded-Proto:
- Verify your proxy includes:
proxy_set_header X-Forwarded-Proto $scheme; - Most platforms/load balancers set this automatically
- Verify your proxy includes:
-
Wrong ALLOWED_HOSTS:
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
Must match the domain you're accessing.
- Check if collectstatic ran:
docker compose logs web | grep "static" - Verify volume mount:
docker compose exec web ls -la /app/staticfiles - Ensure your reverse proxy serves
/static/correctly
- Check database is running:
docker compose ps db - Verify credentials in
.envmatchdocker-compose.yml - Test connection:
docker compose exec web python manage.py dbshell
- Test SMTP settings:
docker compose exec web python manage.py shellfrom django.core.mail import send_mail send_mail('Test', 'Body', 'from@example.com', ['to@example.com'])
- Check logs:
docker compose logs web | grep -i email - Verify firewall allows outbound SMTP (port 587/465)
# Fix static files permissions
docker compose exec web chown -R www-data:www-data /app/staticfiles
# Fix media files permissions
docker compose exec web chown -R www-data:www-data /app/mediafiles-
Never commit
.envfiles to version control.envis in.gitignoreby default- Use
.env.productionas a template only
-
Generate unique keys for production
# SECRET_KEY (Django) python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' # ENCRYPTION_KEY (for SMTP passwords) python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
WARNING: Never reuse the default
ENCRYPTION_KEYfrom.env.examplein production! -
Keep dependencies updated
uv pip list --outdated uv pip install --upgrade <package>
-
Regular security audits
uv pip install safety safety check
-
Use strong passwords
- Database password: 32+ characters
- Admin password: 16+ characters
- SECRET_KEY: Django's generated key (50+ characters)
- ENCRYPTION_KEY: Fernet-generated key
-
Enable fail2ban or similar
- Protect against brute force login attempts
-
Regular backups
- Database: Daily automated backups
- Retention: Keep 7-30 days of backups
- Test restore process quarterly
-
Monitor logs
- Set up centralized logging (e.g., Sentry, LogDNA)
- Alert on errors and security events
-
Review docker-compose.yml
- Remove the hardcoded
SECRET_KEYline if present - Ensure all secrets come from environment variables
- Remove the hardcoded
- Documentation:
/docs/directory - Issues: GitHub Issues
- Setup Wizard: Available at
/setup/on first run