This guide provides production-ready Docker Compose configurations for deploying Cachet with security hardening, multiple database options, and operational best practices.
| Component | Version | Image |
|---|---|---|
| Cachet (Stable) | v2.4.1 | cachethq/docker:2.4.1 |
| Cachet (Latest v2) | latest | cachethq/docker:latest |
| Cachet (v3 Dev) | 3.x | cachethq/cachet:3.x |
| MariaDB | 10.11 | mariadb:10.11 |
| MySQL | 8.0 | mysql:8.0 |
| PostgreSQL | 15 | postgres:15 |
| Redis | 7 | redis:7-alpine |
For testing and development environments only.
# docker-compose.dev.yml
services:
cachet:
image: cachethq/docker:2.4.1
container_name: cachet
restart: unless-stopped
ports:
- "8080:8000"
environment:
- APP_ENV=local
- APP_DEBUG=true
- APP_KEY=base64:xJ8K9mN2pQ4rT6vW8yZ0aB3cD5eF7gH9i==
- APP_URL=http://localhost:8080
- DB_DRIVER=sqlite
- DB_DATABASE=/var/www/html/database/database.sqlite
volumes:
- cachet_data:/var/www/html
volumes:
cachet_data:
# Start the stack
docker compose -f docker-compose.dev.yml up -d
# View logs
docker compose logs -f cachet
# Access at http://localhost:8080
β οΈ Warning: This configuration uses SQLite and debug mode. Not suitable for production.
Recommended configuration for most production environments.
/opt/cachet/
βββ docker-compose.yml
βββ .env
βββ secrets/
β βββ db_password.txt
β βββ redis_password.txt
β βββ app_key.txt
βββ nginx/
βββ nginx.conf
# docker-compose.yml
services:
cachet:
image: cachethq/docker:2.4.1
container_name: cachet
restart: unless-stopped
ports:
- "127.0.0.1:8080:8000" # Bind to localhost only
env_file:
- .env
secrets:
- db_password
- redis_password
- app_key
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- cachet_data:/var/www/html
networks:
- cachet_public
- cachet_private
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
- /var/www/html/storage/framework/cache:noexec,nosuid,size=100m
- /var/www/html/storage/framework/sessions:noexec,nosuid,size=100m
- /var/www/html/storage/framework/views:noexec,nosuid,size=100m
- /var/www/html/storage/logs:noexec,nosuid,size=100m
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
db:
image: mariadb:10.11
container_name: cachet-db
restart: unless-stopped
env_file:
- .env.db
secrets:
- db_password
volumes:
- cachet_db:/var/lib/mysql
networks:
- cachet_private
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=50m
- /var/run:noexec,nosuid,size=10m
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: cachet-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass "$$(cat /run/secrets/redis_password)"
secrets:
- redis_password
volumes:
- cachet_redis:/data
networks:
- cachet_private
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=50m
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
volumes:
cachet_data:
cachet_db:
cachet_redis:
networks:
cachet_public:
driver: bridge
cachet_private:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
redis_password:
file: ./secrets/redis_password.txt
app_key:
file: ./secrets/app_key.txt
# .env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://status.example.com
APP_NAME="Production Status"
APP_TIMEZONE=Europe/Berlin
DB_DRIVER=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=cachet
DB_USERNAME=cachet
DB_PASSWORD_FILE=/run/secrets/db_password
CACHE_DRIVER=redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD_FILE=/run/secrets/redis_password
SESSION_DRIVER=redis
SESSION_SECURE_COOKIE=true
QUEUE_DRIVER=redis
# .env.db
MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password
MYSQL_DATABASE=cachet
MYSQL_USER=cachet
MYSQL_PASSWORD_FILE=/run/secrets/db_password
# Create secrets directory
mkdir -p secrets
# Generate secure passwords
openssl rand -base64 32 > secrets/db_password.txt
openssl rand -base64 32 > secrets/redis_password.txt
# Generate Laravel app key
docker run --rm cachethq/docker:2.4.1 php artisan key:generate --show | cut -d: -f2 > secrets/app_key.txt
# Set restrictive permissions
chmod 600 secrets/*.txt
# Validate configuration
docker compose config
# Start the stack
docker compose up -d
# Verify all services are healthy
docker compose ps
# View logs
docker compose logs -f cachet
docker compose logs -f db
Alternative configuration using PostgreSQL database.
# docker-compose.postgres.yml
services:
cachet:
image: cachethq/docker:2.4.1
container_name: cachet
restart: unless-stopped
ports:
- "127.0.0.1:8080:8000"
env_file:
- .env.pg
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- cachet_data:/var/www/html
networks:
- cachet_public
- cachet_private
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
container_name: cachet-db
restart: unless-stopped
environment:
- POSTGRES_DB=cachet
- POSTGRES_USER=cachet
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
secrets:
- db_password
volumes:
- cachet_db:/var/lib/postgresql/data
networks:
- cachet_private
healthcheck:
test: ["CMD-SHELL", "pg_isready -U cachet -d cachet"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: cachet-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- cachet_redis:/data
networks:
- cachet_private
volumes:
cachet_data:
cachet_db:
cachet_redis:
networks:
cachet_public:
driver: bridge
cachet_private:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
# .env.pg
APP_ENV=production
APP_DEBUG=false
APP_URL=https://status.example.com
DB_DRIVER=pgsql
DB_HOST=db
DB_PORT=5432
DB_DATABASE=cachet
DB_USERNAME=cachet
DB_PASSWORD_FILE=/run/secrets/db_password
CACHE_DRIVER=redis
REDIS_HOST=redis
REDIS_PORT=6379
SESSION_DRIVER=redis
QUEUE_DRIVER=redis
# Add to docker-compose.yml
services:
nginx:
image: nginx:alpine
container_name: cachet-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/letsencrypt:ro
depends_on:
- cachet
networks:
- cachet_public
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /var/cache/nginx:noexec,nosuid,size=100m
- /var/run:noexec,nosuid,size=10m
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
# nginx/nginx.conf
user nginx;
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;
# 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;
# Security headers
server_tokens off;
include /etc/nginx/conf.d/*.conf;
}
# nginx/conf.d/cachet.conf
server {
listen 80;
server_name status.example.com;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name status.example.com;
# SSL certificates
ssl_certificate /etc/letsencrypt/live/status.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/status.example.com/privkey.pem;
# TLS 1.3 only
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 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'; frame-ancestors 'self';" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=cachet_limit:10m rate=10r/s;
limit_req zone=cachet_limit burst=20 nodelay;
# Logging
access_log /var/log/nginx/cachet_access.log;
error_log /var/log/nginx/cachet_error.log;
location / {
proxy_pass http://cachet: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;
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;
}
# Block sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
#!/bin/bash
# /usr/local/bin/cachet-docker-backup.sh
set -e
BACKUP_DIR="/backup/cachet"
DATE=$(date +%Y%m%d_%H%M%S)
COMPOSE_FILE="/opt/cachet/docker-compose.yml"
cd /opt/cachet
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Backup database
docker compose exec -T db mysqldump -u cachet -p"$$(cat secrets/db_password.txt)" cachet | \
gzip > "$BACKUP_DIR/cachet-db-$DATE.sql.gz"
# Backup volumes
docker run --rm \
-v cachet_data:/data:ro \
-v "$BACKUP_DIR:/backup" \
alpine tar -czf "/backup/cachet-data-$DATE.tar.gz" -C /data .
# Backup secrets
tar -czf "$BACKUP_DIR/cachet-secrets-$DATE.tar.gz" -C /opt/cachet/secrets .
chmod 600 "$BACKUP_DIR/cachet-secrets-$DATE.tar.gz"
# Retention: Keep last 30 days
find "$BACKUP_DIR" -name "cachet-*.gz" -mtime +30 -delete
echo "Backup completed: $DATE"
#!/bin/bash
# Restore from backup
BACKUP_FILE="$1"
COMPOSE_FILE="/opt/cachet/docker-compose.yml"
cd /opt/cachet
# Stop services
docker compose down
# Restore database
gunzip -c "$BACKUP_FILE" | docker compose exec -T db mysql -u cachet -p"$$(cat secrets/db_password.txt)" cachet
# Restore volumes
docker run --rm \
-v cachet_data:/data \
-v "$(dirname $BACKUP_FILE):/backup" \
alpine tar -xzf "/backup/cachet-data-*.tar.gz" -C /data
# Start services
docker compose up -d
# Verify
docker compose ps
#!/bin/bash
# Update Cachet to latest v2.x
cd /opt/cachet
# Pull latest image
docker compose pull cachet
# Backup before update
/usr/local/bin/cachet-docker-backup.sh
# Recreate container
docker compose up -d --force-recreate cachet
# Verify
docker compose ps
docker compose logs -f cachet
# Check container status
docker compose ps
# View resource usage
docker stats cachet db redis
# View logs
docker compose logs -f cachet
docker compose logs --tail=100 cachet
# Health check
docker compose exec cachet curl -f http://localhost:8000/api
# Database connection test
docker compose exec db mysql -u cachet -p -e "SELECT 1"
# Redis connection test
docker compose exec redis redis-cli ping
Container wonβt start:
# Check logs
docker compose logs cachet
# Check configuration
docker compose config
# Verify secrets
ls -la secrets/
cat secrets/db_password.txt
Database connection errors:
# Check database health
docker compose ps db
docker compose logs db
# Test connection
docker compose exec cachet php artisan tinker
>>> DB::connection()->getPdo();
Permission issues:
# Fix volume permissions
docker compose exec cachet chown -R www-data:www-data /var/www/html/storage
docker compose exec cachet chmod -R 775 /var/www/html/storage
500 Internal Server Error:
# Clear caches
docker compose exec cachet php artisan config:clear
docker compose exec cachet php artisan cache:clear
docker compose exec cachet php artisan view:clear
docker compose exec cachet php artisan route:clear
Any questions?
Feel free to contact us. Find all contact information on our contact page.