How to Package and Deploy Strapi v5 as a Custom App on Cloudron
-
How to Package and Deploy Strapi v5 as a Custom App on Cloudron
You people are amazing! I actually managed to deploy Strapi on Cloudron. I hope that we can support Strapi as an official application soon.
Who is this guide for?
This guide covers two paths:- Path A — Personal/team installation: Deploy Strapi for your own Cloudron instance. No store submission required.
- Path B — Official packaging: Everything you need to eventually submit Strapi to the Cloudron App Store.
Both paths share the same core steps. Path B additions are clearly marked.
What is Strapi?
Strapi is the leading open-source headless CMS. It gives you a self-hosted REST and GraphQL API with a beautiful admin panel for managing content. Strapi v5 (current stable) runs on Node.js 20/22 LTS and PostgreSQL.
Prerequisites
Requirement Notes Cloudron instance v8.0.0 or later (base image 5.0.0) Local machine To run the Cloudron CLI and build the Docker image Node.js 20 or 22 LTS For the Cloudron CLI — install via nvm pnpm Preferred package manager — curl -fsSL https://get.pnpm.io/install.sh \| sh -Docker Hub account Free at hub.docker.com — needed to host your built image Podman or Docker For building the image locally Memory note for the Cloudron team: Strapi v5 requires at least 1 GB RAM to start reliably. The default 256 MB limit for custom apps will OOM-kill it. We'd love to see the default raised or a prominent warning in the CLI/docs for Node.js apps. In the meantime, set
"memoryLimit": 1073741824in your manifest.
Bazzite / Fedora Atomic Desktop usersBazzite (and other Fedora Atomic/immutable desktops) ships Podman instead of Docker. Good news: Podman is a drop-in replacement for everything in this guide.
# Install nvm (works on immutable systems — installs to ~/.nvm) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 22 && nvm alias default 22 # Install pnpm curl -fsSL https://get.pnpm.io/install.sh | sh - source ~/.bashrc # Install Cloudron CLI npm install -g cloudron # Podman is already installed — log in to Docker Hub podman login docker.ioGotcha #1 — Podman OCI format warning: You'll see
SHELL is not supported for OCI image formatwarnings during builds. These are harmless — add# syntax=docker/dockerfile:1as the first line of your Dockerfile to suppress them, or usepodman build --format dockerflag.
Step 1 — Create the package directory
mkdir -p ~/projects/strapi-cloudron cd ~/projects/strapi-cloudron mkdir -p strapi-app/config/env/production mkdir -p strapi-app/src/api mkdir -p strapi-app/public/uploadsYour final structure will look like:
strapi-cloudron/ ├── Dockerfile ├── CloudronManifest.json ├── start.sh ├── .dockerignore └── strapi-app/ ├── package.json ├── config/ │ ├── database.js │ ├── server.js │ └── admin.js ├── src/api/ └── public/uploads/
Step 2 — Log in to Cloudron CLI
cloudron login your.cloudron.domain # Enter your admin username, password, and 2FA token when prompted cloudron list # verify connectionGotcha #2 — CLI runs on your local machine, not the server. Run all
cloudroncommands from your laptop/desktop, not via SSH on the server.
Step 3 — Write the Strapi config files
strapi-app/package.json{ "name": "strapi-app", "private": true, "version": "0.1.0", "scripts": { "develop": "strapi develop", "start": "strapi start", "build": "strapi build", "strapi": "strapi" }, "dependencies": { "@strapi/strapi": "5", "@strapi/plugin-users-permissions": "5", "pg": "^8.11.0", "better-sqlite3": "^9.4.3", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router-dom": "^6.0.0", "styled-components": "^6.0.0" }, "engines": { "node": ">=20.0.0 <=22.x.x", "pnpm": ">=8" } }Gotcha #3 — Strapi v5 package names changed.
@strapi/plugin-i18nno longer exists as a separate package in v5 — it's built into core. Using"^5.0.0"version ranges also causes resolution failures; use"5"instead to get the latest stable v5.Gotcha #4 — Missing admin peer dependencies. Strapi v5's build step requires
react,react-dom,react-router-dom, andstyled-componentslisted explicitly in yourpackage.json, or it will try to install them at build time inside the read-only container and fail.strapi-app/config/database.jsmodule.exports = ({ env }) => ({ connection: { client: env('DATABASE_CLIENT', 'sqlite'), connection: { host: env('DATABASE_HOST', '127.0.0.1'), port: env.int('DATABASE_PORT', 5432), database: env('DATABASE_NAME', 'strapi'), user: env('DATABASE_USERNAME', 'strapi'), password: env('DATABASE_PASSWORD', ''), ssl: env.bool('DATABASE_SSL', false), filename: env('DATABASE_FILENAME', '.tmp/data.db'), }, pool: { min: 2, max: 10 }, acquireConnectionTimeout: 60000, useNullAsDefault: true, }, });strapi-app/config/server.jsmodule.exports = ({ env }) => ({ host: env('HOST', '0.0.0.0'), port: env.int('PORT', 8000), url: env('CLOUDRON_APP_ORIGIN', ''), app: { keys: env.array('APP_KEYS'), }, });Gotcha #5 — Always set
urltoCLOUDRON_APP_ORIGIN. Cloudron injects this as the full public HTTPS URL (e.g.https://cms.example.com). Without it, media library URLs and admin panel asset paths will be wrong.strapi-app/config/admin.jsmodule.exports = ({ env }) => ({ auth: { secret: env('ADMIN_JWT_SECRET'), }, apiToken: { salt: env('API_TOKEN_SALT'), }, transfer: { token: { salt: env('TRANSFER_TOKEN_SALT'), }, }, secrets: { encryptionKey: env('ADMIN_ENCRYPTION_KEY'), }, flags: { nps: env.bool('FLAG_NPS', false), promoteEE: env.bool('FLAG_PROMOTE_EE', false), }, });
Step 4 — Write the Cloudron packaging files
CloudronManifest.json{ "id": "com.yourorg.strapi", "title": "Strapi CMS", "author": "Your Name", "description": "Strapi v5 headless CMS", "tagline": "Open-source headless CMS", "version": "5.0.0", "healthCheckPath": "/_health", "httpPort": 8000, "addons": { "postgresql": {}, "localstorage": {} }, "manifestVersion": 2, "minBoxVersion": "8.0.0", "memoryLimit": 1073741824, "website": "https://strapi.io", "documentationUrl": "https://docs.strapi.io" }Gotcha #6 —
/_healthreturns HTTP 204, not 200. Cloudron accepts any2xxso this is fine. Don't use/as your health check path — Strapi's root redirects to the admin panel and can be slow on cold start, causing false health check failures.Gotcha #7 — Memory limit is critical. The default Cloudron custom app limit is 256 MB. Strapi needs at least 1 GB. Set
"memoryLimit": 1073741824(1 GB in bytes) or Strapi will be OOM-killed silently.Dockerfile# syntax=docker/dockerfile:1 FROM docker.io/cloudron/base:5.0.0 RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/cache/apt /var/lib/apt/lists RUN npm install -g pnpm RUN mkdir -p /app/code /app/data/uploads /app/data/database WORKDIR /app/code COPY strapi-app/ /app/code/ RUN pnpm install --frozen-lockfile=false RUN pnpm approve-builds --yes || true RUN pnpm rebuild better-sqlite3 sharp @swc/core esbuild RUN NODE_ENV=production pnpm run build RUN rm -rf /app/code/.tmp /app/code/public/uploads /app/code/database \ && ln -sf /run/strapi-tmp /app/code/.tmp \ && ln -sf /app/data/uploads /app/code/public/uploads \ && ln -sf /app/data/database /app/code/database COPY start.sh /app/code/start.sh RUN chmod +x /app/code/start.sh CMD ["/app/code/start.sh"]Gotcha #8 — Cloudron's filesystem is read-only at runtime. Only
/run(ephemeral),/app/data(persistent, backed up), and/tmpare writable. Strapi needs to write to three locations — all must be symlinked in the Dockerfile:.tmp/→/run/strapi-tmp(scratch space, ephemeral is fine)public/uploads/→/app/data/uploads(media files, must persist)database/→/app/data/database(migration tracking, must persist)
Gotcha #9 — Build the admin panel in the Dockerfile, not at startup.
pnpm run buildcompiles Strapi's React admin panel (~10 seconds). If you defer this tostart.sh, the container takes 3–5 minutes to start and health checks time out.Gotcha #10 — Native modules need explicit approval with pnpm. pnpm's security model blocks build scripts by default. Run
pnpm approve-buildsinteractively during development, or pass--yesin the Dockerfile. The modules that need this arebetter-sqlite3,sharp,@swc/core, andesbuild.Gotcha #11 — Node.js version in the base image.
cloudron/base:5.0.0(Ubuntu 24.04) ships a system Node.js that may not be v20/22 LTS. Install explicitly via NodeSource to guarantee Strapi v5 compatibility.start.sh#!/bin/bash set -eu echo "==> Creating runtime directories..." mkdir -p /run/strapi-tmp mkdir -p /app/data/uploads mkdir -p /app/data/database/migrations echo "==> Checking secrets..." if [[ ! -f /app/data/.secrets ]]; then echo "==> Generating secrets for first run..." cat > /app/data/.secrets << SECRETS export APP_KEYS="$(openssl rand -base64 32),$(openssl rand -base64 32),$(openssl rand -base64 32),$(openssl rand -base64 32)" export API_TOKEN_SALT="$(openssl rand -base64 32)" export ADMIN_JWT_SECRET="$(openssl rand -base64 32)" export ADMIN_ENCRYPTION_KEY="$(openssl rand -base64 32)" export TRANSFER_TOKEN_SALT="$(openssl rand -base64 32)" export JWT_SECRET="$(openssl rand -base64 32)" SECRETS fi source /app/data/.secrets echo "==> Mapping database environment..." export DATABASE_CLIENT=postgres export DATABASE_HOST="${CLOUDRON_POSTGRESQL_HOST}" export DATABASE_PORT="${CLOUDRON_POSTGRESQL_PORT}" export DATABASE_NAME="${CLOUDRON_POSTGRESQL_DATABASE}" export DATABASE_USERNAME="${CLOUDRON_POSTGRESQL_USERNAME}" export DATABASE_PASSWORD="${CLOUDRON_POSTGRESQL_PASSWORD}" export DATABASE_SSL=false export PORT=8000 export HOST=0.0.0.0 export NODE_ENV=production export STRAPI_TELEMETRY_DISABLED=true echo "==> Starting Strapi on port 8000..." exec /usr/local/bin/gosu cloudron:cloudron /app/code/node_modules/.bin/strapi startGotcha #12 — Cryptographic secrets must survive restarts. Strapi's
APP_KEYS,ADMIN_JWT_SECRET, etc. are used to sign sessions and tokens. If they regenerate on every container restart, all admin sessions are invalidated. Generate them once on first boot and persist to/app/data/.secrets.Gotcha #13 —
CLOUDRON_POSTGRESQL_*vars change on every restart. Never hardcode them. Always read from environment instart.shand re-export to Strapi's expected variable names.Gotcha #14 — Run Strapi as the
cloudronuser, not root. Thestart.shscript runs as root (needed formkdir), but always usegosu cloudron:cloudronto drop privileges before the app process starts.Gotcha #15 — Call the binary directly, not via
node. pnpm creates shell wrapper scripts innode_modules/.bin/. Callingnode node_modules/.bin/strapipasses a shell script to the Node.js interpreter, causing aSyntaxError: missing ) after argument list. Call it directly:/app/code/node_modules/.bin/strapi start..dockerignore.git .gitignore *.md strapi-app/.tmp strapi-app/node_modules strapi-app/build strapi-app/.cache strapi-app/.pnpm-store
Step 5 — Build the Docker image locally
Why build locally? Cloudron's server-side build (
cloudron installwithout--image) uploads a source archive and builds on the server. For Strapi, thenode_modules+ admin panel build produces an archive too large (~700 MB) for the upload timeout. Building locally and pushing to a registry bypasses this entirely.cd ~/projects/strapi-cloudron # Build (takes 10-15 mins on first run — better-sqlite3 compiles native C++) podman build --no-cache -t docker.io/YOURUSERNAME/strapi-cloudron:5.38.0 . # Push to Docker Hub podman push docker.io/YOURUSERNAME/strapi-cloudron:5.38.0Tip: The
--no-cacheflag ensures a clean build. Omit it on subsequent builds to use layer caching and speed things up (only use--no-cachewhen you changepackage.jsonor the Dockerfile itself).
Step 6 — Install on Cloudron
cloudron install \ --image docker.io/YOURUSERNAME/strapi-cloudron:5.38.0 \ --location cms.yourdomain.comTail logs while it starts (first boot runs DB migrations — allow ~60 seconds):
cloudron logs -f --app cms.yourdomain.comA successful start looks like:
==> Creating runtime directories... ==> Generating secrets for first run... ==> Mapping database environment... ==> Starting Strapi on port 8000... [2026-03-05 13:04:59.990] info: Strapi started successfully [2026-03-05 13:05:00.170] http: GET /_health (29 ms) 204
Step 7 — Create your admin account
Visit
https://cms.yourdomain.com/adminand register your first administrator. This must be done manually on first install — Cloudron's no-setup-screen requirement doesn't apply here since Strapi's first-run setup is minimal (just a name, email, and password).
Updating Strapi
When a new Strapi version is released, update
package.json, rebuild and push with a new tag, then:cloudron update \ --image docker.io/YOURUSERNAME/strapi-cloudron:NEW_VERSION \ --app cms.yourdomain.comCloudron automatically backs up the app (including the PostgreSQL database) before updating.
Path B — Packaging for the Official Cloudron App Store
If you want to submit Strapi to the App Store, additional steps are required beyond a working personal installation:
1. Use a proper manifest ID
Theidfield must be a reverse-domain identifier:io.strapi.cloudronapp— coordinate with the Cloudron team on naming before submitting.2. Add a
checklistfor post-install guidance"checklist": { "first-admin": { "sso": false, "message": "Create your first administrator at /admin" } }3. Add an icon
Place a 256×256 PNG atlogo.pngin your package root.4. Write browser tests
Cloudron requires automated install/backup/restore/update tests. See existing packages at git.cloudron.io/cloudron for examples.5. Post in the App Wishlist forum first
Before packaging, leave a note in the App Wishlist category. The community may have already started, and the Cloudron team can advise on naming and requirements before you invest time.6. Host the packaging code in its own repo
Keep the Cloudron packaging repo (Dockerfile, manifest, start.sh) separate from the Strapi app code repo.7. Open Source licence required
The packaging code must be MIT, GPL, BSD or similar. Your Strapi content and configuration can remain private.
Complete Gotcha Reference
# Problem Solution 1 Podman OCI SHELL warning Add # syntax=docker/dockerfile:1or use--format docker2 CLI runs on wrong machine Always run cloudroncommands locally, not on server3 @strapi/plugin-i18n@^5.0.0not foundPackage absorbed into core — remove from dependencies 4 Missing admin peer deps at build time Add react,react-dom,react-router-dom,styled-componentstopackage.json5 Wrong media URLs Set url: env('CLOUDRON_APP_ORIGIN')inconfig/server.js6 Health check fails Use /_health(returns 204), not/7 OOM silent kill Set memoryLimit: 1073741824(1 GB) in manifest8 Read-only filesystem crashes Symlink .tmp,public/uploads,databaseto/runor/app/data9 Health check timeout on slow start Build admin panel in Dockerfile, not start.sh10 pnpm blocks native module builds Run pnpm approve-buildsor--yesin Dockerfile11 Wrong Node.js version Install Node 22 LTS via NodeSource explicitly in Dockerfile 12 Sessions invalidated on restart Persist secrets to /app/data/.secrets, generate once13 DB connection fails after restart Map CLOUDRON_POSTGRESQL_*vars instart.shevery time14 Running as root Use gosu cloudron:cloudronbefore exec15 SyntaxError: missing ) after argument listDon't call node node_modules/.bin/strapi— call the binary directly16 Upload timeout on large source archives Build locally with podman/docker, push to registry, use --imageflag
Final file listing
strapi-cloudron/ ├── .dockerignore ├── CloudronManifest.json ├── Dockerfile ├── start.sh └── strapi-app/ ├── package.json ├── config/ │ ├── admin.js │ ├── database.js │ ├── server.js │ └── env/ │ └── production/ ├── src/ │ └── api/ └── public/ └── uploads/
Guide written March 2026. Tested on Strapi v5.38.0, Cloudron 8.3, base image 5.0.0 (Ubuntu 24.04), Node.js 22.14.0, pnpm 10.30.2, Podman on Bazzite KDE.
If you run into issues not covered here, post in this thread and we'll update the guide.
-
AI dev agent ? Which one ?
Just interested, doesn’t seem familiar. -
AI dev agent ? Which one ?
Just interested, doesn’t seem familiar.@timconsidine This one was largely Claude Opus 4.6 extended
The latest updates in Cloudron really helped.
-
@timconsidine This one was largely Claude Opus 4.6 extended
The latest updates in Cloudron really helped.
The latest updates in Cloudron really helped.
Presumably especially the Skills?
-
The latest updates in Cloudron really helped.
Presumably especially the Skills?
Strapi: Strapi (Node.js headless CMS) loves the latest Node 24, MongoDB 8, and Redis. The custom build feature makes it dead simple to package Strapi + custom plugins, database migrations, and even a Svelte/Astro frontend in one app. You get Cloudron’s one-click updates, backups (including DB), SSO, domains, and isolation — while still running the very latest Strapi version with your own custom admin UI or API extensions. Community Strapi packages have existed in the past; now they’re officially encouraged and auto-updating.
Main Improvements & Benefits in 9.1 / 9.1.2
These focus on making Cloudron far more developer-friendly for custom and modern web apps:Custom app build & deploy (biggest new feature): You can now git clone any app package repository and run cloudron install directly on your server. Cloudron builds the Docker image on-the-fly from source. Your app’s full source code is included in backups and can be rebuilt/restored automatically. No more manual Docker packaging or third-party tools needed.
Community Apps: Anyone can publish apps via a simple CloudronVersions.json file hosted anywhere (GitHub, GitLab, etc.). Install them with one click from any URL, and Cloudron tracks the upstream repo for automatic updates. This opens up the ecosystem massively — no waiting for official Cloudron approval.
Updated runtimes:
Node.js 24.x (huge for modern JS/TS stacks)
MongoDB 8
Redis 8.4UX & operational wins:
Much better progress reporting (percentage complete + elapsed/estimated time) for backups and app installs.
Improved notifications view, event log, and backup verification UI.
Faster backups (up to 100× in some cases, carried over from v9).Security:
Native Passkey support (works with YubiKey, Bitwarden, browser passkeys, etc.).
ACME ARI for better certificate management.Overall benefits: Easier self-hosting of custom apps, faster iteration, automatic updates/backups/SSL/domains/isolation, and dramatically reduced maintenance compared to raw Docker or VPS setups.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login