Deployment Guide
This guide covers best practices for deploying Total CMS sites to production and managing deployments with version control.
Git Configuration
Section titled “Git Configuration”When using Git for version control, add the following to your .gitignore file to exclude Total CMS runtime files:
# Total CMS 3tcms-data**/tcms/cache**/tcms/logs**/tcms/tmpWhat These Directories Contain
Section titled “What These Directories Contain”| Directory | Purpose | Why Ignore |
|---|---|---|
tcms-data | All CMS content (collections, files, uploads) | Content is environment-specific and managed through the CMS |
tcms/cache | Twig template cache, computed data | Generated at runtime, varies by environment |
tcms/logs | Application logs | Environment-specific, should not be shared |
tcms/tmp | Temporary files (uploads in progress, etc.) | Transient data |
Files to Commit
Section titled “Files to Commit”You should commit your customization files:
tcms.php- Site configuration- Custom templates in your theme directory
- Custom schemas if you’ve created any
Versioning Content (Optional)
Section titled “Versioning Content (Optional)”Ignoring tcms-data is the right default: on most sites editors work in the admin on production, and git and the admin cannot both be the source of truth for the same content. To move content between a local copy and production, use tcms push and tcms pull instead.
Some sites work the other way round — content written by developers, or by an agent working locally, then deployed like code. Content can be versioned there, but never commit the whole tcms-data folder. It holds more than content:
| Path | What it holds | Why it must not be committed |
|---|---|---|
tcms-data/.system/ | Settings, API keys, the site encryption key (site.key), OAuth signing keys, sessions, remember-me tokens, OAuth grants, job and automation queues, migration state, record history, and logs on zip installs | API keys and the sync key are stored as-is, so the repository becomes a set of admin credentials. Sessions, queues and migration state are live runtime state: deploying them logs people out, re-runs jobs, or skips a migration |
tcms-data/auth/ | User records, with password hashes and passkeys | Credentials, even hashed, do not belong in a repository |
tcms-data/**/.index.json | Each collection’s index | Rebuilt on every save, so every commit touches it and any two branches conflict on it. It can be regenerated |
Commit the collection folders (each object file and the collection’s .meta.json) and .schemas/, and ignore the rest:
# Total CMS 3 — versioned content, never runtime statetcms-data/.system/tcms-data/auth/tcms-data/**/.index.json**/tcms/cache**/tcms/logs**/tcms/tmpAfter cloning or pulling, rebuild the indexes so they match the files on disk:
tcms repair:index --allThree things to expect:
.meta.jsonchanges on saves. It is the collection’s definition, so it must be committed, but it also carries the object count and a last-updated time. Expect those lines in diffs, and resolve a conflict on them either way —repair:indexrecalculates the count.- Uploads are binary. Images, files and depots live inside the object folders, so committing them puts every upload’s full history in the repository. Consider Git LFS for them, or keep them out of git.
- Diffs read best in Markdown. A collection can store each object as Markdown with YAML front matter instead of JSON; see Storage Format.
Deployment Pipeline
Section titled “Deployment Pipeline”After pulling new code, four things have to happen in the right order to bring a Total CMS site fully up to date:
- Composer install — fetch and optimise PHP dependencies
- Frontend build — compile CSS/JS via Vite or your chosen pipeline
tcms deploy— wipe the compiled DI container, clear application caches, run pending migrations- Reload PHP-FPM — flush the FPM worker OPcache (CLI can’t reach it)
The tcms deploy Command
Section titled “The tcms deploy Command”Total CMS ships a single CLI command that owns the runtime cleanup the library knows how to do safely:
vendor/bin/tcms deployIt does three things, in order:
| Step | Why |
|---|---|
Wipe cache/container/ | The compiled PHP-DI container caches class constructor signatures. When a deploy changes a constructor (new dependency, removed parameter), stale compiled containers crash with TypeError. The compiled-class name embeds container.php’s mtime but doesn’t track other class changes — manual wipe is the only way to force a clean regen. |
| Clear all application caches | APCu, Redis, Memcached, filesystem cache, image cache, CLI OPcache. Equivalent to tcms cache:clear but bundled into the deploy flow. |
| Run pending migrations | One-shot data migrations in MigrationRunner get applied immediately rather than firing on the next user request (where a slow migration would manifest as latency). |
Each step has a --skip-* flag (--skip-container, --skip-cache, --skip-migrations) for special-case deploys.
tcms deploy is the recommended entry point for any deploy script.
On a Composer install you rarely have to type it. totalcms/cms is a Composer plugin, and after every composer update it runs tcms deploy itself, so a version bump cannot leave the site on a stale compiled container or with migrations unapplied. It does not run after a plain composer install of an unchanged lockfile, which needs neither. The deploy script below still calls it explicitly — the command is idempotent, so running it twice is harmless — and the PHP-FPM reload stays yours: no CLI process can reach the workers’ OPcache.
Markdown Collections
Section titled “Markdown Collections”A collection stored in markdown format is edited as plain .md files — often committed to the same git repository as the site’s code. Those edits are invisible to Total CMS until the collection’s index is rebuilt, so add one line to your deploy script for each markdown collection you keep in git:
vendor/bin/tcms repair:index docsRun it after the code checkout and before (or as part of) tcms deploy, so a page added or edited in the last commit shows up in the admin, API, and Twig as soon as the deploy finishes. repair:index also clears each object’s cached copy right after it rebuilds the index (the admin’s Rebuild Index button does the same for markdown collections), so a request that already had the old contents cached doesn’t keep serving them after the deploy.
Standard bin/deploy.sh
Section titled “Standard bin/deploy.sh”The project skeleton (totalcms/totalcms-project) ships a reference script at bin/deploy.sh. The shape:
#!/usr/bin/env bashset -euo pipefail
PHP_FPM_SERVICE="php8.3-fpm" # edit for your distro/version
cd "$(dirname "$0")/.."
composer install --no-dev --optimize-autoloader --no-interaction --no-progress
if [ -d frontend ]; then ( cd frontend && npm ci --no-audit --no-fund --silent && npm run build )fi
vendor/bin/tcms deploy
if [ -n "$PHP_FPM_SERVICE" ]; then sudo systemctl reload "$PHP_FPM_SERVICE"fiWire it up to whatever triggers your deploy — webhook, cron, CI/CD job, etc.
Why FPM Reload Is Separate
Section titled “Why FPM Reload Is Separate”tcms deploy runs from CLI, which has its own OPcache instance. PHP-FPM workers each have a separate OPcache that the CLI process can’t reach. So even after tcms deploy resets CLI OPcache, FPM is still serving the old bytecode until you reload it.
systemctl reload php-fpm is graceful — in-flight requests finish on the old workers; new requests pick up the new code. No dropped connections, no downtime.
If you run with opcache.validate_timestamps=1 (slower in steady state but auto-detects file changes), you can skip the reload — but most production setups disable timestamp validation for performance.
CI/CD Integration
Section titled “CI/CD Integration”GitHub Actions (SSH deploy)
Section titled “GitHub Actions (SSH deploy)”- name: Deploy uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: deploy key: ${{ secrets.DEPLOY_KEY }} script: | cd /var/www/example.com git pull --ff-only bash bin/deploy.shGitLab CI
Section titled “GitLab CI”deploy: script: - ssh deploy@$DEPLOY_HOST 'cd /var/www/example.com && git pull --ff-only && bash bin/deploy.sh'Cache Clear Without Shell Access
Section titled “Cache Clear Without Shell Access”For shared-hosting environments where you can’t run shell commands, Total CMS exposes an HTTP fallback that clears application caches (but not the compiled DI container or PHP-FPM OPcache):
curl -s https://example.com/tcms/api/emergency/cache/clearThis is a last resort — it can’t wipe cache/container/ or reload FPM workers, so any deploy that changes class signatures or constructor wiring needs proper shell access to tcms deploy.
Troubleshooting Deployments
Section titled “Troubleshooting Deployments”Changes Not Appearing
Section titled “Changes Not Appearing”- Clear the cache using the endpoint above
- Check that OPcache is clearing (may require PHP-FPM restart)
- Verify CDN cache is cleared if using one
Cache Clear Endpoint Not Working
Section titled “Cache Clear Endpoint Not Working”If the endpoint returns an error:
- Check that the site is accessible
- Review PHP error logs
- As a fallback, restart PHP-FPM:
systemctl restart php-fpm
Permission Issues After Deployment
Section titled “Permission Issues After Deployment”Ensure the web server user has write access to:
tcms-data/- Content storagetcms/cache/- Template cachetcms/logs/- Application logstcms/tmp/- Temporary files
chown -R www-data:www-data tcms-data tcms/cache tcms/logs tcms/tmpSymlink Versioned Deployments
Section titled “Symlink Versioned Deployments”For zero-downtime updates with instant rollback, you can use versioned directories with a symlink. This is the same pattern used by Capistrano, Laravel Envoyer, and similar deployment tools.
Directory Structure
Section titled “Directory Structure”/var/www/example.com/├── tcms -> tcms-3.5.0/ # symlink to active version├── tcms-3.2.2/ # previous version (kept for rollback)├── tcms-3.5.0/ # current version├── tcms-data/ # shared data (never changes between versions)└── public/ └── index.php # references tcms/ (follows symlink)Deploying a New Version
Section titled “Deploying a New Version”# Upload or extract the new versionunzip totalcms-3.5.0.zip -d /var/www/example.com/tcms-3.5.0
# Switch the symlink (atomic operation)cd /var/www/example.comln -sfn tcms-3.5.0 tcms
# Clear cachephp tcms/resources/bin/tcms cache:clearThe ln -sfn command atomically replaces the symlink. There is no moment where the application is unavailable — requests in progress continue using the old version, and new requests use the new one.
Rolling Back
Section titled “Rolling Back”cd /var/www/example.comln -sfn tcms-3.2.2 tcmsphp tcms/resources/bin/tcms cache:clearCleanup
Section titled “Cleanup”Keep one or two previous versions for rollback, then remove older ones:
# Remove old versions (keep current and one previous)rm -rf tcms-3.2.1/tcms-data/is shared across all versions — it sits outside the versioned directories and is never touched during deployments- The
cache/,logs/, andtmp/directories inside each version can be symlinked to shared directories if needed, or left as-is (they’re recreated automatically) - This approach works well with CI/CD pipelines — your build step creates the versioned directory, and the deploy step switches the symlink
- The built-in one-click updater in the admin dashboard uses a simpler backup-and-swap approach. The symlink pattern is for teams that manage their own deployment process