This guide provides production-ready Docker Compose configurations for deploying Psono with PostgreSQL, reverse proxy, TLS termination, and security hardening. Suitable for Linux DevOps teams requiring containerized secret management.
Current Stable Version: v15.x | Docker Images:
psono/psono-server:latest,psono/psono-server:15.1, Alpine variant available
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Internet β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Reverse Proxy (Nginx) β
β TLS Termination (443) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Psono Server Container β
β (Python/Django, Port 8080 internal) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL Container β
β (Port 5432 internal) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Requirement | Version | Notes |
|---|---|---|
| Docker | 20.10+ | Docker Engine or Docker Desktop |
| Docker Compose | 2.0+ | Or Docker Compose Plugin |
| Linux OS | Debian 10+, Ubuntu 20.04+, RHEL 9+ | Kernel 4.15+ |
| CPU | 2+ cores | 4+ cores recommended |
| RAM | 2 GB minimum | 4-8 GB recommended |
| Storage | 10 GB+ SSD | 50+ GB for production |
| Domain | Valid domain name | For TLS certificates |
For evaluation purposes only:
# Create project directory
mkdir -p ~/psono-demo && cd ~/psono-demo
# Create minimal docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
psono:
image: psono/psono-server:latest
container_name: psono
restart: unless-stopped
ports:
- "8080:8080"
environment:
- PSONO_DATABASE_ENGINE=django.db.backends.postgresql
- PSONO_DATABASE_NAME=psono
- PSONO_DATABASE_USER=psono
- PSONO_DATABASE_PASSWORD=psono_demo_password_change_me
- PSONO_DATABASE_HOST=postgres
- PSONO_SECRET_KEY=demo_secret_key_change_in_production_64chars
depends_on:
- postgres
postgres:
image: postgres:15-alpine
container_name: psono-postgres
restart: unless-stopped
environment:
- POSTGRES_DB=psono
- POSTGRES_USER=psono
- POSTGRES_PASSWORD=psono_demo_password_change_me
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:
EOF
# Start services
docker compose up -d
# View logs
docker compose logs -f
Access at: http://localhost:8080
β οΈ Warning: This configuration is NOT secure for production. It lacks TLS, uses weak passwords, and exposes the database port. Use only for testing.
/opt/psono/
βββ docker-compose.yml
βββ .env
βββ nginx/
β βββ nginx.conf
β βββ ssl/
β βββ fullchain.pem
β βββ privkey.pem
βββ backups/
βββ logs/
sudo mkdir -p /opt/psono/{nginx/ssl,backups,logs}
cd /opt/psono
# Generate SECRET_KEY (64 characters)
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(48))")
echo "SECRET_KEY=${SECRET_KEY}" >> .env
# Generate database password
DB_PASSWORD=$(openssl rand -base64 32)
echo "DB_PASSWORD=${DB_PASSWORD}" >> .env
# Generate admin password (store securely)
ADMIN_PASSWORD=$(openssl rand -base64 24)
echo "ADMIN_PASSWORD=${ADMIN_PASSWORD}" >> .env
# Review (then secure or delete .env)
cat .env
# Secure the file
chmod 600 .env
sudo tee /opt/psono/docker-compose.yml << 'EOF'
version: '3.8'
services:
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Psono Server
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
psono-server:
image: psono/psono-server:15.1
container_name: psono-server
restart: unless-stopped
stop_grace_period: 30s
# Network configuration
networks:
- psono-internal
expose:
- "8080"
# Environment variables
environment:
# Database configuration
- PSONO_DATABASE_ENGINE=django.db.backends.postgresql
- PSONO_DATABASE_NAME=${PSONO_DB_NAME:-psono}
- PSONO_DATABASE_USER=${PSONO_DB_USER:-psono}
- PSONO_DATABASE_PASSWORD=${PSONO_DB_PASSWORD}
- PSONO_DATABASE_HOST=postgres
- PSONO_DATABASE_PORT=5432
# Security
- PSONO_SECRET_KEY=${PSONO_SECRET_KEY}
- PSONO_ALLOWED_HOSTS=${PSONO_ALLOWED_HOSTS:-psono.example.com}
# Optional: Email configuration
- PSONO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
- PSONO_EMAIL_HOST=${PSONO_EMAIL_HOST:-smtp.example.com}
- PSONO_EMAIL_PORT=${PSONO_EMAIL_PORT:-587}
- PSONO_EMAIL_USE_TLS=true
- PSONO_EMAIL_HOST_USER=${PSONO_EMAIL_USER:-}
- PSONO_EMAIL_HOST_PASSWORD=${PSONO_EMAIL_PASSWORD:-}
- PSONO_DEFAULT_FROM_EMAIL=${PSONO_FROM_EMAIL:-noreply@example.com}
# Volumes
volumes:
# Settings directory
- psono-settings:/etc/psono
# Static files
- psono-static:/var/www/static
# Logs
- ./logs:/var/log/psono
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Resource limits
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# Dependencies
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# PostgreSQL Database
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
postgres:
image: postgres:15-alpine
container_name: psono-postgres
restart: unless-stopped
networks:
- psono-internal
environment:
- POSTGRES_DB=${PSONO_DB_NAME:-psono}
- POSTGRES_USER=${PSONO_DB_USER:-psono}
- POSTGRES_PASSWORD=${PSONO_DB_PASSWORD}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
volumes:
- postgres-data:/var/lib/postgresql/data
- ./backups/postgres:/backups
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${PSONO_DB_USER:-psono}"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Redis (Optional - for caching/sessions)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
redis:
image: redis:7-alpine
container_name: psono-redis
restart: unless-stopped
command: redis-server --appendonly yes
networks:
- psono-internal
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Nginx Reverse Proxy
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
nginx:
image: nginx:alpine
container_name: psono-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- psono-internal
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- psono-static:/var/www/static:ro
depends_on:
- psono-server
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Networks
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
networks:
psono-internal:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Volumes
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
volumes:
psono-settings:
driver: local
psono-static:
driver: local
postgres-data:
driver: local
redis-data:
driver: local
EOF
sudo tee /opt/psono/nginx/nginx.conf << 'EOF'
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Security headers
server_tokens off;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
listen 80;
server_name psono.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name psono.example.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Static files
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Proxy to Psono server
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://psono-server:8080;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Health check endpoint (no rate limiting)
location /health/ {
proxy_pass http://psono-server:8080/health/;
access_log off;
}
}
}
EOF
# Stop nginx temporarily
docker compose up -d postgres redis psono-server
# Get certificates with Certbot
sudo docker run --rm \
-v /opt/psono/nginx/ssl:/etc/letsencrypt \
certbot/certbot certonly \
--standalone \
-d psono.example.com \
--email admin@example.com \
--agree-tos \
--no-eff-email
# Copy certificates to expected names
sudo cp /opt/psono/nginx/ssl/live/psono.example.com/fullchain.pem /opt/psono/nginx/ssl/fullchain.pem
sudo cp /opt/psono/nginx/ssl/live/psono.example.com/privkey.pem /opt/psono/nginx/ssl/privkey.pem
# Set permissions
sudo chmod 600 /opt/psono/nginx/ssl/privkey.pem
cd /opt/psono
# Start all services
docker compose up -d
# Verify health
docker compose ps
# Check logs
docker compose logs -f psono-server
# Test connectivity
curl -I https://psono.example.com
| Variable | Description | Example | Required |
|---|---|---|---|
PSONO_SECRET_KEY |
Django secret key (64+ chars) | abc123... |
β Yes |
PSONO_ALLOWED_HOSTS |
Comma-separated allowed domains | psono.example.com |
β Yes |
PSONO_DATABASE_ENGINE |
Database backend | django.db.backends.postgresql |
β Yes |
PSONO_DATABASE_NAME |
Database name | psono |
β Yes |
PSONO_DATABASE_USER |
Database user | psono |
β Yes |
PSONO_DATABASE_PASSWORD |
Database password | strong_password |
β Yes |
PSONO_DATABASE_HOST |
Database host | postgres |
β Yes |
PSONO_DATABASE_PORT |
Database port | 5432 |
No |
PSONO_EMAIL_HOST |
SMTP server | smtp.example.com |
No |
PSONO_EMAIL_PORT |
SMTP port | 587 |
No |
PSONO_EMAIL_USER |
SMTP username | noreply@example.com |
No |
PSONO_EMAIL_PASSWORD |
SMTP password | smtp_password |
No |
PSONO_FROM_EMAIL |
From address | noreply@example.com |
No |
sudo tee /usr/local/bin/psono-docker-backup.sh << 'EOF'
#!/bin/bash
set -e
BACKUP_DIR="/opt/psono/backups"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
echo "Starting Psono backup: ${DATE}"
# Create backup directory
mkdir -p ${BACKUP_DIR}/{postgres,settings}
# Backup PostgreSQL
docker exec psono-postgres pg_dump -U psono psono | gzip > ${BACKUP_DIR}/postgres/db_${DATE}.sql.gz
# Backup settings volume
docker run --rm \
-v psono_settings:/source:ro \
-v ${BACKUP_DIR}/settings:${BACKUP_DIR}/settings \
alpine tar czf ${BACKUP_DIR}/settings/settings_${DATE}.tar.gz -C /source .
# Backup static files
docker run --rm \
-v psono_static:/source:ro \
-v ${BACKUP_DIR}/static:${BACKUP_DIR}/static \
alpine tar czf ${BACKUP_DIR}/static/static_${DATE}.tar.gz -C /source .
# Cleanup old backups
find ${BACKUP_DIR} -type f -mtime +${RETENTION_DAYS} -delete
echo "Backup completed: ${DATE}"
EOF
sudo chmod +x /usr/local/bin/psono-docker-backup.sh
# Add to crontab (daily at 2:00 AM)
echo "0 2 * * * /usr/local/bin/psono-docker-backup.sh >> /var/log/psono-backup.log 2>&1" | sudo crontab -
# Stop services
docker compose down
# Restore PostgreSQL
gunzip -c /opt/psono/backups/postgres/db_YYYYMMDD_HHMMSS.sql.gz | \
docker exec -i psono-postgres psql -U psono psono
# Restore settings
docker run --rm \
-v psono_settings:/target \
-v /opt/psono/backups/settings:/source:ro \
alpine tar xzf /source/settings_YYYYMMDD_HHMMSS.tar.gz -C /target
# Restart services
docker compose up -d
# View all logs
docker compose logs -f
# View specific service
docker compose logs -f psono-server
# Last 100 lines
docker compose logs --tail=100 psono-server
# Check container health
docker inspect --format='{{.State.Health.Status}}' psono-server
# Check database health
docker exec psono-postgres pg_isready -U psono
# Check Redis health
docker exec psono-redis redis-cli ping
Add to docker-compose.yml for metrics export:
# Add to existing services
psono-server:
# ... existing config ...
environment:
# ... existing env vars ...
- PSONO_PROMETHEUS_ENABLED=true
- PSONO_PROMETHEUS_PORT=9090
# Add to each service in docker-compose.yml
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /run
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
| Issue | Diagnosis | Solution |
|---|---|---|
| Container wonβt start | Check logs | docker compose logs psono-server |
| Database connection failed | Verify env vars | Check PSONO_DATABASE_* variables |
| TLS certificate errors | Check cert paths | Verify nginx/ssl/ permissions |
| High memory usage | Check limits | Adjust deploy.resources.limits |
| Slow performance | Check resources | Increase CPU/memory allocations |
# Restart all services
docker compose restart
# Rebuild containers
docker compose up -d --force-recreate
# Update to new version
docker compose pull
docker compose up -d
# View resource usage
docker stats
# Access database
docker exec -it psono-postgres psql -U psono
# Access application shell
docker exec -it psono-server /bin/bash
# Clean up unused resources
docker system prune -af
| Tag | Description | Use Case |
|---|---|---|
latest |
Latest stable release | Development/testing |
15.1 |
Specific v15.1 release | Production (pinned) |
15 |
Latest v15.x | Production (minor updates OK) |
alpine |
Alpine-based (smaller) | Resource-constrained environments |
debian |
Debian-based (larger) | Compatibility-focused deployments |
π‘ Production Hint: Always pin to specific version tags in production (e.g.,
15.1) rather than usinglatest. This ensures reproducible deployments and prevents unexpected breaking changes.
Any questions?
Feel free to contact us. Find all contact information on our contact page.