Ubuntu VDS installation
This guide covers a clean KaevCMS deployment on a dedicated VDS running Ubuntu Server 24.04 LTS, nginx, PHP 8.3-FPM, and MySQL.
Ubuntu 24.04 is the KaevCMS baseline because its standard repositories provide PHP 8.3, matching the minimum and pinned Composer platform. Ubuntu 26.04 LTS ships PHP 8.5; it may be tested separately, but it is not the primary validated platform until the complete quality gate passes on it.
Resulting layout
/var/www/kaevcms/
├── app/
├── bootstrap/
├── config/
├── public/ ← the only nginx Document Root
├── storage/
├── vendor/
├── .env
└── artisanNever point nginx at /var/www/kaevcms. Only /var/www/kaevcms/public may be public.
Before you begin
Prepare:
- a clean Ubuntu Server 24.04 LTS VDS;
- a domain A record pointing to the VDS IPv4 address — optional for a local or test installation;
- SSH access with
sudo; - the complete KaevCMS release ZIP;
- separate passwords for MySQL and the KaevCMS owner.
Replace these examples:
example.com with your domain, when available
192.168.50.111 with the actual server IP
admin@example.com with your Let's Encrypt email
KaevCmsDbPasswordHere with a long unique MySQL passwordAn IP address is sufficient for the first KaevCMS test. A domain and certificate may be configured later.
1. Update the operating system
sudo apt update
sudo apt full-upgrade -y
sudo rebootReconnect through SSH after the reboot.
2. Configure the firewall
When SSH uses the standard port 22:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verboseWhen SSH uses a custom port, allow that port before enabling UFW.
Do not expose the local KaevCMS MySQL port 3306 to the Internet.
3. Install nginx, MySQL, PHP, and utilities
sudo apt install -y \
nginx \
mysql-server \
unzip \
curl \
ca-certificates \
composer \
php8.3-cli \
php8.3-fpm \
php8.3-mysql \
php8.3-mbstring \
php8.3-xml \
php8.3-curl \
php8.3-zip \
php8.3-intl \
php8.3-gdVerify versions and services:
php -v
composer --version
sudo systemctl status nginx --no-pager
sudo systemctl status php8.3-fpm --no-pager
sudo systemctl status mysql --no-pagerKaevCMS requires PHP 8.3 or newer. For this baseline, php -v should report an 8.3.x version.
4. Create the KaevCMS database
Open MySQL as the system administrator:
sudo mysqlRun:
CREATE DATABASE kaevcms_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER 'kaevcms_user'@'localhost'
IDENTIFIED BY 'KaevCmsDbPasswordHere';
GRANT ALL PRIVILEGES ON kaevcms_db.*
TO 'kaevcms_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;Verify the dedicated account:
mysql -u kaevcms_user -p -h 127.0.0.1 kaevcms_dbExit with EXIT; after a successful connection.
Use these Web Installer values:
Host: 127.0.0.1
Port: 3306
Database: kaevcms_db
Username: kaevcms_user
Password: the password created above5. Upload and extract KaevCMS
Transfer the complete release ZIP through SFTP/SCP, for example to /tmp/KaevCMS-full.zip.
sudo mkdir -p /var/www/kaevcms
sudo unzip /tmp/KaevCMS-full.zip -d /var/www/kaevcms
sudo chown -R "$USER":www-data /var/www/kaevcms
cd /var/www/kaevcmsVerify that the archive did not create an extra directory level:
test -f /var/www/kaevcms/artisan && echo "KaevCMS root OK"If artisan is under a nested path such as /var/www/kaevcms/KaevCMS-0.x.x/artisan, move that directory's contents one level up.
Delete the uploaded archive:
sudo rm -f /tmp/KaevCMS-full.zip6. Install production dependencies
Run Composer as the normal SSH user that owns the project, not as root:
cd /var/www/kaevcms
composer install --no-dev --optimize-autoloader --no-interaction
composer check-platform-reqs --no-devNode.js and npm are not required on the VDS because built frontend assets are included in the complete release.
7. Apply permissions
Start with safe base permissions:
sudo find /var/www/kaevcms -type d -exec chmod 755 {} \;
sudo find /var/www/kaevcms -type f -exec chmod 644 {} \;Allow nginx/PHP-FPM to write only to runtime directories:
sudo chgrp -R www-data \
/var/www/kaevcms/storage \
/var/www/kaevcms/bootstrap/cache \
/var/www/kaevcms/public/uploads
sudo chmod -R g+rwX \
/var/www/kaevcms/storage \
/var/www/kaevcms/bootstrap/cache \
/var/www/kaevcms/public/uploads
sudo find /var/www/kaevcms/storage \
/var/www/kaevcms/bootstrap/cache \
/var/www/kaevcms/public/uploads \
-type d -exec chmod 2775 {} \;Verify that the PHP-FPM account can create nested cache directories and files:
cd /var/www/kaevcms
sudo -u www-data php artisan kaevcms:runtime-directories --probeKaevCMS keeps the general application cache on files by default, but stores login and other rate-limit counters in the CMS database:
CACHE_STORE=file
CACHE_LIMITER=databaseThe Web Installer must create .env in the project root. Temporarily allow group write access:
sudo chmod 775 /var/www/kaevcmsNever recursively assign 0777 to the project.
8. Configure PHP limits
Open the PHP-FPM configuration:
sudo nano /etc/php/8.3/fpm/php.iniRecommended starting values:
memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 70M
max_execution_time = 120
max_input_time = 120Apply the changes:
sudo systemctl restart php8.3-fpmVDS updates are applied through the CLI as the project-file owner, so a large update ZIP does not need to pass through PHP uploads.
9. Configure nginx
Create the site configuration:
sudo nano /etc/nginx/sites-available/kaevcmsUse:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/kaevcms/public;
index index.php;
charset utf-8;
client_max_body_size 512m;
access_log /var/log/nginx/kaevcms-access.log;
error_log /var/log/nginx/kaevcms-error.log;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /index.php {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location = /install/index.php {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ^~ /uploads/ {
try_files $uri =404;
}
location ~ \.php$ {
return 404;
}
location ~ /\.(?!well-known).* {
deny all;
}
}Enable it:
sudo ln -s /etc/nginx/sites-available/kaevcms /etc/nginx/sites-enabled/kaevcms
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxVerify the Document Root:
sudo nginx -T | grep -A4 -B4 '/var/www/kaevcms/public'When no domain is available, use the actual server IP in nginx:
server_name 192.168.50.111;10. Configure HTTPS when a domain is available
If the domain and its A record already point to the VDS, install Certbot:
sudo apt install -y certbot python3-certbot-nginxRequest the certificate:
sudo certbot --nginx \
-d example.com \
-d www.example.com \
-m admin@example.com \
--agree-tos \
--redirectVerify renewal:
sudo systemctl status certbot.timer --no-pager
sudo certbot renew --dry-runIf www is not used and has no DNS record, remove the second -d and remove www.example.com from server_name.
If no domain or A record is available yet, skip Certbot. Web Installer allows HTTP installation and only displays a warning. This is convenient for a test VDS, local IP, or private network. Configure a domain and HTTPS before opening a public website to users.
nginx security settings
These settings apply to the VDS itself. They are useful for both new installations and already running sites.
1. Hide the nginx version. Open the main configuration:
sudo nano /etc/nginx/nginx.confInside the http { ... } section, find the existing server_tokens directive (change it rather than duplicating it) and set:
server_tokens off;This hides the exact nginx version in the Server response header and standard error pages; it does not replace security updates.
2. Enable HSTS only after HTTPS is verified. First ensure the certificate works, HTTP redirects to HTTPS, and renewal passes sudo certbot renew --dry-run. Then edit:
sudo nano /etc/nginx/sites-available/kaevcmsPlace this directive inside every HTTPS server { ... } block (typically using listen 443 ssl), including a separate HTTPS redirect for www, if configured:
add_header Strict-Transport-Security "max-age=604800" always;604800 means 7 days. Verify HTTPS across the site before extending the lifetime. Do not set HSTS in an HTTP server block, on an IP-only installation, or enable includeSubDomains / preload without reviewing all subdomains. An nginx location with its own add_header directives may not inherit HSTS; repeat it in that location.
3. Validate and apply:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com/
curl -I https://example.com/login
curl -I https://example.com/not-a-real-pageLook for Strict-Transport-Security: max-age=604800 on HTTPS responses, including 404s. Login pages and authenticated responses in the updated CMS also receive Cache-Control: no-store.
Do not duplicate CSP in nginx
KaevCMS sets Content-Security-Policy (CSP) and private-response cache protection in the application. A second nginx CSP is not required: browsers enforce both policies and may block the website, player account features, or YooMoney checkout.
The application protections were added in commit 7fb7bade, after release 0.53.1. The installed CMS build must include this commit or a later equivalent fix for them to apply.
11. Run the Web Installer
With a domain and certificate, open:
https://example.com/install/Without a domain, open the server IP address:
http://192.168.50.111/install/Replace the example with the actual IP. Enter the database credentials created above and use the empty kaevcms database. The HTTP warning does not block the database check or final installation.
After installation, verify that Web Installer removed the public /install directory automatically, then sign in to the administrator panel. If the final page reports a cleanup failure, remove the directory manually.
12. Remove temporary write access
After .env has been created:
sudo chmod 755 /var/www/kaevcms
sudo chown "$USER":www-data /var/www/kaevcms/.env
sudo chmod 640 /var/www/kaevcms/.env
sudo chown "$USER":www-data /var/www/kaevcms/storage/app/installed.lock
sudo chmod 640 /var/www/kaevcms/storage/app/installed.lockVerify that private files are not served:
curl -I https://example.com/.env
curl -I https://example.com/vendor/autoload.phpExpect 404 or 403, never file contents.
13. Configure the scheduler
Create a system cron file:
sudo nano /etc/cron.d/kaevcmsContents:
* * * * * www-data cd /var/www/kaevcms && /usr/bin/php8.3 artisan schedule:run >> /dev/null 2>&1Apply permissions and test it:
sudo chmod 644 /etc/cron.d/kaevcms
sudo -u www-data /usr/bin/php8.3 /var/www/kaevcms/artisan schedule:runCheck the KaevCMS scheduler heartbeat after several minutes.
14. Queue worker for asynchronous mail
This section is only required after asynchronous queue delivery is enabled in KaevCMS.
Create a systemd service:
sudo nano /etc/systemd/system/kaevcms-queue.serviceContents:
[Unit]
Description=KaevCMS queue worker
After=network.target mysql.service php8.3-fpm.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/kaevcms
ExecStart=/usr/bin/php8.3 /var/www/kaevcms/artisan queue:work database --queue=mail-probe,mail,default --sleep=1 --tries=3 --timeout=120
Restart=always
RestartSec=5
TimeoutStopSec=360
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now kaevcms-queue
sudo systemctl status kaevcms-queue --no-pagerRead the log:
sudo journalctl -u kaevcms-queue -n 100 --no-pager15. Final verification
cd /var/www/kaevcms
sudo -u www-data php artisan about --only=environment
sudo -u www-data php artisan kaevcms:release-version --no-ansi
sudo -u www-data php artisan kaevcms:maintenance-status --no-ansi
sudo -u www-data php artisan kaevcms:encryption-health --no-ansi
sudo nginx -t
sudo systemctl is-active nginx php8.3-fpm mysql
sudo systemctl is-enabled nginx php8.3-fpm mysqlCheck in the browser:
- the home page;
/admin;- owner sign-in;
- news-image upload;
- SMTP test;
- LoginServer/GameServer connectivity;
- runtime diagnostics;
Updating later
For Update Agent installation, updates from the admin panel and SSH recovery, see Updating KaevCMS.
Changing the site address after installation
The KaevCMS address is stored in two places:
/var/www/kaevcms/.env
/etc/nginx/sites-available/kaevcmsMoving to another IP or using HTTP
Open .env:
sudo nano /var/www/kaevcms/.envSet the actual address and disable forced HTTPS:
APP_URL=http://192.168.50.112
APP_FORCE_HTTPS=false
SESSION_SECURE_COOKIE=falseAPP_FORCE_HTTPS=true makes KaevCMS generate HTTPS URLs. SESSION_SECURE_COOKIE=true prevents the browser from sending the session cookie over HTTP and may cause 419 Page Expired during sign-in.
Then change server_name in nginx:
sudo nano /etc/nginx/sites-available/kaevcmsserver_name 192.168.50.112;Apply the changes:
cd /var/www/kaevcms
php artisan optimize:clear
sudo nginx -t
sudo systemctl reload nginxAfter changing the IP, domain, or protocol, remove the old site cookies or open the site in a private browser window.
Moving to a domain and HTTPS
After configuring DNS and the certificate, update .env:
APP_URL=https://cms.example.com
APP_FORCE_HTTPS=true
SESSION_SECURE_COOKIE=trueAnd nginx:
server_name cms.example.com;Run php artisan optimize:clear, validate nginx, and reload it again.
Troubleshooting
502 Bad Gateway
sudo systemctl status php8.3-fpm --no-pager
ls -la /run/php/
sudo tail -n 100 /var/log/nginx/kaevcms-error.logEnsure nginx references an existing socket:
fastcgi_pass unix:/run/php/php8.3-fpm.sock;403 Forbidden
namei -l /var/www/kaevcms/public/index.php
sudo -u www-data test -r /var/www/kaevcms/public/index.php && echo readableEvery parent directory must provide execute/traverse permission to nginx.
Write-access failure
sudo -u www-data test -w /var/www/kaevcms/storage && echo storage-writable
sudo -u www-data test -w /var/www/kaevcms/bootstrap/cache && echo cache-writable
sudo -u www-data test -w /var/www/kaevcms/public/uploads && echo uploads-writableDo not fix this with chmod -R 777 /var/www/kaevcms.
If Laravel reports storage/framework/cache/data/...: No such file or directory, recreate and write-test the runtime tree as the PHP-FPM account:
cd /var/www/kaevcms
sudo mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache
sudo chown -R "$USER":www-data storage bootstrap/cache
sudo chmod -R g+rwX storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod 2775 {} \;
sudo -u www-data php artisan kaevcms:runtime-directories --probe
php artisan optimize:clear
sudo -u www-data php artisan kaevcms:runtime-directories --probeoptimize:clear is not a substitute for correct ownership. The updater runs the runtime-directory probe both before and after clearing caches.
419 Page Expired after signing in over HTTP
Check /var/www/kaevcms/.env:
APP_URL=http://192.168.50.111
APP_FORCE_HTTPS=false
SESSION_SECURE_COOKIE=false
SESSION_DOMAIN=Then run:
cd /var/www/kaevcms
php artisan optimize:clearDelete cookies for the old address or use a private browser window. APP_URL must match the address currently open in the browser.
Upload directories are reported as not writable
KaevCMS uses:
public/uploads/news
public/uploads/pages
public/uploads/settings
public/uploads/account-avatars
public/uploads/game-assetsThe current installer and system diagnostics create missing directories automatically. To repair permissions manually, run:
sudo mkdir -p \
/var/www/kaevcms/public/uploads/news \
/var/www/kaevcms/public/uploads/pages \
/var/www/kaevcms/public/uploads/settings \
/var/www/kaevcms/public/uploads/account-avatars \
/var/www/kaevcms/public/uploads/game-assets
sudo chgrp -R www-data /var/www/kaevcms/public/uploads
sudo chmod -R g+rwX /var/www/kaevcms/public/uploads
sudo find /var/www/kaevcms/public/uploads -type d -exec chmod 2775 {} \;Verify:
sudo -u www-data test -w /var/www/kaevcms/public/uploads/news && echo news-writable
sudo -u www-data test -w /var/www/kaevcms/public/uploads/pages && echo pages-writable
sudo -u www-data test -w /var/www/kaevcms/public/uploads/settings && echo settings-writableDo not use chmod -R 777.
A news item was created but is not visible on the site
Check that the item is not a draft, publishing is enabled, and the publication date is not in the future. A missing image does not prevent a published news item from appearing.
Logs
sudo tail -n 100 /var/log/nginx/kaevcms-error.log
sudo tail -n 100 /var/www/kaevcms/storage/logs/laravel.log
sudo journalctl -u php8.3-fpm -n 100 --no-pager
sudo journalctl -u kaevcms-queue -n 100 --no-pagerOfficial references
- Ubuntu 24.04 LTS release notes — PHP 8.3 baseline.
- Ubuntu 26.04 LTS summary — PHP 8.5 baseline.
- Install nginx on Ubuntu Server.
- Install PHP on Ubuntu Server.
- Install and configure MySQL.
- Ubuntu firewall documentation.
- Obtain TLS certificates on Ubuntu.
- Laravel deployment — serve only
public/index.php. - Composer install command.
- Certbot nginx instructions.
