This guide provides Docker Compose deployment options for Postiz, from quick start configurations to production-ready setups with security hardening.
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 2 cores | 4+ cores |
| RAM | 4 GB | 8+ GB |
| Storage | 10 GB | 50+ GB SSD |
| OS | Debian 11, Ubuntu 20.04 | Debian 12, Ubuntu 24.04, RHEL 9+ |
Verify installation:
docker --version
docker compose version
postiz.example.com)Get Postiz running in under 5 minutes for testing or development.
git clone https://github.com/gitroomhq/postiz-app postiz
cd postiz
cat > .env << EOF
APP_ENV=production
APP_URL=http://localhost:3000
POSTGRES_USER=postiz
POSTGRES_PASSWORD=postiz-dev-password-change-in-production
POSTGRES_DB=postiz
JWT_SECRET=dev-secret-change-in-production-minimum-32-chars
REDIS_PASSWORD=redis-dev-password
EOF
docker compose up -d
docker compose ps
docker compose logs -f app
Open browser: http://localhost:3000
Note: This configuration is for development/testing only. Use production configurations below for live deployments.
Basic Docker Compose setup for small deployments or testing.
services:
app:
image: ghcr.io/gitroomhq/postiz-app:latest
container_name: postiz-app
restart: unless-stopped
environment:
- NODE_ENV=production
- APP_URL=http://localhost:3000
- DATABASE_URL=postgresql://postiz:${POSTGRES_PASSWORD}@postgres:5432/postiz
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
- JWT_SECRET=${JWT_SECRET}
ports:
- "3000:3000"
depends_on:
- postgres
- redis
networks:
- postiz-network
volumes:
- app-uploads:/app/uploads
postgres:
image: postgres:15-alpine
container_name: postiz-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=postiz
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=postiz
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- postiz-network
redis:
image: redis:7-alpine
container_name: postiz-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- postiz-network
worker:
image: ghcr.io/gitroomhq/postiz-app:latest
container_name: postiz-worker
restart: unless-stopped
command: ["node", "dist/apps/workers/main.js"]
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postiz:${POSTGRES_PASSWORD}@postgres:5432/postiz
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
- postgres
- redis
networks:
- postiz-network
networks:
postiz-network:
driver: bridge
volumes:
postgres_data:
driver: local
redis_data:
driver: local
app-uploads:
driver: local
# Database
POSTGRES_PASSWORD=change-this-to-strong-password
# Redis
REDIS_PASSWORD=change-this-to-strong-password
# Application
JWT_SECRET=generate-random-32-char-secret-minimum
APP_URL=http://localhost:3000
NODE_ENV=production
# Email (optional, for notifications)
RESEND_API_KEY=
FROM_EMAIL=
Generate secure secrets:
# JWT Secret (48 characters)
openssl rand -base64 48
# Passwords (32 characters)
openssl rand -base64 32
docker compose up -d
docker compose ps
docker compose logs -f
Hardened Docker Compose configuration for production deployments with security best practices.
services:
app:
image: ghcr.io/gitroomhq/postiz-app:v2.18.0
container_name: postiz-app
restart: unless-stopped
environment:
- NODE_ENV=production
- APP_URL=https://postiz.example.com
- DATABASE_URL=postgresql://postiz:${POSTGRES_PASSWORD}@postgres:5432/postiz
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
- JWT_SECRET=${JWT_SECRET}
- RESEND_API_KEY=${RESEND_API_KEY}
- FROM_EMAIL=${FROM_EMAIL}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- postiz-network
volumes:
- app-uploads:/app/uploads
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:size=100M,mode=1777
cap_drop:
- ALL
user: "1000:1000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
worker:
image: ghcr.io/gitroomhq/postiz-app:v2.18.0
container_name: postiz-worker
restart: unless-stopped
command: ["node", "dist/apps/workers/main.js"]
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postiz:${POSTGRES_PASSWORD}@postgres:5432/postiz
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- postiz-network
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:size=100M,mode=1777
cap_drop:
- ALL
user: "1000:1000"
healthcheck:
test: ["CMD", "pgrep", "-f", "node"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.25'
memory: 256M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
postgres:
image: postgres:15-alpine
container_name: postiz-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=postiz
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=postiz
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --lc-collate=C --lc-ctype=C
volumes:
- postgres_data:/var/lib/postgresql/data
- ./backups:/backups
networks:
- postiz-network
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETGID
- SETUID
- DAC_OVERRIDE
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postiz -d postiz"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
container_name: postiz-redis
restart: unless-stopped
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- postiz-network
security_opt:
- no-new-privileges:true
read_only: true
cap_drop:
- ALL
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
postiz-network:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.28.0.0/16
internal: false
volumes:
postgres_data:
driver: local
redis_data:
driver: local
app-uploads:
driver: local
# ===========================================
# Postiz Production Configuration
# ===========================================
# Database - Generate strong random passwords
POSTGRES_PASSWORD=openssl rand -base64 32
# Redis
REDIS_PASSWORD=openssl rand -base64 32
# Application Security
JWT_SECRET=openssl rand -base64 48
# Application Settings
APP_URL=https://postiz.example.com
NODE_ENV=production
# Email Configuration (for notifications)
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxx
FROM_EMAIL=noreply@example.com
# Optional: R2/S3 Storage
R2_ENDPOINT=
R2_ACCESS_KEY=
R2_SECRET_KEY=
R2_BUCKET=
R2_REGION=auto
# Optional: OIDC Authentication
OIDC_ENABLED=false
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_SCOPE=openid,email,profile
OIDC_LOGIN_ENABLED=false
# Copy environment file
cp .env.production .env
# Edit environment variables
nano .env
# Deploy with production configuration
docker compose -f docker-compose.prod.yml up -d
# Verify deployment
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f app
Applied in production configuration:
| Security Setting | Purpose |
|---|---|
read_only: true |
Prevent filesystem modifications |
cap_drop: ALL |
Remove all Linux capabilities |
security_opt: no-new-privileges |
Prevent privilege escalation |
tmpfs mounts |
Temporary writable directories |
user: "1000:1000" |
Run as non-root user |
| Resource limits | Prevent DoS via resource exhaustion |
| Network isolation | Internal network for DB/Redis |
1. Enable Docker Content Trust:
export DOCKER_CONTENT_TRUST=1
2. Use Docker secrets for sensitive data:
echo "your-secret" | docker secret create jwt_secret -
3. Apply AppArmor profile:
# Create /etc/apparmor.d/docker-postiz
docker run --security-opt apparmor=docker-postiz ...
4. Enable user namespace remapping:
// /etc/docker/daemon.json
{
"userns-remap": "default"
}
UFW (Ubuntu/Debian):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (for Let's Encrypt)
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
firewalld (RHEL/CentOS):
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
Install Nginx:
sudo apt-get install -y nginx # Debian/Ubuntu
sudo dnf install -y nginx # RHEL/CentOS
Create configuration:
sudo nano /etc/nginx/sites-available/postiz
server {
listen 80;
server_name postiz.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name postiz.example.com;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/postiz.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/postiz.example.com/privkey.pem;
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;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self';" always;
# Logging
access_log /var/log/nginx/postiz_access.log;
error_log /var/log/nginx/postiz_error.log;
# Client upload size limit
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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_cache_bypass $http_upgrade;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
# Health check endpoint
location /health {
proxy_pass http://127.0.0.1:3000/health;
access_log off;
}
}
Enable site:
sudo ln -s /etc/nginx/sites-available/postiz /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Obtain TLS certificate:
sudo apt-get install -y certbot python3-certbot-nginx # Debian/Ubuntu
sudo certbot --nginx -d postiz.example.com
docker-compose.traefik.yml:
services:
app:
image: ghcr.io/gitroomhq/postiz-app:v2.18.0
labels:
- "traefik.enable=true"
- "traefik.http.routers.postiz.rule=Host(`postiz.example.com`)"
- "traefik.http.routers.postiz.entrypoints=websecure"
- "traefik.http.routers.postiz.tls.certresolver=letsencrypt"
- "traefik.http.services.postiz.loadbalancer.server.port=3000"
- "traefik.http.middlewares.postiz-headers.headers.stsSeconds=31536000"
- "traefik.http.middlewares.postiz-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.postiz-headers.headers.stsPreload=true"
Create backup script:
sudo nano /opt/postiz/backup.sh
#!/bin/bash
set -e
BACKUP_DIR="/opt/postiz/backups"
DATE=$(date +%Y%m%d_%H%M%S)
POSTGRES_CONTAINER="postiz-postgres"
REDIS_CONTAINER="postiz-redis"
POSTGRES_USER="postiz"
POSTGRES_DB="postiz"
# Create backup directory
mkdir -p $BACKUP_DIR/{postgres,redis,uploads}
# Backup PostgreSQL
docker exec $POSTGRES_CONTAINER pg_dump -U $POSTGRES_USER $POSTGRES_DB | gzip > $BACKUP_DIR/postgres/postiz_$DATE.sql.gz
# Backup Redis (if persistence enabled)
if [ -f /var/lib/docker/volumes/postiz_redis_data/_data/dump.rdb ]; then
cp /var/lib/docker/volumes/postiz_redis_data/_data/dump.rdb $BACKUP_DIR/redis/dump_$DATE.rdb
fi
# Backup uploads
tar -czf $BACKUP_DIR/uploads/uploads_$DATE.tar.gz -C /var/lib/docker/volumes/postiz_app-uploads/_data .
# Backup environment file
cp /opt/postiz/.env $BACKUP_DIR/env_$DATE.backup
# Remove backups older than 30 days
find $BACKUP_DIR -type f -mtime +30 -delete
echo "Backup completed: $DATE"
Make executable:
chmod +x /opt/postiz/backup.sh
Add cron job:
crontab -e
# Daily backup at 2 AM
0 2 * * * /opt/postiz/backup.sh >> /var/log/postiz-backup.log 2>&1
1. Stop services:
docker compose -f docker-compose.prod.yml down
2. Restore PostgreSQL:
gunzip < postgres/postiz_20260216_020000.sql.gz | docker exec -i postiz-postgres psql -U postiz -d postiz
3. Restore uploads:
tar -xzf uploads/uploads_20260216_020000.tar.gz -C /var/lib/docker/volumes/postiz_app-uploads/_data
4. Restart services:
docker compose -f docker-compose.prod.yml up -d
Check container status:
docker compose ps
View resource usage:
docker stats postiz-app postiz-worker postgres redis
Check logs:
docker compose logs -f app
docker compose logs -f worker
docker compose logs -f postgres
Configure rsyslog:
sudo nano /etc/rsyslog.d/postiz.conf
:programname, isequal, "docker" /var/log/postiz/docker.log
& stop
1. Check for updates:
docker compose pull
2. Review release notes:
https://github.com/gitroomhq/postiz-app/releases
3. Backup before update:
/opt/postiz/backup.sh
4. Update:
docker compose down
docker compose up -d
5. Verify:
docker compose ps
docker compose logs -f
Container won’t start:
docker compose logs app
docker compose down && docker compose up -d
Database connection timeout:
docker compose exec postgres pg_isready
docker compose logs postgres
Worker not processing jobs:
docker compose logs worker
docker compose restart worker
Permission denied errors:
sudo chown -R 1000:1000 /var/lib/docker/volumes/postiz_app-uploads/_data
Memory issues:
docker update --memory=2g postiz-app
Enable debug logging:
# In .env file
LOG_LEVEL=debug
DEBUG=true
Restart with debug:
docker compose down
docker compose up -d
docker compose logs -f
Complete reset (WARNING: Deletes all data):
docker compose down -v
rm -rf .env
# Reconfigure from scratch
Any questions?
Feel free to contact us. Find all contact information on our contact page.