Deployment

This guide covers deploying kiosk-show-replacement in production environments using Docker.

Note

Docker is the only supported deployment method for production.

For local development setup without Docker, see Development.

System Requirements

  • Docker Engine 20.10 or higher

  • Docker Compose 2.0 or higher

  • 512MB+ RAM (1GB+ recommended)

  • 1GB+ disk space

The application supports both x86_64 and ARM64 architectures, including Raspberry Pi 4 and newer.

Docker Deployment

Docker provides the easiest and most reproducible deployment method. The application supports both x86_64 and ARM64 architectures (including Raspberry Pi).

Quick Start

  1. Clone the repository:

    git clone https://github.com/jantman/kiosk-show-replacement.git
    cd kiosk-show-replacement
    
  2. Copy and configure the environment file:

    cp .env.docker.example .env
    
  3. Edit .env and set required values:

    # Generate a secure secret key
    python -c "import secrets; print(secrets.token_hex(32))"
    
    # Edit .env with your values
    SECRET_KEY=<generated-key>
    MYSQL_ROOT_PASSWORD=<strong-password>
    MYSQL_PASSWORD=<strong-password>
    KIOSK_ADMIN_PASSWORD=<strong-password>
    
  4. Create data directories with correct ownership:

    mkdir -p .docker-data/prod/uploads .docker-data/prod/mariadb
    sudo chown -R 1000:1000 .docker-data/prod/uploads .docker-data/prod/mariadb
    chmod 750 .docker-data/prod/mariadb
    

    Important

    The MariaDB container runs as UID:GID 1000:1000 to avoid permission issues with bind-mounted volumes. The data directories must be owned by this UID:GID before starting the containers for the first time. If you need to use a different UID:GID, edit the user: directive in docker-compose.prod.yml.

  5. Start the application:

    docker-compose -f docker-compose.prod.yml --env-file .env up -d
    
  6. Access the application at http://localhost:5000/admin

Verifying Installation

Check the application health:

curl http://localhost:5000/health

Check container status:

docker-compose -f docker-compose.prod.yml ps

View logs:

docker-compose -f docker-compose.prod.yml logs -f app

Development with Docker

For development with Docker (using SQLite):

# Start development environment
docker-compose up

# The application will be available at http://localhost:5000

This uses SQLite and mounts the source code for development visibility.

Production Configuration

Environment Variables

Required variables for production:

Variable

Description

SECRET_KEY

Flask secret key for session security

MYSQL_ROOT_PASSWORD

MariaDB root password

MYSQL_PASSWORD

MariaDB application user password

KIOSK_ADMIN_PASSWORD

Initial admin user password

Optional variables (with defaults):

Variable

Default

APP_PORT

5000

TZ

America/New_York (IANA timezone for calendar display)

MYSQL_DATABASE

kiosk_show

MYSQL_USER

kiosk

MYSQL_HOST

(none; enables MySQL auto-configuration)

MYSQL_PORT

3306

KIOSK_ADMIN_USERNAME

admin

KIOSK_ADMIN_EMAIL

(none)

GUNICORN_WORKERS

2

Database Configuration

The application supports multiple database backends. There are two ways to configure the database connection:

Option 1: Individual MYSQL_* variables (recommended for Docker)

Set the individual MYSQL_* environment variables and the application will automatically construct the DATABASE_URL connection string. This happens both in the Docker entrypoint script and in the Python configuration module.

MYSQL_HOST=mariadb
MYSQL_USER=kiosk
MYSQL_PASSWORD=secretpassword
MYSQL_DATABASE=kiosk_show
MYSQL_PORT=3306          # optional, defaults to 3306

When MYSQL_HOST is set and DATABASE_URL is not, the application constructs: mysql+pymysql://user:password@host:port/database

Option 2: Direct DATABASE_URL

Set the DATABASE_URL environment variable directly to a full connection string. This takes precedence over individual MYSQL_* variables.

SQLite (development/single-node):

DATABASE_URL=sqlite:///kiosk_show.db

MariaDB/MySQL (production):

DATABASE_URL=mysql+pymysql://user:password@host:3306/database

PostgreSQL:

DATABASE_URL=postgresql://user:password@host:5432/database

The production Docker Compose file automatically configures MariaDB.

Important

In production mode, the application will not start if the database resolves to SQLite. You must configure either DATABASE_URL or the MYSQL_* variables. This prevents silent data loss from an ephemeral SQLite database inside a container.

NewRelic Monitoring

The application supports optional NewRelic monitoring for performance insights and error tracking. Both APM (backend) and Browser (frontend) monitoring are automatically enabled when the NEW_RELIC_LICENSE_KEY environment variable is set.

What’s Included:

  • APM Monitoring - Backend performance, database queries, external calls

  • Browser Monitoring - Frontend page load times, JavaScript errors, user sessions

  • Distributed Tracing - Automatic correlation between browser requests and backend transactions

Enabling NewRelic:

  1. Obtain a license key from NewRelic

  2. Add the following to your .env file:

    NEW_RELIC_LICENSE_KEY=your-license-key-here
    NEW_RELIC_APP_NAME=kiosk-show-replacement
    
  3. Restart the application:

    docker-compose -f docker-compose.prod.yml restart app
    

NewRelic Environment Variables:

Required:

Variable

Description

NEW_RELIC_LICENSE_KEY

Your NewRelic license key.

NEW_RELIC_APP_NAME

Application name in dashboard (default: kiosk-show-replacement)

Optional (general):

Variable

Description

NEW_RELIC_ENVIRONMENT

Environment tag (e.g., production, staging)

NEW_RELIC_LOG_LEVEL

Agent log level: critical, error, warning, info, debug (default: info)

NEW_RELIC_API_KEY

NewRelic User API key (if needed)

Feature toggles (all default to true when not set):

Note

The NewRelic Python agent controls these features locally via environment variables (or a config file). In the NewRelic UI, these appear as managed “per server configuration.” To enable them, set the corresponding environment variable to true.

Variable

Description

NEW_RELIC_BROWSER_MONITORING_AUTO_INSTRUMENT

Auto-inject the NewRelic Browser JavaScript agent into HTML pages for Real User Monitoring (page loads, JS errors, sessions)

NEW_RELIC_DISTRIBUTED_TRACING_ENABLED

Correlate requests across services and between browser and backend transactions

NEW_RELIC_TRANSACTION_TRACER_ENABLED

Capture deep information about slow transactions (response time, DB queries)

NEW_RELIC_SLOW_SQL_ENABLED

Capture details from long-running SQL database queries

NEW_RELIC_ERROR_COLLECTOR_ENABLED

Capture uncaught and logged exceptions for viewing in the NewRelic UI

NEW_RELIC_THREAD_PROFILER_ENABLED

Allow thread profiling sessions to be scheduled via the NewRelic UI

Verifying NewRelic is Active:

Check the application logs for NewRelic activation:

docker-compose -f docker-compose.prod.yml logs app | grep -i newrelic

You should see “NewRelic monitoring enabled” if properly configured.

To verify browser monitoring, view page source in your browser and look for the NewRelic browser agent script in the <head> section. The script is automatically injected into all HTML pages when the APM agent is active.

Prometheus Metrics

The application exposes a /metrics endpoint in Prometheus text format for scraping by Prometheus or compatible monitoring systems.

Endpoint: GET /metrics

Authentication: None required (publicly accessible)

Metrics Exposed:

HTTP Metrics:

  • http_requests_total - Total HTTP requests (labels: method, endpoint, status)

  • http_request_duration_seconds - Request duration histogram (label: endpoint)

System Metrics:

  • active_sse_connections - Current number of Server-Sent Events connections

  • database_errors_total - Total database errors

  • storage_errors_total - Total storage errors

Display Metrics (per-display, labels: display_id, display_name):

  • display_info - Info gauge (always 1) with slideshow_id/name labels

  • display_online - Online status (1=online, 0=offline)

  • display_resolution_width_pixels - Display width in pixels

  • display_resolution_height_pixels - Display height in pixels

  • display_rotation_degrees - Rotation (0, 90, 180, 270)

  • display_last_seen_timestamp_seconds - Unix timestamp of last heartbeat

  • display_heartbeat_interval_seconds - Configured heartbeat interval

  • display_heartbeat_age_seconds - Seconds since last heartbeat

  • display_missed_heartbeats - Number of missed heartbeats

  • display_sse_connected - SSE connection status (1=connected, 0=disconnected)

  • display_is_active - Active status (1=active, 0=inactive)

Summary Metrics:

  • displays_total - Total number of displays

  • displays_online_total - Number of online displays

  • displays_active_total - Number of active (not disabled) displays

  • slideshows_total - Total number of slideshows

Example Prometheus Configuration:

scrape_configs:
  - job_name: 'kiosk-show'
    static_configs:
      - targets: ['localhost:5000']
    metrics_path: '/metrics'

Example Queries:

# Percentage of online displays
displays_online_total / displays_total * 100

# Displays with missed heartbeats
display_missed_heartbeats > 0

# Request rate by endpoint
rate(http_requests_total[5m])

Resource Limits

The production Docker Compose file includes resource limits:

Application container:

  • CPU: 2 cores max, 0.5 cores reserved

  • Memory: 1GB max, 256MB reserved

Database container:

  • CPU: 1 core max, 0.25 cores reserved

  • Memory: 512MB max, 128MB reserved

For Raspberry Pi deployments, consider reducing these limits:

# In docker-compose.prod.yml
deploy:
  resources:
    limits:
      cpus: '1'
      memory: 512M

Production Checklist

Before deploying to production:

  1. Security

    • [ ] Generate a strong SECRET_KEY

    • [ ] Set strong passwords for database and admin user

    • [ ] Change the default admin password immediately after first login

    • [ ] Create individual user accounts for each administrator

    • [ ] Deactivate or change credentials for any default/test accounts

    • [ ] Review and restrict network access

    • [ ] Consider adding a reverse proxy (nginx, Caddy) with TLS

  2. Database

    • [ ] Configure database backups

    • [ ] Consider using external managed database for critical deployments

  3. Monitoring

    • [ ] Set up container health monitoring

    • [ ] Configure log aggregation if needed

    • [ ] Set up alerts for container failures

    • [ ] Consider enabling NewRelic APM for performance monitoring

  4. Data Persistence

    • [ ] Verify volume mounts are working

    • [ ] Test backup and restore procedures

    • [ ] Document recovery procedures

Upgrade Procedures

To upgrade to a new version:

  1. Backup your data:

    # Stop the application
    docker-compose -f docker-compose.prod.yml down
    
    # Backup database (MariaDB)
    docker-compose -f docker-compose.prod.yml run --rm db \
      mysqldump -u root -p$MYSQL_ROOT_PASSWORD $MYSQL_DATABASE > backup.sql
    
    # Backup uploads
    tar -czf uploads_backup.tar.gz ./data/uploads
    
  2. Pull new version:

    git pull origin main
    
  3. Rebuild and restart:

    docker-compose -f docker-compose.prod.yml build
    docker-compose -f docker-compose.prod.yml up -d
    
  4. Verify:

    # Check health
    curl http://localhost:5000/health
    
    # Check logs for errors
    docker-compose -f docker-compose.prod.yml logs -f app
    

Database migrations run automatically on container startup.

Troubleshooting

Container won’t start

Check the logs:

docker-compose -f docker-compose.prod.yml logs app

Common issues:

  • Missing environment variables: Ensure all required variables are set

  • Database not ready: The entrypoint waits up to 60 seconds for database

  • Port already in use: Change APP_PORT or stop conflicting service

Database connection errors

Verify database is healthy:

docker-compose -f docker-compose.prod.yml exec db \
  mysql -u root -p$MYSQL_ROOT_PASSWORD -e "SELECT 1"

Check database logs:

docker-compose -f docker-compose.prod.yml logs db

Permission issues with uploads

The container runs as non-root user (UID 1000). Ensure volume permissions:

# Fix permissions on host
sudo chown -R 1000:1000 ./data/uploads

MariaDB healthcheck failing

If the MariaDB container shows repeated “Access denied for user ‘root’@’localhost’ (using password: NO)” errors and never becomes healthy, this is typically a permission issue with the healthcheck credentials file.

Cause: MariaDB creates a .my-healthcheck.cnf file in the data directory during initialization. If the data directory has overly permissive permissions (e.g., 777), MariaDB will ignore the config file as a security measure.

Solution:

  1. Stop the containers:

    docker-compose -f docker-compose.prod.yml down
    
  2. Fix the permissions on the healthcheck file:

    sudo chmod 600 .docker-data/prod/mariadb/.my-healthcheck.cnf
    
  3. Ensure the data directory has correct ownership and permissions:

    sudo chown -R 1000:1000 .docker-data/prod/mariadb
    chmod 750 .docker-data/prod/mariadb
    
  4. Restart the containers:

    docker-compose -f docker-compose.prod.yml up -d
    

Prevention: Always create the data directories with correct ownership before first run (see Quick Start step 4).

Data disappearing after container restart

If your data disappears after restarting the container, the application is likely using an ephemeral SQLite database inside the container instead of your configured MySQL/MariaDB database.

Symptoms:

  • Data lost on container restart

  • MySQL database has no tables

  • Health check /health/db shows database_type: "sqlite"

Cause: The application only reads the DATABASE_URL environment variable for its connection string. If you set individual MYSQL_* variables but not DATABASE_URL, and are not using docker-compose.prod.yml (which constructs it automatically), the app falls back to SQLite.

Solution:

As of version 0.3.0, the application automatically constructs DATABASE_URL from MYSQL_* variables. For older versions, either:

  1. Set DATABASE_URL explicitly, or

  2. Use docker-compose.prod.yml which constructs it for you

You can verify which database is in use via the health endpoint:

curl http://localhost:5000/health/db | jq .database_type

Health check failing

The health check uses /health endpoint:

# Test manually
curl http://localhost:5000/health

# Check container health status
docker inspect --format='{{.State.Health.Status}}' \
  $(docker-compose -f docker-compose.prod.yml ps -q app)

SQLite to MariaDB Migration

If you’re migrating from a SQLite development database to MariaDB production:

  1. Export data from SQLite:

    # Using Flask shell
    flask shell
    >>> from kiosk_show_replacement.models import *
    >>> # Export your data using SQLAlchemy queries
    
  2. Start fresh with MariaDB:

    The application will create tables and a default admin user automatically on first start.

  3. Re-import data (if needed):

    Use the admin interface or API to recreate slideshows and content.

For large datasets, consider using database migration tools like pgloader or custom scripts.