— Journal · 11 MIN READ · Cockpit

Cockpit CMS v2 in production: twelve lessons from four sites

Self-hosted, open-source, headless. The things that bit us in production and the patterns that earn their keep.

We've shipped four production sites on Cockpit CMS v2 in the past 18 months. Including this one. It's not the most-Googled headless CMS — that crown belongs to Sanity, Strapi, Contentful, in some order — but Cockpit fits a niche the famous names ignore: small to medium content workloads on cheap infrastructure, edited by people who don't want a 90-minute training session.

Here are twelve things we learned the hard way. If you're evaluating Cockpit, this will save you a week.

1. Cockpit v2 is not Cockpit v1

If a tutorial is from before 2024, throw it out. API shape changed. Directory layout changed. Admin UI changed. Data model changed. v2 is, for practical purposes, a different product. Filter your search results to "v2" or "2024+".

2. Self-hosting means you own the WAL

Cockpit v2's default storage is SQLite. SQLite uses Write-Ahead Logging by default. The .sqlite-wal and .sqlite-shm files matter. If you rsync only the .sqlite file to a new server, you lose recent writes. Either checkpoint first (PRAGMA wal_checkpoint(FULL)) or copy all three files together.

3. The schema cache is a real thing

After editing a model JSON, Cockpit caches the parsed schema in storage/data/app.memory.sqlite. Until you delete that file, the admin UI will show stale fields. Build it into your deploy script.

4. i18n is per-field, not per-document

You mark fields as i18n: true in the schema. Cockpit stores them as field (default locale) and field_pt, field_es, etc. The API serves whichever locale you request via ?locale=pt. Plan your schema with this in mind; retrofitting later is a manual SQL job.

5. The API key in the URL is a footgun

Don't pass the API key as a query parameter. It ends up in logs, in browser history, in referrer headers. Use the api-key HTTP header instead. Build your fetch helper to enforce it.

6. Image assets need a proxy

Cockpit's image API requires the API key. If you reference Cockpit URLs directly from your frontend, you expose the key. We wrote a 30-line PHP proxy that takes ?id=&w=, fetches with the key server-side, and returns WebP with cache-control headers. This is true for any headless CMS with a private asset API.

7. Self-HTTPS loopback breaks on some hosts

When PHP on example.com tries to call https://example.com/cockpit/api/..., some shared hosts refuse the loopback. file_get_contents() returns false with no error. The fix: use cURL (it works where file_get_contents doesn't) with a sane fallback. Don't assume your local dev environment matches your production one.

8. Cockpit doesn't have a built-in webhook for "content changed"

There's an event system inside Cockpit's PHP, but no out-of-the-box outbound webhook on save. If you need to invalidate a CDN on publish, you'll write the bridge yourself: a small Cockpit addon that listens to collection.save.after and POSTs to your CDN's purge endpoint.

9. The model JSON is your real source of truth

Cockpit's UI lets you build models visually, but version-control the model JSON files. Keep them in your repo alongside the site code. If your Cockpit instance ever dies, you can rebuild the entire schema from those files. The data itself lives in SQLite — back that up separately.

10. Roles + API keys are decoupled

An API key has a role attached at creation. Changing the role of an API key in the admin UI requires deleting the key and making a new one. Don't bake API keys into a deployment until you've decided what permissions it needs.

11. Markdown is not the default — WYSIWYG is

If you want devs writing in Markdown and editors writing in WYSIWYG, you'll pick the WYSIWYG widget. The output is HTML. Embed Prism.js or similar on the rendering side if you want code blocks to render with syntax highlighting; Cockpit does not do this for you.

12. The community is small but responsive

Cockpit's GitHub issues get attention from the maintainer (Artur Heinze) directly. Documentation is patchy — read the source. This is the price of admission for a CMS that's not VC-backed: less Stack Overflow, more reading modules/Content/Controller/Api.php when something behaves unexpectedly. We've found this more reliable than dealing with paid support at VC-funded vendors.

The error messages, and what they actually mean

None of these produce a stack trace. Every one of them looks like something else — which is why they cost hours. All were hit on this site.

A model you just created returns 404 from the API

You created tender_source, the admin shows it, and /api/content/items/tender_source answers 404. The model was saved under a different name: Content/Helper/Model.php runs preg_replace('/[^A-Za-z0-9]/', '', $name) on creation, so tender_source became tendersource. Underscores and hyphens are stripped silently, with no warning and no error. Never put anything but letters and digits in a model name.

createModel() and updateModel() return false instead of throwing

A failed model write is not an exception. Both return false, so try/catch catches nothing and an importer that only watches for exceptions will report success while creating nothing. Check the return value, then confirm with exists() — the store, not the return, is the source of truth.

A partial POST created a duplicate item instead of updating

saveItem() merges when the payload carries _id — array_merge($current, $item) — so posting two fields updates two fields and leaves the rest alone. Omit _id and the same payload merges with the model defaults instead and inserts a new item. The difference between an update and a duplicate is one key.

Translations look like they vanished after saving

You write i18n fields with suffix keys in the default payload — title_pt, body_es — but reading the item back without ?locale= returns the default locale only, with no suffix keys anywhere in the response. Nothing was lost; the read is localised. Pass ?locale=pt to see it. And when you filter at the same time, the filter must be a JSON string, not a nested query array.

A blank white page after a PHP version change

Cockpit 2.14 requires PHP 8.3. On anything older, bootstrap.php reaches Composer's platform_check.php, which calls die() — not an exception. No try/catch can intercept it, so any page that boots Cockpit renders blank with nothing in the response. If your host sets one PHP version for the web and another for cron, the cron job dies this way while the site keeps working.

Searching for entries-batchedit.tag?

Then you are reading Cockpit v1 documentation. Riot .tag files do not exist in v2 — batch editing lives in Content/views/collection/items.php. Most of the Cockpit answers on the web predate v2 and describe a different product; see lesson 1.

FAQ

Can I follow pre-2024 Cockpit CMS tutorials for v2?

No — Cockpit CMS v2 is not v1, so you should throw out pre-2024 tutorials. Many patterns and behaviours changed between the major versions.

How do I safely back up a self-hosted Cockpit SQLite database?

Because SQLite uses Write-Ahead Logging (WAL), copy the .sqlite, -wal, and -shm files together, or checkpoint first. Self-hosting means you own the WAL, so copying only the main file risks an inconsistent backup.

My schema changes aren't showing up after editing a model — why?

Cockpit has a real schema cache, so delete storage/data/app.memory.sqlite after model edits. The model JSON is your real source of truth, so version-control it.

How does Cockpit handle internationalisation and how should I pass the API key?

i18n is per-field, not per-document — you work with field, field_pt, and field_es, requesting a locale with ?locale=pt. Pass credentials via the api-key header rather than in the URL, which is a footgun.

How do I purge a CDN when content changes in Cockpit?

Cockpit has no built-in outbound webhook for content-changed events, so write an addon on collection.save.after to purge the CDN. For images, use a proxy that keeps the key server-side and returns WebP.

The summary

If you need a CMS that's: free, self-hostable on cheap infrastructure, fast to edit, with first-class i18n and a real REST API — Cockpit is the strongest option in 2026. If you need real-time collaborative editing, an army of integrations out of the box, or hand-holding documentation — pick something else. The trade-off is honest. We pay it gladly.

— The journal, by email

One email when something worth reading goes up.

No sequences, no sharing, unsubscribe in one click. See our privacy policy.

— Read next
— Next steps

Tell us what was only ever described.

Start a build

or write to info@amplifiedcreations.com · a senior member of the team answers