This guide provides a reference for configuring Psono in production environments. Covers server settings, environment variables, database configuration, email setup, and operational parameters for Linux DevOps teams.
Current Version: v15.x | Configuration File:
settings.yamlor environment variables
Psono supports multiple configuration approaches:
| Method | Location | Use Case |
|---|---|---|
| Environment Variables | Docker/container environment | Containerized deployments |
| settings.yaml | /etc/psono/settings.yaml |
Traditional installations |
| Django Settings | Python settings module | Custom deployments |
# /etc/psono/settings.yaml
# Allowed hosts (domain names that can access the server)
ALLOWED_HOSTS:
- psono.example.com
- www.psono.example.com
- localhost
# Django secret key (64+ characters, cryptographically random)
SECRET_KEY: "your-64-character-random-secret-key-here"
# Debug mode (disable in production!)
DEBUG: false
# Application URL
APPLICATION_URL: "https://psono.example.com"
# Docker Compose environment
environment:
- PSONO_ALLOWED_HOSTS=psono.example.com,www.psono.example.com
- PSONO_SECRET_KEY=your-64-character-random-secret-key-here
- PSONO_DEBUG=false
- PSONO_APPLICATION_URL=https://psono.example.com
DATABASES:
default:
ENGINE: django.db.backends.postgresql
NAME: psono
USER: psono
PASSWORD: "your-strong-database-password"
HOST: postgres
PORT: 5432
CONN_MAX_AGE: 600
OPTIONS:
sslmode: require # For production with TLS
PSONO_DATABASE_ENGINE=django.db.backends.postgresql
PSONO_DATABASE_NAME=psono
PSONO_DATABASE_USER=psono
PSONO_DATABASE_PASSWORD=your-strong-database-password
PSONO_DATABASE_HOST=postgres
PSONO_DATABASE_PORT=5432
DATABASES:
default:
ENGINE: django.db.backends.mysql
NAME: psono
USER: psono
PASSWORD: "your-strong-database-password"
HOST: mysql
PORT: 3306
OPTIONS:
charset: utf8mb4
init_command: "SET sql_mode='STRICT_TRANS_TABLES'"
DATABASES:
default:
# ... database settings ...
CONN_MAX_AGE: 600 # Persistent connections (seconds)
CONN_HEALTH_CHECKS: true # Validate before reuse
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: smtp.example.com
EMAIL_PORT: 587
EMAIL_USE_TLS: true
EMAIL_HOST_USER: noreply@example.com
EMAIL_HOST_PASSWORD: "your-smtp-password"
DEFAULT_FROM_EMAIL: "Psono <noreply@example.com>"
SERVER_EMAIL: "noreply@example.com"
PSONO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
PSONO_EMAIL_HOST=smtp.example.com
PSONO_EMAIL_PORT=587
PSONO_EMAIL_USE_TLS=true
PSONO_EMAIL_HOST_USER=noreply@example.com
PSONO_EMAIL_HOST_PASSWORD=your-smtp-password
PSONO_DEFAULT_FROM_EMAIL=Psono <noreply@example.com>
EMAIL_HOST: smtp.gmail.com
EMAIL_PORT: 587
EMAIL_USE_TLS: true
EMAIL_HOST_USER: your-account@gmail.com
EMAIL_HOST_PASSWORD: "your-app-password" # Use app password, not account password
EMAIL_HOST: smtp.office365.com
EMAIL_PORT: 587
EMAIL_USE_TLS: true
EMAIL_HOST_USER: your-account@domain.com
EMAIL_HOST_PASSWORD: "your-password"
EMAIL_HOST: smtp.sendgrid.net
EMAIL_PORT: 587
EMAIL_USE_TLS: true
EMAIL_HOST_USER: apikey
EMAIL_HOST_PASSWORD: "your-sendgrid-api-key"
# Session security
SESSION_COOKIE_SECURE: true # HTTPS only
SESSION_COOKIE_HTTPONLY: true # No JavaScript access
SESSION_COOKIE_SAMESITE: "Lax" # CSRF protection
SESSION_COOKIE_AGE: 1209600 # 2 weeks (seconds)
SESSION_SAVE_EVERY_REQUEST: false
# CSRF protection
CSRF_COOKIE_SECURE: true
CSRF_COOKIE_HTTPONLY: true
CSRF_COOKIE_SAMESITE: "Lax"
# Password requirements
AUTH_PASSWORD_VALIDATORS:
- NAME: django.contrib.auth.password_validation.UserAttributeSimilarityValidator
- NAME: django.contrib.auth.password_validation.MinimumLengthValidator
OPTIONS:
MIN_LENGTH: 12
- NAME: django.contrib.auth.password_validation.CommonPasswordValidator
- NAME: django.contrib.auth.password_validation.NumericPasswordValidator
# Login security
LOGIN_URL: /login/
LOGIN_REDIRECT_URL: /
LOGOUT_REDIRECT_URL: /login/
# Two-factor authentication
MFA_ENABLED: true
MFA_REQUIRED_FOR_ADMINS: true
# API rate limiting
RATE_LIMIT_ENABLED: true
RATE_LIMIT_REQUESTS: 100 # Requests per minute
RATE_LIMIT_WINDOW: 60 # Window in seconds
# Login attempt limiting
LOGIN_ATTEMPT_LIMIT: 5
LOGIN_ATTEMPT_TIMEOUT: 300 # 5 minutes
CACHES:
default:
BACKEND: django.core.cache.backends.redis.RedisCache
LOCATION: "redis://redis:6379/0"
OPTIONS:
socket_timeout: 5
socket_connect_timeout: 5
PSONO_CACHE_BACKEND=django.core.cache.backends.redis.RedisCache
PSONO_CACHE_LOCATION=redis://redis:6379/0
CACHES:
default:
BACKEND: django.core.cache.backends.filebased.FileBasedCache
LOCATION: "/var/cache/psono"
TIMEOUT: 300
LOGGING:
version: 1
disable_existing_loggers: false
formatters:
verbose:
format: "{levelname} {asctime} {module} {message}"
style: "{"
json:
(): pythonjsonlogger.jsonlogger.JsonFormatter
handlers:
console:
class: logging.StreamHandler
formatter: verbose
level: INFO
file:
class: logging.handlers.RotatingFileHandler
filename: /var/log/psono/psono.log
maxBytes: 10485760 # 10MB
backupCount: 5
formatter: verbose
level: DEBUG
loggers:
django:
handlers: [console, file]
level: INFO
propagate: false
psono:
handlers: [console, file]
level: DEBUG
propagate: false
| Level | Use Case |
|---|---|
| DEBUG | Development, troubleshooting |
| INFO | Production (normal operations) |
| WARNING | Production (minimal logging) |
| ERROR | Error tracking only |
# Gunicorn workers (for WSGI server)
GUNICORN_WORKERS: 4
GUNICORN_THREADS: 2
GUNICORN_TIMEOUT: 120
GUNICORN_KEEPALIVE: 5
# Connection pool
DATABASE_POOL_SIZE: 10
DATABASE_MAX_OVERFLOW: 20
# Static file settings
STATIC_URL: /static/
STATIC_ROOT: /var/www/static
STATICFILES_STORAGE: django.contrib.staticfiles.storage.ManifestStaticFilesStorage
# Media files
MEDIA_URL: /media/
MEDIA_ROOT: /var/www/media
# Gzip compression
COMPRESS_ENABLED: true
COMPRESS_OFFLINE: true
COMPRESS_CSS_FILTER:
- compressor.filters.css_default.CssAbsoluteFilter
- compressor.filters.cssmin.rCSSMinFilter
COMPRESS_JS_FILTER:
- compressor.filters.jsmin.JSMinFilter
AUTH_LDAP_SERVER_URI: "ldap://ldap.example.com"
AUTH_LDAP_BIND_DN: "cn=admin,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD: "ldap-admin-password"
AUTH_LDAP_USER_SEARCH:
OUs:
- "ou=users,dc=example,dc=com"
SCOPE: SUBTREE
FILTERSTR: "(uid=%(user)s)"
AUTH_LDAP_GROUP_SEARCH:
DN: "ou=groups,dc=example,dc=com"
SCOPE: SUBTREE
FILTERSTR: "(objectClass=groupOfNames)"
AUTH_LDAP_USER_ATTR_MAP:
first_name: givenName
last_name: sn
email: mail
AUTH_LDAP_ALWAYS_UPDATE_USER: true
AUTH_LDAP_CACHE_TIMEOUT: 3600
AUTH_LDAP_SERVER_URI: "ldaps://ad.example.com"
AUTH_LDAP_BIND_DN: "cn=service_account,ou=service,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD: "service-account-password"
AUTH_LDAP_USER_SEARCH:
OUs:
- "ou=users,dc=example,dc=com"
SCOPE: SUBTREE
FILTERSTR: "(sAMAccountName=%(user)s)"
AUTH_LDAP_GROUP_SEARCH:
DN: "ou=groups,dc=example,dc=com"
SCOPE: SUBTREE
FILTERSTR: "(objectClass=group)"
# AD-specific settings
AUTH_LDAP_USER_FLAGS_BY_GROUP:
is_superuser:
- "cn=PsonoAdmins,ou=groups,dc=example,dc=com"
is_staff:
- "cn=PsonoStaff,ou=groups,dc=example,dc=com"
SOCIAL_AUTH_SAML_ENABLED: true
SOCIAL_AUTH_SAML_SP_ENTITY_ID: "https://psono.example.com"
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
SOCIAL_AUTH_SAML_IDP:
entity_id: "https://idp.example.com/saml"
url: "https://idp.example.com/saml/sso"
x509cert: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
SOCIAL_AUTH_SAML_SECURITY_CONFIG:
requestedAuthnContext: true
wantNameId: true
wantNameIdEncrypted: false
wantAssertionsEncrypted: true
wantAssertionsSigned: true
wantMessagesSigned: true
SOCIAL_AUTH_OIDC_ENABLED: true
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: "https://accounts.example.com"
SOCIAL_AUTH_OIDC_KEY: "your-client-id"
SOCIAL_AUTH_OIDC_SECRET: "your-client-secret"
SOCIAL_AUTH_OIDC_SCOPE:
- openid
- email
- profile
# Backup configuration
BACKUP_ENABLED: true
BACKUP_DIR: /var/backups/psono
BACKUP_RETENTION_DAYS: 7
BACKUP_SCHEDULE: "0 2 * * *" # Daily at 2 AM
# What to backup
BACKUP_DATABASE: true
BACKUP_SETTINGS: true
BACKUP_MEDIA: true
#!/bin/bash
# /usr/local/bin/psono-backup.sh
BACKUP_DIR="/var/backups/psono"
DATE=$(date +%Y%m%d_%H%M%S)
# Database backup
docker exec psono-postgres pg_dump -U psono psono | gzip > ${BACKUP_DIR}/db_${DATE}.sql.gz
# Settings backup
docker cp psono-server:/etc/psono ${BACKUP_DIR}/settings_${DATE}
# Cleanup old backups
find ${BACKUP_DIR} -type f -mtime +7 -delete
# /etc/psono/settings.yaml
# Production Configuration for Psono v15.x
# ─────────────────────────────────────────────────────────
# Basic Settings
# ─────────────────────────────────────────────────────────
ALLOWED_HOSTS:
- psono.example.com
- www.psono.example.com
SECRET_KEY: "your-64-character-cryptographically-random-secret-key"
DEBUG: false
APPLICATION_URL: "https://psono.example.com"
# ─────────────────────────────────────────────────────────
# Database Configuration
# ─────────────────────────────────────────────────────────
DATABASES:
default:
ENGINE: django.db.backends.postgresql
NAME: psono
USER: psono
PASSWORD: "your-strong-database-password"
HOST: postgres
PORT: 5432
CONN_MAX_AGE: 600
OPTIONS:
sslmode: require
# ─────────────────────────────────────────────────────────
# Email Configuration
# ─────────────────────────────────────────────────────────
EMAIL_BACKEND: django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST: smtp.example.com
EMAIL_PORT: 587
EMAIL_USE_TLS: true
EMAIL_HOST_USER: noreply@example.com
EMAIL_HOST_PASSWORD: "your-smtp-password"
DEFAULT_FROM_EMAIL: "Psono <noreply@example.com>"
# ─────────────────────────────────────────────────────────
# Security Settings
# ─────────────────────────────────────────────────────────
SESSION_COOKIE_SECURE: true
SESSION_COOKIE_HTTPONLY: true
SESSION_COOKIE_SAMESITE: "Lax"
CSRF_COOKIE_SECURE: true
CSRF_COOKIE_HTTPONLY: true
AUTH_PASSWORD_VALIDATORS:
- NAME: django.contrib.auth.password_validation.MinimumLengthValidator
OPTIONS:
MIN_LENGTH: 12
MFA_ENABLED: true
MFA_REQUIRED_FOR_ADMINS: true
# ─────────────────────────────────────────────────────────
# Cache Configuration
# ─────────────────────────────────────────────────────────
CACHES:
default:
BACKEND: django.core.cache.backends.redis.RedisCache
LOCATION: "redis://redis:6379/0"
# ─────────────────────────────────────────────────────────
# Logging Configuration
# ─────────────────────────────────────────────────────────
LOGGING:
version: 1
handlers:
console:
class: logging.StreamHandler
level: INFO
file:
class: logging.handlers.RotatingFileHandler
filename: /var/log/psono/psono.log
maxBytes: 10485760
backupCount: 5
level: DEBUG
loggers:
psono:
handlers: [console, file]
level: INFO
SECRET_KEY is 64+ characters and cryptographically randomALLOWED_HOSTS includes only your domain(s)DEBUG is set to false# Test database connection
docker exec psono-postgres psql -U psono -c "\l"
# Test email configuration
docker exec psono-server python manage.py sendtestemail admin@example.com
# Check application health
curl -I https://psono.example.com/health/
# Verify settings loaded
docker exec psono-server python manage.py check --deploy
DEBUG: true
ALLOWED_HOSTS:
- localhost
- 127.0.0.1
DATABASES:
default:
ENGINE: django.db.backends.sqlite3
NAME: /var/lib/psono/db.sqlite3
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
DEBUG: false
ALLOWED_HOSTS:
- psono-staging.example.com
# Use production-like settings but with test data
DEBUG: false
ALLOWED_HOSTS:
- psono.example.com
# All security settings enabled
# Full logging and monitoring
Any questions?
Feel free to contact us. Find all contact information on our contact page.