| Component | Minimum version |
|---|---|
| OS | Ubuntu 22.04 LTS (24.04 recommended) |
| PHP | 8.1 (8.3 recommended) |
| PHP extensions | pdo_sqlite, fileinfo, mbstring, simplexml, gd (with FreeType), curl, intl |
| Web server | Nginx 1.18+ |
| PHP process manager | PHP-FPM |
| Composer | 2.x |
apt-get install -y \
nginx \
php8.3-fpm \
php8.3-cli \
php8.3-sqlite3 \
php8.3-mbstring \
php8.3-fileinfo \
php8.3-xml \
php8.3-gd \
php8.3-curl \
php8.3-intl \
libfreetype6-dev \
git \
sqlite3 \
unzip \
curl
systemctl restart php8.3-fpm
php8.3-xmlis required — it provides theSimpleXMLextension used by the XML-RPC API (/admin/xmlrpc.php). It is a separate package fromphp8.3-fpmand will not be pulled in automatically.
git clone https://github.com/your-org/clodd-cms /var/www/cms
cd /var/www/cms
composer install --no-dev --optimize-autoloaderphp bin/setup.phpThe script will:
- Prompt for an admin username and write it into
config.php - Prompt for an admin password and write its bcrypt hash into
config.php - Create the
data/directory and initialize the SQLite database - Optionally set the site title and URL
# Application files owned by deploy user, group www-data
chown -R deploy:www-data /var/www/cms
# Directories the web server must write to
chmod 775 /var/www/cms/data
chmod 775 /var/www/cms/content/media
# Restrict the SQLite database so only the www-data group can read/write it.
# Other users on the server must not be able to read the database directly.
# Run this after setup.php has created data/cms.db:
chmod 660 /var/www/cms/data/cms.db
# Web root writable so the Builder can write generated HTML
chmod 775 /var/www/cms
# Storage directory for CLI script logs (e.g. webmention send log)
mkdir -p /var/www/cms/storage
chmod 775 /var/www/cms/storage
# If generated HTML files already exist (e.g. copied from dev), make them group-writable
# so PHP-FPM (www-data) can overwrite them on rebuild
chmod -R g+w /var/www/cms/posts
chmod -R g+w /var/www/cms/pages
chmod g+w /var/www/cms/index.html /var/www/cms/feed.xml 2>/dev/null || trueEdit /etc/php/8.3/fpm/php.ini:
upload_max_filesize = 50M
post_max_size = 55M
max_execution_time = 60
memory_limit = 128M
display_errors = Off
log_errors = On
error_log = /var/log/php8.3-fpm.logRestart FPM: systemctl restart php8.3-fpm
Also set umask in the PHP-FPM pool config so generated files are group-writable (required for the web process to overwrite static HTML on rebuild):
# Edit /etc/php/8.3/fpm/pool.d/www.conf
# Add or update:
umask = 002systemctl restart php8.3-fpmInstall the site config:
cp /var/www/cms/nginx.conf.example /etc/nginx/sites-available/cms
# Edit server_name and paths as needed
ln -s /etc/nginx/sites-available/cms /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginxAnalytics beacon:
track.phprequires its ownlocation = /track.phpPHP-FPM block withlimit_reqrate limiting andlimit_except POSTto reject non-POST requests. The example config (nginx.conf.example) already includes this block — do not remove it or let the/admin/catch-all handle it.
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot
certbot --nginx -d example.com -d www.example.comCertbot auto-installs the certificate paths and sets up renewal.
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 80/tcp # HTTP → redirects to HTTPS
ufw allow 443/tcp # HTTPS
ufw enableVisit https://example.com/admin/ and log in with the credentials set during setup.
Click Settings to enter your site URL, then Rebuild entire site on the dashboard to generate the initial HTML output.
- Settings — fill in site title, URL, description, author name, timezone, and footer text, then save
- Dashboard → Rebuild entire site — generates the index and feed with your new settings
- Account → Set up two-factor authentication — scan the QR code with an authenticator app, confirm with a code, and save your backup codes somewhere safe
TOTP 2FA is optional but strongly recommended. It is configured per-account in Admin → Account.
Setup:
- Click Set up two-factor authentication
- Scan the QR code with Google Authenticator, Authy, 1Password, or any TOTP app; or enter the manual key
- Enter the 6-digit code from the app to confirm setup
- Copy and store the 8 backup codes — they will not be shown again
After setup is enabled, login requires two steps: password, then a 6-digit TOTP code. A backup code can be used instead of the TOTP code if the device is unavailable.
Backup codes are single-use. Regenerate them at any time from the Account page (requires password confirmation). Each regeneration invalidates all previous codes.
To disable 2FA, click "Disable two-factor authentication" on the Account page and confirm with your password.
The CMS exposes a WordPress-compatible XML-RPC API. To connect MarsEdit:
- Add Blog → choose WordPress
- Endpoint URL:
https://example.com/admin/xmlrpc.php - Username / Password: your admin credentials
MarsEdit will show Posts and Pages sections. All CRUD operations, category/tag assignment, and media uploads work from the client. The same endpoint also supports the MetaWeblog API for other clients.
The CMS can send webmention pings to external URLs linked from your published posts. Because endpoint discovery and pinging can take time, this runs as a CLI script rather than a web request:
php /var/www/cms/bin/send-webmentions.php # send for posts updated since last run
php /var/www/cms/bin/send-webmentions.php --force # re-send for all published postsAdd to your crontab (crontab -e) to run daily at 02:00:
0 2 * * * /usr/bin/php /var/www/cms/bin/send-webmentions.php >> /var/www/cms/storage/webmentions.log 2>&1
Verify the PHP binary path first: which php
The script exits with code 0 on success and 1 if any pings failed (useful for monitoring).
# /etc/cron.daily/cms-backup
#!/bin/bash
DATE=$(date +%Y%m%d)
mkdir -p /var/backups/cms
sqlite3 /var/www/cms/data/cms.db ".backup '/var/backups/cms/cms-${DATE}.db'"
find /var/backups/cms -name "*.db" -mtime +30 -deletechmod +x /etc/cron.daily/cms-backupdocker compose up --build
# Then in a second terminal:
docker compose exec php php bin/setup.phpVisit http://localhost:8080/admin/.
cd /var/www/cms
git pull
composer install --no-dev --optimize-autoloader
# Log in to admin and click "Rebuild entire site"Schema migrations run automatically on the next page load — no manual SQL needed.
| Extension | Package | Purpose |
|---|---|---|
pdo_sqlite |
php8.3-sqlite3 |
SQLite database |
fileinfo |
php8.3-fileinfo |
Upload MIME validation |
mbstring |
php8.3-mbstring |
Required by league/commonmark and spomky-labs/otphp |
simplexml |
php8.3-xml |
XML-RPC API request parsing |
gd |
php8.3-gd |
OG image generation (requires FreeType) |
curl |
php8.3-curl |
Mastodon & Bluesky API calls |
intl |
php8.3-intl |
Locale-aware string handling |
session |
built-in | Admin sessions |
hash |
built-in | Content-hash diffing |
json |
built-in | Config/settings |
openssl |
built-in | Session security |
