8. Deployment
This section covers the processes and considerations for deploying the Kingdom Management Portal to production environments.
8.1 Production Setup
Server Requirements
For a production deployment, the following server configuration is recommended:
- Web Server: Apache 2.4+ or Nginx 1.18+
- PHP: PHP 8.3+ with required extensions (see System Requirements)
- Database: MySQL 5.7+ or MariaDB 10.2+
- Memory: Minimum 2GB RAM (4GB+ recommended)
- Storage: 10GB+ disk space (more if storing many attachments)
- SSL Certificate: Required for secure user authentication
Directory Structure
The recommended production directory structure separates public and non-public files:
/var/www/kmp/ # Root application directory (non-public)
├── app/ # CakePHP application
│ ├── config/ # Configuration files
│ ├── logs/ # Log files
│ ├── src/ # Application source code
│ ├── templates/ # View templates
│ ├── assets/ # Source asset files (CSS/JS)
│ ├── ... # Other application directories
│ └── webroot/ # Public web files (document root)
├── bin/ # CLI commands
├── vendor/ # Composer dependencies
└── tmp/ # Temporary files (cache, sessions, etc.)
/etc/apache2/sites-available/ # Apache configuration
/etc/nginx/sites-available/ # Nginx configuration
/etc/php/8.3/ # PHP configuration
/etc/mysql/ # MySQL configuration
Web Server Configuration
Apache Configuration
<VirtualHost *:80>
ServerName kmp.example.com
DocumentRoot /var/www/kmp/app/webroot
<Directory /var/www/kmp/app/webroot>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/kmp-error.log
CustomLog ${APACHE_LOG_DIR}/kmp-access.log combined
# Redirect to HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>
<VirtualHost *:443>
ServerName kmp.example.com
DocumentRoot /var/www/kmp/app/webroot
<Directory /var/www/kmp/app/webroot>
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/kmp-error.log
CustomLog ${APACHE_LOG_DIR}/kmp-access.log combined
# SSL Configuration
SSLEngine on
SSLCertificateFile /etc/ssl/certs/kmp.example.com.crt
SSLCertificateKeyFile /etc/ssl/private/kmp.example.com.key
SSLCertificateChainFile /etc/ssl/certs/kmp.example.com.chain.crt
# HTTP Strict Transport Security
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>
Nginx Configuration
server {
listen 80;
server_name kmp.example.com;
# Redirect to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name kmp.example.com;
root /var/www/kmp/app/webroot;
index index.php;
# SSL Configuration
ssl_certificate /etc/ssl/certs/kmp.example.com.crt;
ssl_certificate_key /etc/ssl/private/kmp.example.com.key;
ssl_trusted_certificate /etc/ssl/certs/kmp.example.com.chain.crt;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Logs
access_log /var/log/nginx/kmp-access.log;
error_log /var/log/nginx/kmp-error.log;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
}
PHP Configuration
Optimize PHP for production with these php.ini settings:
; Maximum memory allocation
memory_limit = 256M
; Maximum upload file size
upload_max_filesize = 20M
post_max_size = 20M
; Error reporting (disable display, enable logging)
display_errors = Off
log_errors = On
error_log = /var/www/kmp/app/logs/php_errors.log
; Opcode caching
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 4000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1
Environment Configuration
Create an app_local.php file in the config directory with production-specific settings:
return [
'debug' => false,
'Security' => [
'salt' => 'your-long-random-security-salt-string',
],
'Datasources' => [
'default' => [
'host' => 'localhost',
'username' => 'kmp_db_user',
'password' => 'secure-password',
'database' => 'kmp_production',
'log' => false,
],
],
'EmailTransport' => [
'default' => [
'host' => 'smtp.example.com',
'port' => 587,
'username' => 'email@example.com',
'password' => 'email-password',
'tls' => true,
],
],
// Additional production settings...
];
8.2 Migrations
Database changes in KMP are managed through CakePHP’s migrations system, which provides version control for your database schema.
Creating Migrations
When making database changes, create a migration file:
# Create a new migration
cd /var/www/kmp
bin/cake bake migration CreateUsers
# Create a migration for an existing table
bin/cake bake migration AlterUsers
# Create a migration for a plugin
bin/cake bake migration -p PluginName CreatePluginTable
Running Migrations
To apply migrations in production:
# Apply all pending migrations
bin/cake migrations migrate
# Apply migrations for a plugin
bin/cake migrations migrate -p PluginName
# Apply migrations up to a specific version
bin/cake migrations migrate --target=20250401000000
# Rollback the last migration
bin/cake migrations rollback
Migration Deployment Process
Follow this process when deploying database changes:
- Backup: Always back up the production database before applying migrations
mysqldump -u user -p kmp_production > kmp_backup_YYYYMMDD.sql - Maintenance Mode: Enable maintenance mode during database updates
# Set maintenance mode app setting StaticHelpers::setAppSetting('KMP.MaintenanceMode', 'yes'); - Apply Migrations: Run the migrations
bin/cake migrations migrate bin/cake migrations migrate -p PluginName1 bin/cake migrations migrate -p PluginName2 # etc. - Verify: Confirm that migrations were applied successfully
bin/cake migrations status - Disable Maintenance Mode: Return the application to normal operation
StaticHelpers::setAppSetting('KMP.MaintenanceMode', 'no');
Migration Order
Plugins in KMP have a defined migration order, as specified in their plugin registration. This ensures that dependencies between plugins are respected during migration. The order is defined in config/plugins.php:
'Activities' => [
'migrationOrder' => 1,
],
'Officers' => [
'migrationOrder' => 2,
],
'Awards' => [
'migrationOrder' => 3,
],
8.3 Updates
Keeping your KMP installation up-to-date involves updating both the application code and the database schema.
Update Process
Follow these steps to update KMP in a production environment:
- Backup: Create backups of both code and database
# Backup code cp -r /var/www/kmp /var/www/kmp-backup-YYYYMMDD # Backup database mysqldump -u user -p kmp_production > kmp_backup_YYYYMMDD.sql - Maintenance Mode: Enable maintenance mode
StaticHelpers::setAppSetting('KMP.MaintenanceMode', 'yes'); - Get Updates: Pull the latest code from the repository
cd /var/www/kmp git fetch origin git checkout v1.x.x # Replace with the target version - Update Dependencies: Update Composer and NPM dependencies
composer install --no-dev --optimize-autoloader npm ci npm run prod - Clear Cache: Remove cached files
bin/cake cache clear_all - Apply Migrations: Update the database schema
bin/cake migrations migrate # Apply plugin migrations as needed - Update Settings: Apply any new app settings
bin/cake app_settings initialize - Test: Verify the application works correctly
bin/cake server -p 8080 # Test on a non-production port - Disable Maintenance Mode: Return to normal operation
StaticHelpers::setAppSetting('KMP.MaintenanceMode', 'no');
Rollback Procedure
If issues are encountered during the update, follow these steps to roll back:
- Restore Code: Replace the code directory with the backup
rm -rf /var/www/kmp cp -r /var/www/kmp-backup-YYYYMMDD /var/www/kmp - Restore Database: Restore the database backup
mysql -u user -p kmp_production < kmp_backup_YYYYMMDD.sql - Clear Cache: Clear any cached files
bin/cake cache clear_all - Disable Maintenance Mode: Return to normal operation
StaticHelpers::setAppSetting('KMP.MaintenanceMode', 'no');
Version Management
KMP uses semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR: Incompatible API changes requiring significant migration effort
- MINOR: Backward-compatible new features
- PATCH: Backward-compatible bug fixes
The current application version is stored in the app_settings table:
StaticHelpers::getAppSetting('App.version');
8.4 Cloud Object Storage Configuration
The DocumentService uses Flysystem to abstract storage operations and supports multiple storage backends. By default, files are stored on the local filesystem, but production deployments can use Azure Blob Storage or S3-compatible object storage.
Storage Adapters
KMP supports three storage adapters:
- Local Filesystem (default) - Files stored on the server’s local filesystem
- Azure Blob Storage - Files stored in Azure cloud storage
- S3-Compatible Storage - Files stored in AWS S3 or compatible providers
Local Filesystem Configuration (Default)
Add this configuration to config/app_local.php:
'Documents' => [
'storage' => [
'adapter' => 'local',
'local' => [
'path' => ROOT . DS . 'images' . DS . 'uploaded',
],
],
],
Azure Blob Storage Configuration
For production deployments, configure Azure Blob Storage in config/app_local.php:
'Documents' => [
'storage' => [
'adapter' => 'azure',
'azure' => [
'connectionString' => env('AZURE_STORAGE_CONNECTION_STRING'),
'container' => 'documents',
'prefix' => '', // Optional: prefix all paths (e.g., 'kmp/documents/')
],
],
],
Amazon S3 Configuration
For production deployments, configure S3-compatible storage in config/app_local.php:
Prerequisite: Install the S3 Flysystem adapter in your app container/image:
composer require league/flysystem-aws-s3-v3
'Documents' => [
'storage' => [
'adapter' => 's3',
's3' => [
'bucket' => env('AWS_S3_BUCKET'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'sessionToken' => env('AWS_SESSION_TOKEN'),
'prefix' => env('AWS_S3_PREFIX', ''),
'endpoint' => env('AWS_S3_ENDPOINT'),
'usePathStyleEndpoint' => filter_var(
env('AWS_S3_USE_PATH_STYLE_ENDPOINT', false),
FILTER_VALIDATE_BOOLEAN,
),
],
],
],
Setting up Azure Blob Storage
1. Create Azure Storage Account
- Log into Azure Portal
- Navigate to Storage Accounts
- Click + Create
- Fill in the required information:
- Subscription: Your Azure subscription
- Resource Group: Create new or use existing
- Storage account name: Must be globally unique (lowercase, numbers only)
- Region: Choose closest to your application
- Performance: Standard (or Premium if needed)
- Redundancy: Choose based on your needs (LRS for cost-effective, GRS for geo-redundancy)
- Click Review + Create and then Create
2. Create Blob Container
- Open your newly created Storage Account
- In the left menu, under Data storage, click Containers
- Click + Container
- Enter a name (e.g.,
documents) - Set Public access level to Private (no anonymous access)
- Click Create
3. Get Connection String
- In your Storage Account, go to Security + networking > Access keys
- Click Show next to one of the connection strings
- Copy the entire connection string
4. Configure Application
Add the connection string to your environment variables:
Using .env file (Development)
- Copy
config/.env.exampletoconfig/.env - Add the connection string:
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net"
Using System Environment Variables (Production - Recommended)
Set the environment variable in your hosting environment:
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net"
For systemd services, add to your service file:
[Service]
Environment="AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=..."
Direct Configuration (Not Recommended)
You can set it directly in config/app_local.php, but this is less secure:
'Documents' => [
'storage' => [
'adapter' => 'azure',
'azure' => [
'connectionString' => 'DefaultEndpointsProtocol=https;AccountName=...',
'container' => 'documents',
],
],
],
5. Update Configuration
Edit config/app_local.php and add/update the Documents configuration as shown above.
Setting up Amazon S3
1. Create a Bucket
- Open the AWS console and navigate to S3
- Create a bucket (for example,
kmp-documents-prod) - Enable encryption at rest (SSE-S3 or SSE-KMS)
- Keep block public access enabled unless you have a specific requirement
2. Configure Access
Use one of these approaches:
- Recommended (AWS runtime): assign an IAM role with
s3:GetObject,s3:PutObject,s3:DeleteObjecton the bucket - Alternative: set
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYenvironment variables
3. Set Environment Variables
export AWS_S3_BUCKET="kmp-documents-prod"
export AWS_DEFAULT_REGION="us-east-1"
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN=""
export AWS_S3_PREFIX=""
export AWS_S3_ENDPOINT=""
export AWS_S3_USE_PATH_STYLE_ENDPOINT="false"
For S3-compatible providers (MinIO, DigitalOcean Spaces, etc.), set AWS_S3_ENDPOINT and enable AWS_S3_USE_PATH_STYLE_ENDPOINT=true when required by the provider.
Testing the Configuration
After configuring cloud storage (Azure or S3), test it by:
- Uploading a waiver document through the application
- Verifying the file appears in your configured storage container/bucket
- Downloading the document to ensure retrieval works
You can verify objects in the provider console:
- Azure: Storage Account → Containers → your container
- S3: S3 → Buckets → your bucket
Troubleshooting
Connection Errors
Issue: “Failed to initialize Azure Blob Storage”
Solutions:
- Verify your connection string is correct
- Ensure the storage account exists and is accessible
- Check that the account key hasn’t been regenerated
- Verify network connectivity to Azure
- Check firewall rules on the storage account allow your application’s IP
S3 Initialization Errors
Issue: “Failed to initialize S3 storage”
Solutions:
- Verify
AWS_S3_BUCKETandAWS_DEFAULT_REGIONare set correctly - If using access keys, verify
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY - If using IAM roles, verify the runtime has S3 permissions for get/put/delete
- For S3-compatible providers, verify
AWS_S3_ENDPOINTand path-style setting - Confirm bucket policy and KMS permissions allow your runtime principal
Container Not Found
Issue: “Container does not exist”
Solutions:
- Ensure the container name in configuration matches the actual container name (case-sensitive)
- Verify the container exists in the Storage Account
- Check that the connection string has access to the container
Permission Errors
Issue: “Authorization failed”
Solutions:
- Ensure you’re using a valid access key
- Check that the storage account hasn’t been deleted or restricted
- Verify firewall rules on the storage account allow your application’s IP
- Confirm the connection string includes the correct account key
Performance Considerations
For production deployments:
- Choose a region close to your application for lower latency
- Consider using Azure CDN for frequently accessed documents
- Monitor storage costs and optimize as needed
- Consider using lifecycle management to archive old documents
- Use GRS (Geo-Redundant Storage) for critical data requiring disaster recovery
- Enable Azure Storage encryption at rest (enabled by default)
Migration from Local to Azure
To migrate existing documents from local filesystem to Azure:
- Configure Azure Blob Storage as documented above
- Keep local configuration temporarily in
app_local.php - Upload new documents - They will go to Azure based on the active adapter setting
-
Manually copy existing files to Azure using Azure Storage Explorer, Azure CLI, or a migration script:
# Using Azure CLI az storage blob upload-batch \ --account-name <storage-account-name> \ --destination documents \ --source /var/www/kmp/images/uploaded -
Update document records in the database to reflect new
storage_adapter:UPDATE documents SET storage_adapter = 'azure' WHERE storage_adapter = 'local' OR storage_adapter IS NULL; - Verify migration by testing document downloads
- Remove local configuration once migration is complete and verified
Security Best Practices
- Never commit connection strings to version control
- Use environment variables for sensitive configuration
- Rotate access keys regularly (Azure supports two keys for zero-downtime rotation)
- Use private containers (no anonymous access)
- Enable Azure Storage encryption at rest (enabled by default)
- Monitor access logs for unusual activity
- Consider using Azure Managed Identity in production instead of connection strings
- Enable soft delete for blob containers to protect against accidental deletion
- Configure network rules to restrict access to specific IP ranges
- Use HTTPS only (enforced by default in the configuration)
Monitoring and Logging
The DocumentService logs important events:
- Successful initialization of storage adapters
- Fallback to local storage when a cloud adapter fails
- File upload/download operations
- Storage errors and exceptions
Monitor these logs in production:
# View document service logs
tail -f /var/www/kmp/app/logs/error.log | grep -i "document\|storage\|azure"
Azure Storage also provides built-in metrics and logging:
- Storage Analytics: Monitor request metrics, capacity, and availability
- Diagnostic Logs: Track storage operations for auditing and troubleshooting
- Azure Monitor: Set up alerts for storage issues
````