Skip to content

Production Deployment Guide

This guide covers deploying Pathary in production environments with HTTPS, reverse proxies, monitoring, and security best practices.

Prerequisites

Before deploying to production:

  1. Complete basic installation using the Quickstart Guide or Docker Installation
  2. Test functionality locally
  3. Prepare production domain and SSL certificates

Production Checklist

Before going live, ensure you have:

  • Domain configured with DNS pointing to your server
  • SSL/TLS certificates obtained (Let's Encrypt, commercial CA, or self-signed for testing)
  • Reverse proxy configured (Nginx, Traefik, or Caddy)
  • DATABASE_MODE=mysql for production (SQLite OK for small deployments)
  • Strong passwords for database and admin accounts
  • TMDB_API_KEY configured
  • APPLICATION_URL set to your production domain (with https://)
  • Backup strategy in place
  • Monitoring configured (optional but recommended)

Production Docker Compose

Basic Production Setup

version: '3.8'

services:
  pathary:
    image: ghcr.io/benjaminmue/pathary:latest
    container_name: pathary
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"  # Only expose to localhost (reverse proxy handles external)
    environment:
      # Required
      TMDB_API_KEY: ${TMDB_API_KEY}
      APPLICATION_URL: "https://<your_domain>"

      # Database
      DATABASE_MODE: "mysql"
      DATABASE_MYSQL_HOST: "mysql"
      DATABASE_MYSQL_NAME: "<database_name>"
      DATABASE_MYSQL_USER: "<database_user>"
      DATABASE_MYSQL_PASSWORD: ${DATABASE_MYSQL_PASSWORD}

      # Production logging
      LOG_LEVEL: "warning"
      LOG_ENABLE_FILE_LOGGING: "1"

      # Optional performance
      TMDB_ENABLE_IMAGE_CACHING: "1"
    volumes:
      - pathary-storage:/app/storage
    depends_on:
      mysql:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  mysql:
    image: mysql:8.0
    container_name: pathary-mysql
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: "<database_name>"
      MYSQL_USER: "<database_user>"
      MYSQL_PASSWORD: ${DATABASE_MYSQL_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pathary-storage:
  mysql-data:

Environment file (.env - keep this secure):

TMDB_API_KEY=<tmdb_api_key>
DATABASE_MYSQL_PASSWORD=<database_password>
MYSQL_ROOT_PASSWORD=<mysql_root_password>

Security

  • Never commit .env files to version control
  • Use strong random passwords (min 32 characters)
  • Restrict file permissions: chmod 600 .env

For enhanced security, use Docker secrets:

version: '3.8'

services:
  pathary:
    image: ghcr.io/benjaminmue/pathary:latest
    container_name: pathary
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    environment:
      TMDB_API_KEY_FILE: /run/secrets/tmdb_api_key
      APPLICATION_URL: "https://<your_domain>"
      DATABASE_MODE: "mysql"
      DATABASE_MYSQL_HOST: "mysql"
      DATABASE_MYSQL_NAME: "<database_name>"
      DATABASE_MYSQL_USER: "<database_user>"
      DATABASE_MYSQL_PASSWORD_FILE: /run/secrets/database_password
      LOG_LEVEL: "warning"
    volumes:
      - pathary-storage:/app/storage
    secrets:
      - tmdb_api_key
      - database_password
    depends_on:
      mysql:
        condition: service_healthy

  mysql:
    image: mysql:8.0
    container_name: pathary-mysql
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: "<database_name>"
      MYSQL_USER: "<database_user>"
      MYSQL_PASSWORD_FILE: /run/secrets/database_password
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password
    volumes:
      - mysql-data:/var/lib/mysql
    secrets:
      - database_password
      - mysql_root_password

secrets:
  tmdb_api_key:
    file: ./secrets/tmdb_api_key.txt
  database_password:
    file: ./secrets/database_password.txt
  mysql_root_password:
    file: ./secrets/mysql_root_password.txt

volumes:
  pathary-storage:
  mysql-data:

Reverse Proxy Configuration

Pathary should run behind a reverse proxy that handles HTTPS/SSL termination.

Required Proxy Headers

Your reverse proxy must forward these headers for Pathary to work correctly:

Header Purpose Example
Host Original hostname <your_domain>
X-Real-IP Client IP address 203.0.113.1
X-Forwarded-For Proxy chain IPs 203.0.113.1, 198.51.100.1
X-Forwarded-Proto Original protocol https
X-Forwarded-Host Original host <your_domain>

HTTPS Detection

Pathary detects HTTPS via X-Forwarded-Proto: https header. Without this header, redirects and cookie security flags will fail.

Nginx

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name <your_domain>;

    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/<your_domain>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<your_domain>/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Proxy Configuration
    location / {
        proxy_pass http://127.0.0.1:8080;

        # Required proxy headers
        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;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Buffering
        proxy_buffering off;
        proxy_request_buffering off;
    }

    # Optional: Increase max body size for image uploads
    client_max_body_size 10M;
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name <your_domain>;

    return 301 https://$server_name$request_uri;
}

Traefik (Docker Labels)

services:
  pathary:
    image: ghcr.io/benjaminmue/pathary:latest
    restart: unless-stopped
    environment:
      APPLICATION_URL: "https://<your_domain>"
      # ... other environment variables
    networks:
      - traefik-public
    labels:
      - "traefik.enable=true"

      # HTTP router (redirects to HTTPS)
      - "traefik.http.routers.pathary-http.rule=Host(`<your_domain>`)"
      - "traefik.http.routers.pathary-http.entrypoints=web"
      - "traefik.http.routers.pathary-http.middlewares=pathary-https-redirect"

      # HTTPS router
      - "traefik.http.routers.pathary.rule=Host(`<your_domain>`)"
      - "traefik.http.routers.pathary.entrypoints=websecure"
      - "traefik.http.routers.pathary.tls=true"
      - "traefik.http.routers.pathary.tls.certresolver=letsencrypt"

      # Service
      - "traefik.http.services.pathary.loadbalancer.server.port=80"

      # Middlewares
      - "traefik.http.middlewares.pathary-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.pathary-https-redirect.redirectscheme.permanent=true"

networks:
  traefik-public:
    external: true

Caddy

Caddy automatically handles HTTPS with Let's Encrypt:

<your_domain> {
    reverse_proxy pathary:80
}

That's it! Caddy handles: - Automatic HTTPS with Let's Encrypt - HTTP to HTTPS redirect - Required proxy headers - Certificate renewal

SSL/TLS Certificates

With Certbot (Nginx/Apache):

# Install certbot
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d <your_domain>

# Auto-renewal is configured automatically

With Traefik (automatic):

# Traefik handles Let's Encrypt automatically when using tls.certresolver
labels:
  - "traefik.http.routers.pathary.tls.certresolver=letsencrypt"

With Caddy (automatic):

# Caddy obtains and renews certificates automatically
<your_domain> {
    reverse_proxy pathary:80
}

Commercial CA

Upload your certificate files and configure your reverse proxy to use them (see Nginx example above).

Self-Signed (Testing Only)

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout /etc/ssl/private/pathary-selfsigned.key \
  -out /etc/ssl/certs/pathary-selfsigned.crt \
  -subj "/CN=<your_domain>"

Self-Signed Certificates

Only use self-signed certificates for testing. Browsers will show security warnings.

Monitoring and Health Checks

Built-in Health Endpoint

Pathary exposes a health check endpoint at /health:

curl -f https://<your_domain>/health

Uptime Monitoring

Use external monitoring services: - Uptime Robot (free tier available) - Pingdom - StatusCake - UptimeRobot

Configure them to check https://<your_domain>/health every 5 minutes.

Docker Health Checks

Add to your docker-compose.yml:

services:
  pathary:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

Check health status:

docker ps  # Shows (healthy) or (unhealthy)
docker inspect pathary | grep -A 10 Health

Log Monitoring

Monitor application logs for errors:

# View recent logs
docker compose logs --tail=100 -f pathary

# Monitor error patterns
docker compose logs pathary | grep -i error

# Inside container
docker exec pathary tail -f /app/storage/logs/pathary.log

Backup Strategy

Database Backups (MySQL)

Automated daily backup:

#!/bin/bash
# backup-pathary-db.sh

BACKUP_DIR="<backup_directory>"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"

docker exec pathary-mysql mysqldump \
  -u <database_user> \
  -p"$DATABASE_PASSWORD" \
  <database_name> \
  | gzip > "$BACKUP_DIR/pathary-db-$DATE.sql.gz"

# Keep only last 30 days
find "$BACKUP_DIR" -name "pathary-db-*.sql.gz" -mtime +30 -delete

Schedule with cron:

# Run daily at 2 AM
0 2 * * * <path_to_backup_script>/backup-pathary-db.sh

Storage Volume Backup

Backup the entire storage volume (includes SQLite DB if used, logs, images):

#!/bin/bash
# backup-pathary-storage.sh

BACKUP_DIR="<backup_directory>"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"

docker run --rm \
  -v pathary-storage:/source:ro \
  -v "$BACKUP_DIR":/backup \
  alpine \
  tar czf "/backup/pathary-storage-$DATE.tar.gz" -C /source .

# Keep only last 30 days
find "$BACKUP_DIR" -name "pathary-storage-*.tar.gz" -mtime +30 -delete

Restore from Backup

Restore MySQL database:

# Stop Pathary
docker compose stop pathary

# Restore database
gunzip < pathary-db-20260113.sql.gz | \
  docker exec -i pathary-mysql mysql -u <database_user> -p"$DATABASE_PASSWORD" <database_name>

# Start Pathary
docker compose start pathary

Restore storage volume:

# Stop Pathary
docker compose stop pathary

# Restore storage
docker run --rm \
  -v pathary-storage:/target \
  -v /backups/pathary:/backup \
  alpine \
  tar xzf /backup/pathary-storage-20260113.tar.gz -C /target

# Start Pathary
docker compose start pathary

Updates and Maintenance

Update Pathary

# Pull latest image
docker compose pull pathary

# Recreate container with new image
docker compose up -d pathary

# Migrations run automatically on startup

Downtime During Updates

Expect 30-60 seconds of downtime during updates while the container restarts and runs migrations.

Check Current Version

Visit Settings → About in the Pathary web interface to see the current version.

Rollback to Previous Version

# Use a specific version tag
docker compose pull ghcr.io/benjaminmue/pathary:v1.0.0

# Update compose file to pin version
# image: ghcr.io/benjaminmue/pathary:v1.0.0

# Recreate container
docker compose up -d pathary

Database Migrations

Migrations run automatically by default. To run manually:

# Check migration status
docker compose exec pathary php bin/console.php database:migration:status

# Run pending migrations
docker compose exec pathary php bin/console.php database:migration:migrate

# Rollback last migration (if needed)
docker compose exec pathary php bin/console.php database:migration:rollback

Security Hardening

Environment Variables

  • ✅ Use strong random passwords (min 32 characters)
  • ✅ Use Docker secrets for sensitive data
  • ✅ Never commit .env files to git
  • ✅ Restrict .env file permissions: chmod 600 .env

Network Security

  • ✅ Only expose Pathary to 127.0.0.1 (reverse proxy handles external traffic)
  • ✅ Use firewall rules to restrict access
  • ✅ Keep Docker and system packages updated

Application Security

  • ✅ Enable 2FA for all admin accounts
  • ✅ Use strong passwords (enforced by password policy)
  • ✅ Keep Pathary updated to latest version
  • ✅ Monitor logs for suspicious activity

Reverse Proxy Security Headers

Add to Nginx configuration:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Troubleshooting Production Issues

502 Bad Gateway

Causes: - Pathary container is not running - Reverse proxy can't reach Pathary - Wrong port configuration

Solutions:

# Check if container is running
docker ps | grep pathary

# Check container logs
docker compose logs pathary

# Verify network connectivity
docker exec nginx ping pathary

# Check port binding
docker port pathary

HTTPS Not Working

Causes: - X-Forwarded-Proto header not set - APPLICATION_URL doesn't include https:// - SSL certificate issues

Solutions:

# Verify APPLICATION_URL
docker exec pathary env | grep APPLICATION_URL

# Check proxy headers (from inside Pathary container)
docker exec pathary curl -I http://localhost/

# Test SSL certificate
openssl s_client -connect <your_domain>:443

Session/Login Issues

Causes: - APPLICATION_URL mismatch - Cookie domain/secure flag issues - Missing proxy headers

Solutions: - Ensure APPLICATION_URL matches your actual domain - Verify X-Forwarded-Proto: https is being forwarded - Clear browser cookies and try again

Database Connection Errors

# Check MySQL is running
docker compose ps mysql

# Test connection from Pathary
docker compose exec pathary php bin/console.php database:migration:status

# Check MySQL logs
docker compose logs mysql