Production Deployment

Complete guide for deploying the Inventory Management API to a production Windows Server environment.

Table of Contents

  1. TOC

Prerequisites

System Requirements

Component Minimum Recommended
OS Windows Server 2019 Windows Server 2022
RAM 4 GB 8 GB+
Storage 20 GB 50 GB+ SSD
CPU 2 cores 4 cores+

Software Requirements

Step 1: Server Preparation

1.1 Install PHP

# Download PHP 8.2+ for Windows
# Extract to C:\php
# Add C:\php to PATH environment variable

# Required PHP extensions:
# - fileinfo
# - zip
# - sqlite3
# - pdo_sqlite
# - pdo_mysql
# - gd
# - exif
# - openssl
# - curl
# - mbstring

1.2 Install Composer

# Download and install Composer from https://getcomposer.org/
# Verify installation
composer --version

1.3 Install Node.js

# Download Node.js 18+ LTS from https://nodejs.org/
# Verify installation
node --version
npm --version

1.4 Install Database

# Install MariaDB 10.5+ or MySQL 8.0+
# Create database and user
CREATE DATABASE inventory_production;
CREATE USER 'inventory_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON inventory_production.* TO 'inventory_user'@'localhost';
FLUSH PRIVILEGES;

Step 2: Application Deployment

2.1 Download Application

# Clone repository to production directory
cd C:\inetpub\wwwroot\
git clone https://github.com/museumwithnofrontiers/inventory-app.git
cd inventory-app

# Or download release package
# Extract to C:\inetpub\wwwroot\inventory-app

2.2 Install Dependencies

# Install PHP dependencies
composer install --optimize-autoloader --no-dev

# Install Node.js dependencies
npm ci --only=production

# Build frontend assets
npm run build

2.3 Environment Configuration

# Copy environment file
cp .env.example .env

# Generate application key
php artisan key:generate

Edit .env file with production settings:

# Application
APP_NAME="Inventory Management"
APP_ENV=production
APP_KEY=base64:YOUR_GENERATED_KEY
APP_DEBUG=false
APP_TIMEZONE=UTC
APP_URL=https://your-domain.com

# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=inventory_production
DB_USERNAME=inventory_user
DB_PASSWORD=secure_password

# Image Storage (these are the defaults; see the Configuration guide)
# Uploads: transient, private
UPLOAD_IMAGES_DISK=local
UPLOAD_IMAGES_DIRECTORY=image_uploads
# Originals: pristine, private, never web-reachable - never "public"
AVAILABLE_IMAGES_DISK=image-originals
AVAILABLE_IMAGES_DIRECTORY=images
# Pictures: the cache of burned renditions that /pub serves
PICTURES_DISK=public
PICTURES_DIRECTORY=pictures

# Cache
CACHE_STORE=file
SESSION_DRIVER=file
QUEUE_CONNECTION=database

# Mail (configure as needed)
MAIL_MAILER=smtp
MAIL_HOST=your-smtp-host
MAIL_PORT=587
MAIL_USERNAME=your-email
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls

# Security
# The REST API answers 404 unless switched on; OAuth signing keys are
# created once by scripts/deploy.sh in shared storage
API_ENABLED=false

# SESSION_DOMAIN: Set to .yourdomain.com (with leading dot) for subdomain cookie sharing
# Leave as null if you don't need subdomain support
# SESSION_DOMAIN=.museumwnf.org
SESSION_DOMAIN=null

SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=strict

# Logging
LOG_CHANNEL=daily
LOG_LEVEL=error

2.4 Database Setup

# Run database migrations
php artisan migrate --force

# Seed initial data
php artisan db:seed --force

# Create admin user (optional)
php artisan make:user-admin

2.5 Optimize Application

# Cache configuration
php artisan config:cache

# Cache routes
php artisan route:cache

# Cache views
php artisan view:cache

# Create symbolic link for storage
php artisan storage:link

Step 3: Web Server Configuration

3.1 Apache Configuration

Use the provided configuration files:

# Copy Apache configuration
cp deployment\apache-windows.conf C:\Apache24\conf\extra\httpd-inventory.conf

# Include in main Apache config
# Add to httpd.conf:
Include conf/extra/httpd-inventory.conf

Update deployment\apache-windows.conf:

<VirtualHost *:80>
    ServerName your-domain.com
    DocumentRoot "C:/inetpub/wwwroot/inventory-app/public"

    <Directory "C:/inetpub/wwwroot/inventory-app/public">
        AllowOverride All
        Require all granted
        Options -Indexes +FollowSymLinks

        RewriteEngine On
        RewriteCond %{REQUEST_FILENAME} !-f
        RewriteCond %{REQUEST_FILENAME} !-d
        RewriteRule ^(.*)$ index.php [QSA,L]
    </Directory>

    ErrorLog "C:/Apache24/logs/inventory-app-error.log"
    CustomLog "C:/Apache24/logs/inventory-app-access.log" combined
</VirtualHost>

3.2 SSL Configuration

<VirtualHost *:443>
    ServerName your-domain.com
    DocumentRoot "C:/inetpub/wwwroot/inventory-app/public"

    SSLEngine on
    SSLCertificateFile "C:/Apache24/conf/ssl/certificate.crt"
    SSLCertificateKeyFile "C:/Apache24/conf/ssl/private.key"

    # ... rest of configuration
</VirtualHost>

Step 4: Security Hardening

4.1 File Permissions

# Set proper permissions
icacls "C:\inetpub\wwwroot\inventory-app" /grant "IIS_IUSRS:F" /T
icacls "C:\inetpub\wwwroot\inventory-app\storage" /grant "IIS_IUSRS:F" /T
icacls "C:\inetpub\wwwroot\inventory-app\bootstrap\cache" /grant "IIS_IUSRS:F" /T

# Restrict access to sensitive files
icacls "C:\inetpub\wwwroot\inventory-app\.env" /deny "Everyone:R"

4.2 Firewall Configuration

# Allow HTTP and HTTPS
netsh advfirewall firewall add rule name="HTTP" dir=in action=allow protocol=TCP localport=80
netsh advfirewall firewall add rule name="HTTPS" dir=in action=allow protocol=TCP localport=443

4.3 SSL Certificate

# Install SSL certificate from trusted CA
# Configure automatic renewal if using Let's Encrypt
# Update Apache configuration with certificate paths

4.4 Image Storage Permissions (Linux)

On the Linux server (scripts/provision.sh), two users share the image disks:

Directory (under /opt/inventory/shared/storage/app) Owner Mode Why
private/image-originals/ and images/ deploy:www-data 2770 The pristine originals. Group read/write, nothing for others, and setgid keeps new files in www-data.
public/pictures/ deploy:www-data 2775 The burned renditions www-data writes and /pub serves.

The app chmods every original it writes to 0660. It can’t fix a directory that the process umask narrowed when creating it, which is why the directories are created up front.

provision.sh creates them on a new server. On a server provisioned before M9, apply them once as deploy, who owns the tree, so no sudo is needed:

cd /opt/inventory/shared/storage/app
mkdir -p private/image-originals/images public/pictures
chgrp -R www-data private/image-originals public/pictures
find private/image-originals -type d -exec chmod 2770 {} +
find private/image-originals -type f -exec chmod 0660 {} +
chmod 2775 public/pictures

If a find line reports Operation not permitted, the file was created by www-data, and only its owner or root can change its mode. Run that line as root.

4.5 Secrets Permissions (Linux)

Two places on the Linux server hold the database password and the app key: the .env and the cached configuration. scripts/deploy.sh closes both to everyone but deploy and the www-data group (PHP-FPM, the queue worker):

Path Owner Mode Why
/opt/inventory/shared/.env deploy:www-data 640 Applied on every deploy, so a .env copied or restored by hand is closed again at the next one.
bootstrap/cache/ of each release deploy:www-data 2770 php artisan optimize writes config.php there with every value of the .env. Closing the directory keeps a later optimize or config:cache run by hand closed too, and setgid keeps the files in www-data.

A backup of the .env made with cp is readable by everyone: chmod 640 it.

The deploy fails, before the new release goes live, if deploy can’t give either one to www-data. That happens when deploy isn’t in the www-data group, which provision.sh sets up, or when the .env isn’t owned by deploy.

Step 5: Automated Deployment

5.1 Using Deployment Script

# Run automated deployment script
.\deployment\deploy-windows.ps1 -Domain "your-domain.com" -AppPath "C:\inetpub\wwwroot\inventory-app"

5.2 Manual Deployment Steps

# 1. Download/update application code
git pull origin main

# 2. Update dependencies
composer install --optimize-autoloader --no-dev
npm ci --only=production
npm run build

# 3. Update database
php artisan migrate --force

# 4. Clear caches
php artisan config:cache
php artisan route:cache
php artisan view:cache

# 5. Restart web server
Restart-Service -Name "Apache2.4"

5.3 Moving an Existing Server to the M9 Image Storage (once)

Since M9, attached images keep their pristine original on the private originals disk. /pub serves them with the copyright burned in, and caches those renditions on the pictures disk. A server that ran an earlier version kept each attached image’s only file in the pictures directory. Moving it over is a one-off, in this order:

  1. Fix .env. Check grep '^AVAILABLE_IMAGES_DISK' /opt/inventory/shared/.env. .env.example pinned public before M9, and deploy.sh copies it only on the first deploy. Set it to image-originals, or delete the line so the default applies. The configuration is cached, so the change takes effect at the next deploy. deploy.sh refuses to deploy while the disk resolves to public.
  2. Create the directories with their ownership and modes (see 4.4).
  3. Deploy. This runs the migrations, which add the copyright columns and the path indexes.
  4. Backfill the originals, as deploy: php artisan images:backfill-originals --force.
    • For every attached image, it copies the file from the pictures directory to the originals disk, then removes the public copy.
    • The first /pub request for each image then burns and caches its rendition.
    • It reports the rows that have no file in either place and carries on. --dry-run shows what it would do.

6.1 Log Files

Monitor these log files:

6.2 Health Checks

# Check application status
curl https://your-domain.com/api/health

# Check database connectivity
php artisan tinker
>>> DB::connection()->getPdo();

6.3 Backup Strategy

# Database backup
mysqldump -u inventory_user -p inventory_production > backup_$(Get-Date -Format "yyyyMMdd_HHmmss").sql

# File backup
robocopy "C:\inetpub\wwwroot\inventory-app" "C:\Backups\inventory-app" /MIR /XD "vendor" "node_modules" ".git"

Step 7: Performance Optimization

7.1 PHP Optimization

; php.ini optimizations
memory_limit = 256M
max_execution_time = 300
max_input_vars = 3000
upload_max_filesize = 64M
post_max_size = 64M

; OPcache
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

7.2 Database Optimization

-- Create indexes for performance
CREATE INDEX idx_projects_created_at ON projects(created_at);
CREATE INDEX idx_contexts_project_id ON contexts(project_id);
CREATE INDEX idx_languages_internal_name ON languages(internal_name);

7.3 Web Server Optimization

# Enable compression
LoadModule deflate_module modules/mod_deflate.so
<Location />
    SetOutputFilter DEFLATE
</Location>

# Enable caching
LoadModule expires_module modules/mod_expires.so
<LocationMatch "\.(css|js|png|jpg|jpeg|gif|ico|svg)$">
    ExpiresActive On
    ExpiresDefault "access plus 1 year"
</LocationMatch>

Troubleshooting

Common Issues

  1. 419 CSRF Token Mismatch / Session Errors

    Symptoms: Users get 419 errors after logging in or when making requests

    Solution:

    # In your production .env file:
    
    # For subdomain support (e.g., api.domain.com + app.domain.com):
    SESSION_DOMAIN=.museumwnf.org  # Note the leading dot
    
    # For single domain deployments:
    SESSION_DOMAIN=null
    
    # Ensure these are also set:
    SESSION_SECURE_COOKIE=true
    SESSION_SAME_SITE=lax
    

    After changing .env:

    php artisan config:clear
    php artisan cache:clear
    
  2. 500 Internal Server Error
    • Check PHP error logs
    • Verify file permissions
    • Ensure .env file is properly configured
  3. Database Connection Failed
    • Verify database credentials
    • Check database server status
    • Confirm firewall settings
  4. Static Assets Not Loading
    • Verify npm run build was executed
    • Check file permissions on public/build/
    • Confirm web server static file handling
  5. SSL Certificate Issues
    • Verify certificate installation
    • Check certificate expiration
    • Confirm intermediate certificates

Performance Issues

  1. Slow Response Times
    • Enable PHP OPcache
    • Optimize database queries
    • Enable web server compression
  2. High Memory Usage
    • Increase PHP memory limit
    • Optimize Laravel caching
    • Monitor for memory leaks

Next Steps