Adding new Community Recipes for Container Apps

Continuing the discussion from Welcome the new Virtualmin Podman Plugin:

I have a recipe that I want to share with Virtualmin, I have been working and testing with my local recipe and it’s running great. It’s a CRM. What are the steps for onboarding?

Hello,

You can post it here explaining the recipe, and I will review it and see if we can add it. Or you can request an app’s recipe to be added, and I will add it myself.

LightserveCRM — Virtualmin Container Apps recipe

Looking for feedback on a recipe we would like in the catalog.

What it is: a field-service CRM (contacts, jobs, invoices, Book Now, customer portal, SMS). One company per domain. First visit is a setup wizard. Full feature list: Features | LightserveCRM

Shape: same as packaged apps (Espo-style): secrets, {{db_password}}, role / suffix / ready, one public application.

Pod (rootless):

db — Postgres 16 (private)
api — Rust backend (private, :3200)
ui — SvelteKit (private, :5280)
web — Caddy on 8080 (only public process)
Caddy sends /api* to the API and everything else to the UI. We use 8080 so rootless Podman does not bind :80.

Keep-data: after pg_isready, DB initialize resets the Postgres password to the current {{db_password}} before api starts (initialize_before_dependents). That way a redeploy with new secrets still talks to the existing volume.

Updates: pull/reinstall api and ui only (:latest). Leave db alone. Schema migrations run when the API starts.

Recipe file: lightservecrm.json below:

{
  "id": "lightservecrm",
  "name": "LightserveCRM",
  "version": "0.2.2",
  "description": "Field-service CRM (contacts, jobs, invoices, portal, SMS) with private PostgreSQL. UI and API are published together on the domain root via a small reverse proxy. First visit completes company setup in the browser.",
  "categories": ["crm", "business"],
  "app_port": 8080,
  "default_variant": "postgres",
  "required_runtime": "rootless",
  "required_available_memory_bytes": 1073741824,
  "required_free_disk_bytes": 4294967296,
  "required_quota_bytes": 4294967296,
  "secrets": [
    "db_password"
  ],
  "variants": [
    {
      "id": "postgres",
      "label_key": "recipes_db_postgresql",
      "components": [
        {
          "role": "database",
          "suffix": "db",
          "image": "docker.io/library/postgres:16-alpine",
          "envs": [
            "POSTGRES_USER=postgres",
            "POSTGRES_PASSWORD={{db_password}}",
            "POSTGRES_DB=lightservecrm"
          ],
          "volumes": [
            [
              "database",
              "/var/lib/postgresql/data"
            ]
          ],
          "ready": [
            "pg_isready",
            "-U",
            "postgres",
            "-d",
            "lightservecrm"
          ],
          "initialize": [
            "psql",
            "-U",
            "postgres",
            "-d",
            "lightservecrm",
            "-v",
            "ON_ERROR_STOP=1",
            "-c",
            "ALTER USER postgres WITH PASSWORD $ls${{db_password}}$ls$;"
          ],
          "initialize_before_dependents": 1
        },
        {
          "role": "service",
          "suffix": "api",
          "image": "docker.io/texasman35/lightservecrm-backend:latest",
          "envs": [
            "DATABASE_URL=postgres://postgres:{{db_password}}@127.0.0.1:5432/lightservecrm",
            "PORT=3200",
            "RUST_LOG=lightservecrm_backend=info,axum=info",
            "CORS_ORIGIN={{url_no_trailing}}",
            "APP_URL={{url_no_trailing}}",
            "API_PUBLIC_URL={{url_no_trailing}}",
            "EMAIL_ASSETS_DIR=/var/lib/lightservecrm/email-assets",
            "JOB_MEDIA_DIR=/var/lib/lightservecrm/job-media"
          ],
          "volumes": [
            [
              "email-assets",
              "/var/lib/lightservecrm/email-assets"
            ],
            [
              "job-media",
              "/var/lib/lightservecrm/job-media"
            ]
          ]
        },
        {
          "role": "service",
          "suffix": "ui",
          "image": "docker.io/texasman35/lightservecrm-frontend:latest",
          "envs": [
            "NODE_ENV=production",
            "PORT=5280",
            "HOST=0.0.0.0",
            "PUBLIC_API_URL={{url_no_trailing}}"
          ]
        },
        {
          "role": "application",
          "suffix": "web",
          "image": "docker.io/library/caddy:2-alpine",
          "entrypoint": "/bin/sh",
          "command": "-c 'echo OjgwODAgewoJaGFuZGxlIC9hcGkqIHsKCQlyZXZlcnNlX3Byb3h5IDEyNy4wLjAuMTozMjAwCgl9CgloYW5kbGUgewoJCXJldmVyc2VfcHJveHkgMTI3LjAuMC4xOjUyODAKCX0KfQ== | base64 -d > /tmp/Caddyfile && exec caddy run --config /tmp/Caddyfile'",
          "ready": [
            "wget",
            "-q",
            "-O",
            "-",
            "http://127.0.0.1:8080/"
          ],
          "ready_timeout": 300
        }
      ]
    }
  ]
}

We should probably have a recipe index, that allows third-party recipes via a link to the maintainers github repo or similar, rather than trying to include everything.

My thoughts exactly, this should also be along with the modules catalogue, I know I have mentioned the later before but it is totally relevant here :smiley:

There should be comments available or maybe a link to a forum thread to allow users to discuss any issues with the recipe or app.

1 Like

I’m very grateful I was able to play with my own sandbox recipe version before posting mine here. I love the introduction of the recipes and containers. This is a huge boon for Virtualmin users to have a nice centralized place for containers and view logs. I love it. Good job all around guys.

1 Like

Hello,
I was just wondering if you had a chance to implement this?

I had a chance to take a look, but as @Joe mentioned, it’s unlikely we’ll be able to include it in the Virtualmin Podman plugin package, and indeed the better option would be to create a recipe index in the future.

Please give this fixed recipe a try and let me know if that works for you:

{
  "id": "lightservecrm",
  "name": "LightserveCRM",
  "version": "0.2.80",
  "description": "Field-service CRM (contacts, jobs, invoices, portal, SMS) with private PostgreSQL. UI and API are published together on the domain root via a small reverse proxy. First visit completes company setup in the browser.",
  "categories": [
    "crm",
    "business"
  ],
  "app_port": 8080,
  "default_variant": "postgres",
  "required_runtime": "rootless",
  "required_available_memory_bytes": 1073741824,
  "required_free_disk_bytes": 4294967296,
  "required_quota_bytes": 4294967296,
  "secrets": [
    "db_password"
  ],
  "variants": [
    {
      "id": "postgres",
      "label_key": "recipes_db_postgresql",
      "components": [
        {
          "role": "database",
          "suffix": "db",
          "image": "docker.io/library/postgres:16-alpine",
          "envs": [
            "POSTGRES_USER=postgres",
            "POSTGRES_PASSWORD={{db_password}}",
            "POSTGRES_DB=lightservecrm"
          ],
          "volumes": [
            [
              "database",
              "/var/lib/postgresql/data"
            ]
          ],
          "ready": [
            "pg_isready",
            "-h",
            "127.0.0.1",
            "-U",
            "postgres",
            "-d",
            "lightservecrm"
          ],
          "initialize": [
            "psql",
            "-U",
            "postgres",
            "-d",
            "lightservecrm",
            "-v",
            "ON_ERROR_STOP=1",
            "-c",
            "ALTER USER postgres WITH PASSWORD $ls${{db_password}}$ls$;"
          ],
          "initialize_before_dependents": 1
        },
        {
          "role": "service",
          "suffix": "api",
          "image": "docker.io/texasman35/lightservecrm-backend:0.2.80",
          "envs": [
            "DATABASE_URL=postgres://postgres:{{db_password}}@127.0.0.1:5432/lightservecrm",
            "PORT=3200",
            "RUST_LOG=lightservecrm_backend=info,axum=info",
            "CORS_ORIGIN={{url_no_trailing}}",
            "APP_URL={{url_no_trailing}}",
            "API_PUBLIC_URL={{url_no_trailing}}",
            "EMAIL_ASSETS_DIR=/var/lib/lightservecrm/email-assets",
            "JOB_MEDIA_DIR=/var/lib/lightservecrm/job-media",
            "TEAM_CHAT_MEDIA_DIR=/var/lib/lightservecrm/team-chat",
            "HRM_DOCS_DIR=/var/lib/lightservecrm/hrm-docs"
          ],
          "volumes": [
            [
              "email-assets",
              "/var/lib/lightservecrm/email-assets"
            ],
            [
              "job-media",
              "/var/lib/lightservecrm/job-media"
            ],
            [
              "team-chat",
              "/var/lib/lightservecrm/team-chat"
            ],
            [
              "hrm-docs",
              "/var/lib/lightservecrm/hrm-docs"
            ]
          ]
        },
        {
          "role": "service",
          "suffix": "ui",
          "image": "docker.io/texasman35/lightservecrm-frontend:0.2.80",
          "envs": [
            "NODE_ENV=production",
            "PORT=5280",
            "HOST=0.0.0.0",
            "PUBLIC_API_URL={{url_no_trailing}}"
          ]
        },
        {
          "role": "application",
          "suffix": "web",
          "image": "docker.io/library/caddy:2-alpine",
          "entrypoint": "/bin/sh",
          "command": "-c 'echo OjgwODAgewoJaGFuZGxlIC9hcGkqIHsKCQlyZXZlcnNlX3Byb3h5IDEyNy4wLjAuMTozMjAwCgl9CgloYW5kbGUgewoJCXJldmVyc2VfcHJveHkgMTI3LjAuMC4xOjUyODAKCX0KfQ== | base64 -d > /tmp/Caddyfile && exec caddy run --config /tmp/Caddyfile'",
          "ready": [
            "/bin/sh",
            "-c",
            "wget -q -O /dev/null http://127.0.0.1:8080/api/v1/setup/status && wget -q -O /dev/null http://127.0.0.1:8080/"
          ],
          "ready_timeout": 300
        }
      ]
    }
  ],
  "architectures": [
    "amd64"
  ],
  "post_install": [
    "Open {{url}} to create the company and first administrator account.",
    "Self-hosting requires a LightserveCRM license after its trial period."
  ]
}

And here you can see what changed compared to your original recipe:

--- original/lightservecrm.json
+++ corrected/lightservecrm.json
@@ -3,3 +3,3 @@
   "name": "LightserveCRM",
-  "version": "0.2.2",
+  "version": "0.2.80",
   "description": "Field-service CRM (contacts, jobs, invoices, portal, SMS) with private PostgreSQL. UI and API are published together on the domain root via a small reverse proxy. First visit completes company setup in the browser.",
@@ -40,2 +40,4 @@
             "pg_isready",
+            "-h",
+            "127.0.0.1",
             "-U",
@@ -61,3 +63,3 @@
           "suffix": "api",
-          "image": "docker.io/texasman35/lightservecrm-backend:latest",
+          "image": "docker.io/texasman35/lightservecrm-backend:0.2.80",
           "envs": [
@@ -70,3 +72,5 @@
             "EMAIL_ASSETS_DIR=/var/lib/lightservecrm/email-assets",
-            "JOB_MEDIA_DIR=/var/lib/lightservecrm/job-media"
+            "JOB_MEDIA_DIR=/var/lib/lightservecrm/job-media",
+            "TEAM_CHAT_MEDIA_DIR=/var/lib/lightservecrm/team-chat",
+            "HRM_DOCS_DIR=/var/lib/lightservecrm/hrm-docs"
           ],
@@ -80,2 +84,10 @@
               "/var/lib/lightservecrm/job-media"
+            ],
+            [
+              "team-chat",
+              "/var/lib/lightservecrm/team-chat"
+            ],
+            [
+              "hrm-docs",
+              "/var/lib/lightservecrm/hrm-docs"
             ]
@@ -86,3 +98,3 @@
           "suffix": "ui",
-          "image": "docker.io/texasman35/lightservecrm-frontend:latest",
+          "image": "docker.io/texasman35/lightservecrm-frontend:0.2.80",
           "envs": [
@@ -101,7 +113,5 @@
           "ready": [
-            "wget",
-            "-q",
-            "-O",
-            "-",
-            "http://127.0.0.1:8080/"
+            "/bin/sh",
+            "-c",
+            "wget -q -O /dev/null http://127.0.0.1:8080/api/v1/setup/status && wget -q -O /dev/null http://127.0.0.1:8080/"
           ],
@@ -111,2 +121,9 @@
     }
+  ],
+  "architectures": [
+    "amd64"
+  ],
+  "post_install": [
+    "Open {{url}} to create the company and first administrator account.",
+    "Self-hosting requires a LightserveCRM license after its trial period."
   ]

That works very nicely. I get now why you put a specific version, so we can still pull latest on demand without any code change. slick.

1 Like