Cloudron makes it easy to run web apps like WordPress, Nextcloud, GitLab on your server. Find out more or install now.


Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Bookmarks
  • Search
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
Brand Logo

Cloudron Forum

Apps - Status | Demo | Docs | Install
  1. Cloudron Forum
  2. App Packaging & Development
  3. 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

Scheduled Pinned Locked Moved App Packaging & Development
strapicustom-appheadless-cmsnodejspostgresql
5 Posts 3 Posters 44 Views 3 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • L Offline
    L Offline
    LoudLemur
    wrote last edited by
    #1

    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": 1073741824 in your manifest.


    🐧 Bazzite / Fedora Atomic Desktop users

    Bazzite (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.io
    

    Gotcha #1 — Podman OCI format warning: You'll see SHELL is not supported for OCI image format warnings during builds. These are harmless — add # syntax=docker/dockerfile:1 as the first line of your Dockerfile to suppress them, or use podman build --format docker flag.


    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/uploads
    

    Your 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 connection
    

    Gotcha #2 — CLI runs on your local machine, not the server. Run all cloudron commands 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-i18n no 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, and styled-components listed explicitly in your package.json, or it will try to install them at build time inside the read-only container and fail.

    strapi-app/config/database.js

    module.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.js

    module.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 url to CLOUDRON_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.js

    module.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 — /_health returns HTTP 204, not 200. Cloudron accepts any 2xx so 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 /tmp are 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 build compiles Strapi's React admin panel (~10 seconds). If you defer this to start.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-builds interactively during development, or pass --yes in the Dockerfile. The modules that need this are better-sqlite3, sharp, @swc/core, and esbuild.

    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 start
    

    Gotcha #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 in start.sh and re-export to Strapi's expected variable names.

    Gotcha #14 — Run Strapi as the cloudron user, not root. The start.sh script runs as root (needed for mkdir), but always use gosu cloudron:cloudron to drop privileges before the app process starts.

    Gotcha #15 — Call the binary directly, not via node. pnpm creates shell wrapper scripts in node_modules/.bin/. Calling node node_modules/.bin/strapi passes a shell script to the Node.js interpreter, causing a SyntaxError: 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 install without --image) uploads a source archive and builds on the server. For Strapi, the node_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.0
    

    Tip: The --no-cache flag ensures a clean build. Omit it on subsequent builds to use layer caching and speed things up (only use --no-cache when you change package.json or the Dockerfile itself).


    Step 6 — Install on Cloudron

    cloudron install \
      --image docker.io/YOURUSERNAME/strapi-cloudron:5.38.0 \
      --location cms.yourdomain.com
    

    Tail logs while it starts (first boot runs DB migrations — allow ~60 seconds):

    cloudron logs -f --app cms.yourdomain.com
    

    A 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/admin and 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.com
    

    Cloudron 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
    The id field must be a reverse-domain identifier: io.strapi.cloudronapp — coordinate with the Cloudron team on naming before submitting.

    2. Add a checklist for post-install guidance

    "checklist": {
      "first-admin": {
        "sso": false,
        "message": "Create your first administrator at /admin"
      }
    }
    

    3. Add an icon
    Place a 256×256 PNG at logo.png in 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:1 or use --format docker
    2 CLI runs on wrong machine Always run cloudron commands locally, not on server
    3 @strapi/plugin-i18n@^5.0.0 not found Package absorbed into core — remove from dependencies
    4 Missing admin peer deps at build time Add react, react-dom, react-router-dom, styled-components to package.json
    5 Wrong media URLs Set url: env('CLOUDRON_APP_ORIGIN') in config/server.js
    6 Health check fails Use /_health (returns 204), not /
    7 OOM silent kill Set memoryLimit: 1073741824 (1 GB) in manifest
    8 Read-only filesystem crashes Symlink .tmp, public/uploads, database to /run or /app/data
    9 Health check timeout on slow start Build admin panel in Dockerfile, not start.sh
    10 pnpm blocks native module builds Run pnpm approve-builds or --yes in Dockerfile
    11 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 once
    13 DB connection fails after restart Map CLOUDRON_POSTGRESQL_* vars in start.sh every time
    14 Running as root Use gosu cloudron:cloudron before exec
    15 SyntaxError: missing ) after argument list Don't call node node_modules/.bin/strapi — call the binary directly
    16 Upload timeout on large source archives Build locally with podman/docker, push to registry, use --image flag

    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.

    1 Reply Last reply
    1
    • timconsidineT Online
      timconsidineT Online
      timconsidine
      App Dev
      wrote last edited by
      #2

      AI dev agent ? Which one ?
      Just interested, doesn’t seem familiar.

      Indie app dev, scratching my itches, lover of Cloudron PaaS

      L 1 Reply Last reply
      1
      • timconsidineT timconsidine

        AI dev agent ? Which one ?
        Just interested, doesn’t seem familiar.

        L Offline
        L Offline
        LoudLemur
        wrote last edited by
        #3

        @timconsidine This one was largely Claude Opus 4.6 extended

        The latest updates in Cloudron really helped.

        jdaviescoatesJ 1 Reply Last reply
        1
        • L LoudLemur

          @timconsidine This one was largely Claude Opus 4.6 extended

          The latest updates in Cloudron really helped.

          jdaviescoatesJ Offline
          jdaviescoatesJ Offline
          jdaviescoates
          wrote last edited by
          #4

          @LoudLemur said:

          The latest updates in Cloudron really helped.

          Presumably especially the Skills?

          I use Cloudron with Gandi & Hetzner

          L 1 Reply Last reply
          0
          • jdaviescoatesJ jdaviescoates

            @LoudLemur said:

            The latest updates in Cloudron really helped.

            Presumably especially the Skills?

            L Offline
            L Offline
            LoudLemur
            wrote last edited by
            #5

            @jdaviescoates

            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.4

            UX & 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.

            1 Reply Last reply
            0

            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
            Reply
            • Reply as topic
            Log in to reply
            • Oldest to Newest
            • Newest to Oldest
            • Most Votes


            • Login

            • Don't have an account? Register

            • Login or register to search.
            • First post
              Last post
            0
            • Categories
            • Recent
            • Tags
            • Popular
            • Bookmarks
            • Search