14 — Installation and deployment#
14.1 Requirements#
| Minimum | Recommended | |
|---|---|---|
| PHP | 8.3 | 8.3 |
| Extensions | mbstring, openssl, pdo_mysql, tokenizer, xml, ctype, json, fileinfo, zip, gd (with WebP) |
plus imagick, opcache |
| MySQL | 8.0 | 8.0 |
| Disk | 500 MB + media | 2 GB |
| Memory limit | 128 MB | 256 MB |
| Access | SSH, one cron entry, the ability to point the document root at public/ or create a symlink |
same |
| Local only | Node 20 + npm (to build assets) | same |
MariaDB is not a supported target. The schema uses MySQL 8 JSON functions and utf8mb4_0900_ai_ci;
MariaDB would work with changes we are not maintaining.
14.2 Local setup#
git clone <repo> kodepress && cd kodepress
composer install
cp .env.example .env
php artisan key:generate
# create the database, then set DB_* in .env
php artisan kodepress:install # migrations, seeds, first admin, storage:link
npm install && npm run build # assets; commit public/build when it changes
php artisan serve # http://127.0.0.1:8000 | /admin
.env essentials:
APP_NAME=KodePress
APP_ENV=local
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000
APP_TIMEZONE=UTC # storage is UTC; display uses sites.timezone
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=kodepress
DB_USERNAME=root
DB_PASSWORD=
CACHE_STORE=file
SESSION_DRIVER=database
QUEUE_CONNECTION=sync
FILESYSTEM_DISK=public
KODEPRESS_ADMIN_PATH=admin
KODEPRESS_CACHE_ENABLED=true
KODEPRESS_MAX_UPLOAD_MB=8
14.3 php artisan kodepress:install#
Idempotent and safe to re-run; it detects an existing install and offers --force to reseed demo
content only.
1 check requirements (PHP, extensions, writable paths) and stop with a readable list if any fail
2 detect the image driver (imagick | gd) and write it to the config cache
3 php artisan migrate --force
4 seed: roles + permissions, theme tokens (default preset), templates,
default header + footer, main menu, demo pages/posts/categories/tags/form/media
5 prompt: first Admin name, email, password (hidden, confirmed, strength-checked)
--name= --email= --password= for non-interactive use
6 php artisan storage:link (or copy the symlink fallback where symlinks are disabled)
7 compile the theme CSS
8 build the block manifest
9 warm the homepage
10 print: Done. Open https://<host>/admin (and the admin path if it is not the default)
Non-interactive form used by CI and by the deploy script:
php artisan kodepress:install --name="Owner" --email=owner@example.com --password="..." --no-demo
14.4 cPanel deployment — first time#
The live directory is a git checkout. Production is never hand-edited and never rsynced.
# 1. local: build assets and push
npm run build
git add public/build && git commit -m "build: assets" && git push origin main
# 2. server: clone beside the webroot
ssh -p 2222 user@host
cd ~
git clone https://github.com/<org>/kodepress.git kodepress
cd kodepress
composer install --no-dev --optimize-autoloader
# 3. env
cp .env.example .env && nano .env # APP_ENV=production, APP_DEBUG=false, DB_*, APP_URL
php artisan key:generate
# 4. database: create it in cPanel, then
php artisan kodepress:install --name="Owner" --email=owner@site.com --no-demo
# 5. document root
# Preferred: point the domain's document root at ~/kodepress/public in cPanel.
# If the host forbids it:
rm -rf ~/public_html && ln -s ~/kodepress/public ~/public_html
# 6. cron (cPanel -> Cron Jobs -> every minute)
* * * * * cd ~/kodepress && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
# 7. verify
php artisan kodepress:doctor
Notes for this class of host:
- Use the host's PHP 8.3 binary, which is often
/usr/local/bin/ea-php83,/opt/alt/php83/usr/bin/phporlsphp83, not the defaultphpon$PATH. The deploy script resolves it once and records it. - Where
public_htmlmust itself be the checkout (some hosts refuse both a changed document root and a symlink), put the application in~/kodepressand copy onlypublic/contents intopublic_html, withindex.phppaths adjusted — and record that in the release notes, because it is the one layout that makesgit pullinsufficient. Prefer the symlink. .user.iniis ignored undermod_lsapion several hosts; never rely on it for limits.- If symlinks are disabled,
storage:linkfalls back to a copy and the installer says so; media then needskodepress:media-syncafter an upload-heavy session. Avoid such hosts.
14.5 Deploy script#
bin/deploy.sh, invoked over SSH. Deploy = git pull. Nothing else is a deploy.
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
PHP=${PHP_BIN:-php}
git fetch --all --prune
git reset --hard "origin/${BRANCH:-main}" # refuse local edits by design
composer install --no-dev --optimize-autoloader --no-interaction
$PHP artisan down --render="errors::503" || true
$PHP artisan migrate --force
$PHP artisan kodepress:blocks-refresh
$PHP artisan kodepress:theme-compile
$PHP artisan cache:clear
$PHP artisan kodepress:cache-clear # the HTML cache
$PHP artisan route:cache
$PHP artisan view:cache
$PHP artisan up
$PHP artisan kodepress:cache-warm --limit=20
$PHP artisan kodepress:doctor
echo "HEAD: $(git rev-parse --short HEAD) origin: $(git rev-parse --short origin/${BRANCH:-main})"
Rules this encodes:
git reset --hard origin/<branch>— the server has no local state worth keeping. If someone edited a file on the server, the deploy discards it, which is the point.- No
config:cache. On these hosts a cached config plus a changed.envproduces a site that reads stale credentials and fails in ways nobody can debug over SSH.route:cacheandview:cacheare safe and kept. - Migrations run with
--forceand only ever add; a destructive migration requires an ADR and a backup taken in the same session. - After deploying, confirm
HEAD == origin/<branch>. The script prints both.
14.6 Post-deploy verification#
Every deploy, in this order:
kodepress:doctor— all green.- Load the homepage as a guest; confirm a 200 and the expected header/footer.
- Load one inner page and one blog post; confirm the cache header reports a hit on the second load.
- Log in to
/admin; open the page editor on a real page; confirm the preview renders. - Publish a trivial change on a scratch page and confirm it appears publicly within one reload.
- Submit the contact form once and confirm the submission and the notification email.
- Confirm
HEADmatchesorigin.
Steps 2–6 are the Playwright smoke suite (15-testing-and-qa.md), so this list is a command, not a ritual.
14.7 Rollback#
cd ~/kodepress
git log --oneline -n 10
git reset --hard <previous-sha>
composer install --no-dev -o
php artisan migrate --force # forward-only; see below
php artisan cache:clear && php artisan kodepress:cache-clear && php artisan kodepress:cache-warm
Schema changes are forward-compatible by rule: a migration adds columns and tables, never renames or drops in the same release that starts using them. A column is dropped only one release after nothing reads it. That makes a code rollback safe without a database rollback, which is the only kind of rollback that works at 2 a.m. on shared hosting.
14.8 Backups#
- Automatic: the nightly cron takes a database backup and keeps 7 (configurable), written to
storage/app/backups, never web-reachable. - Media is backed up weekly, chunked (10).
- Before any schema-changing deploy, take a manual backup in the same SSH session and note its filename in the release notes.
- Off-site copies are the host's job (cPanel backup) plus a manual download from
Settings -> Backup. KodePress does not ship S3 credentials handling in core.
14.9 Domains, TLS and email#
- TLS via AutoSSL / Let's Encrypt in cPanel. After issuing, check the live certificate's issuer:
a rate-limited renewal can install a staging certificate while reporting success, which breaks every
browser and is easy to misread as a DNS problem.
kodepress:doctorverifies the issuer and the expiry. - Force HTTPS in production (
APP_URLwithhttps, plus the host's redirect). HSTS once the certificate is confirmed good. wwwand apex both resolve; one redirects to the other andAPP_URLmatches the canonical one, because the canonical host ends up inside cached HTML.- Mail: use the host's SMTP or a transactional provider; set
MAIL_FROM_ADDRESSto a mailbox that actually exists on the sending domain, and confirm SPF (and DKIM where available). A missing mailbox silently loses contact-form notifications —kodepress:doctorsends a test mail.
14.10 Environment matrix#
| Local | Staging | Production | |
|---|---|---|---|
APP_ENV |
local | staging | production |
APP_DEBUG |
true | true | false |
| HTML cache | off (KODEPRESS_CACHE_ENABLED=false) |
on | on |
log driver |
real, to a test inbox | real | |
| Demo content | yes | yes | no (--no-demo) |
robots.txt |
n/a | Disallow: / |
real |
| Basic auth | no | yes (host-level) | no |
| Backups | no | weekly | nightly |
Staging is optional but strongly advised for anything touching migrations or the header/footer resolver.
14.11 Troubleshooting#
| Symptom | Likely cause | Fix |
|---|---|---|
| 500 on every page after deploy | stale cached config or views | php artisan cache:clear && view:clear && route:clear; never config:cache |
| Styles missing | public/build not committed, or the symlink missing |
rebuild locally and commit; storage:link |
| Images 404 | public/storage symlink missing or host disallows symlinks |
storage:link, else the copy fallback |
| Scheduled posts never publish | cron entry missing or wrong PHP binary | check the Dashboard cron tick; use the host's php83 path |
| "I published but nothing changed" | HTML cache not cleared (a bug) or a CDN in front | kodepress:cache-clear; check the X-KodePress-Cache header |
| Editor saves then reverts | two tabs open on the same page | the conflict modal explains it; close one |
| Bengali shows as boxes | font files not deployed | confirm public/storage/fonts, rebuild |
| Upload fails over ~2 MB | host upload_max_filesize |
the media screen shows the server limit; ask the host |
| Form mail not arriving | sender mailbox does not exist, SPF fails | fix the mailbox, re-run doctor |
| Permission denied writing cache | wrong owner on storage/ after an SSH-as-root action |
chown -R <cpaneluser>:<cpaneluser> storage bootstrap/cache |
KodePress documentation · generated from the Markdown sources by
tools/build-docs-site.py · internal preview, not indexed.