Compare commits

..
Author SHA1 Message Date
github-actions[bot] 9976e9bcc9 Update CHANGELOG.md 2026-11-13 13:48:08 +00:00
push-app-to-main[bot] 2c2265e44b Add marimo (ct) (#18672)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 14:47:44 +01:00
Rögl-Brunner Michel 38cccdd417 pr-script-test: run external installers, and catch failed installs on Incus
CloudPanel (DevScripts PR 2614) installs through upstream's install.sh,
which core runs only after asking; CI cannot answer, so set
var_external_installer=yes for the build and the update.

A failed install on Incus also showed as passed: with dev_mode=keep the
engine still prints "Completed Successfully", and the line that gives the
failure away names the container ci-9000 there, not 9000, so the check
for it never matched.
2026-11-13 14:45:05 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 09a1164445 Update CHANGELOG.md (#18703)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 13:26:56 +00:00
Rögl-Brunner Michel 32bd1440d2 pr-script-test: remove the temporary token probes
They found the cause: marimo-team has an IP allow list, so GitHub
refuses the token from the lab with a 403 that core read as a rate
limit. Fixed in core. The quota line stays.
2026-11-13 13:32:06 +01:00
Rögl-Brunner Michel f9693f2fd3 pr-script-test: probe what GitHub answers the token in the guest (temporary) 2026-11-13 13:25:23 +01:00
Rögl-Brunner Michel c8799b0bcb pr-script-test: log the token length each shell of the update sees (temporary) 2026-11-13 13:20:01 +01:00
Rögl-Brunner Michel 0033568ca5 pr-script-test: log which GitHub calls in the guest carry the token (temporary) 2026-11-13 13:14:04 +01:00
Rögl-Brunner Michel d887fb8e95 pr-script-test: probe whether the guest profile clears the token 2026-11-13 13:08:55 +01:00
Rögl-Brunner Michel fe4e449b2f pr-script-test: probe the token inside the guest, not on the host
On the host the job token reads another repo's release fine (200), yet
installs and updates in the guest hit the anonymous limit. Probe there,
just before the update. Also drops the /tmp file named after $$.
2026-11-13 13:02:11 +01:00
Rögl-Brunner Michel cd4b7aa63d pr-script-test: probe whether the job token can read another repo's release 2026-11-13 12:56:10 +01:00
Rögl-Brunner Michel b80d038846 pr-script-test: log the GitHub API quota, with and without the token
marimo (ProxmoxVE PR 18672) hit the rate limit in its update although the
job token is passed as GITHUB_TOKEN there. Log what both the token and the
lab address have left at the start and when an update hits the limit.
2026-11-13 12:49:35 +01:00
Rögl-Brunner Michel 9368f43aa9 pr-script-test: the label builds only for write access or a bot
Triage can set labels too; /test needs write, so the label does as well.
The merge workflow sets "Ready For Testing" through an app, which passes.
2026-11-13 12:44:20 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d03f124259 Update CHANGELOG.md (#18701)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 11:41:57 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] cd685377dc Update CHANGELOG.md (#18700)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 11:41:28 +00:00
push-app-to-main[bot] 58310699e7 Add goatcounter (ct) (#18662)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 12:41:15 +01:00
push-app-to-main[bot] f71c4c2ee1 Add unsloth (ct) (#18697)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 12:40:55 +01:00
Rögl-Brunner Michel c8661ac328 pr-script-test: build the app when an issue gets "Ready For Testing"
The label on a script issue now starts the same build as `/test <app>`
in that issue: the app from the default branch. The app is the issue
form's "Script path", else "Name of the Script" as a slug (the rule of
pr-merged-ready-for-testing.yml). tools/ issues are skipped. Any other
label gets a group of its own, like a comment that is not a /test.
2026-11-13 12:40:49 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 373b564c29 Update CHANGELOG.md (#18698)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 11:30:41 +00:00
push-app-to-main[bot] e5b615e27d Add colanode (ct) (#18685)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 12:30:02 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6bd95a405b Update CHANGELOG.md (#18696)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 11:29:16 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 462a72a40f Update CHANGELOG.md (#18695)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 11:28:45 +00:00
push-app-to-main[bot] 89032c7630 Add simplelogin (ct) (#18680)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 12:28:35 +01:00
push-app-to-main[bot] bd8ef8f543 Add nightingale (ct) (#18674)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 12:28:15 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 8e9a731afe Update CHANGELOG.md (#18694)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:58:32 +00:00
push-app-to-main[bot] e406c773ba Add temporal (ct) (#18670)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:57:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ce75d40091 Update CHANGELOG.md (#18693)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:51:53 +00:00
push-app-to-main[bot] 3a823384e9 Add trailbase (ct) (#18669)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:51:23 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 36c37028af Update CHANGELOG.md (#18692)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:47:33 +00:00
push-app-to-main[bot] b261ecf63a Add weaviate (ct) (#18671)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:46:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 08a66f4b26 Update CHANGELOG.md (#18691)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:41:48 +00:00
push-app-to-main[bot] 4c813f3ae7 Add spacebot (ct) (#18668)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:41:19 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 90ee3cef62 Update CHANGELOG.md (#18690)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:28:35 +00:00
push-app-to-main[bot] 6d95e87a75 Add new-api (ct) (#18667)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:28:06 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6520393d42 Update CHANGELOG.md (#18689)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:27:51 +00:00
push-app-to-main[bot] 269bd77d74 Add martin (ct) (#18666)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:27:25 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d3dadaf04c Update CHANGELOG.md (#18688)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:27:21 +00:00
push-app-to-main[bot] db323df14f Add langflow (ct) (#18665)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:26:49 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d0f7277ad9 Update CHANGELOG.md (#18687)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:24:18 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 5782a6c273 Update CHANGELOG.md (#18686)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:23:52 +00:00
push-app-to-main[bot] f7c630962a Add inngest (ct) (#18664)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:23:50 +01:00
push-app-to-main[bot] 3e12b796b7 Add hatchet (ct) (#18663)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:23:20 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4d00f20118 Update CHANGELOG.md (#18684)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 09:22:59 +00:00
push-app-to-main[bot] 96c8094109 Add engram (ct) (#18661)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 10:22:30 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 97eb111801 Update CHANGELOG.md (#18683)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:32:21 +00:00
push-app-to-main[bot] d13b503426 Add euro-office (ct) (#18660)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:31:52 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 8f8a1393e0 Update CHANGELOG.md (#18682)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:22:06 +00:00
push-app-to-main[bot] dd75297935 Add cypht (ct) (#18659)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:21:35 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] a57e899058 Update CHANGELOG.md (#18681)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:19:41 +00:00
push-app-to-main[bot] d7bda2e9b8 Add airconnect (ct) (#18653)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:19:12 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 0ff308a706 Update CHANGELOG.md (#18679)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:16:02 +00:00
push-app-to-main[bot] fc4f8e2ac5 Add bulwark (ct) (#18656)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:15:32 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] b107350c78 Update CHANGELOG.md (#18678)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:12:07 +00:00
push-app-to-main[bot] dd5be08eb4 Add convex (ct) (#18658)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:11:37 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d64c231777 Update CHANGELOG.md (#18677)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:07:59 +00:00
push-app-to-main[bot] e3bfc6e0c3 Add chroma (ct) (#18657)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 09:07:26 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 38be7c108b Update CHANGELOG.md (#18676)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 08:00:01 +00:00
push-app-to-main[bot] 61c8c48915 Add bugsink (ct) (#18655)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 08:59:30 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 0b345a0cda Update CHANGELOG.md (#18675)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 07:56:22 +00:00
push-app-to-main[bot] 2fbbc9c185 Add 9router (ct) (#18654)
Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
2026-11-13 08:55:56 +01:00
Rögl-Brunner Michel c9ba40bfdb pr-script-test: fail the URL check on bad answers, not on non-200
Warpgate answers 401 on its root, a login wall, and the check failed it
(DevScripts issue 2537). Any answer now counts as the app being up,
except no answer, 404/410, a 5xx or a redirect loop. Same rule for VMs.
2026-11-13 08:32:29 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] b839ff1862 Update CHANGELOG.md (#18652)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 07:00:52 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] cb8a6fc098 Update CHANGELOG.md (#18651)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-13 07:00:28 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] bd07e8ad76 Update CHANGELOG.md (#18649)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:50:19 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6118f400e4 Update CHANGELOG.md (#18648)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:50:03 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 47e16b872d Update CHANGELOG.md (#18647)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:43:16 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] dc84871d1f Update CHANGELOG.md (#18646)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:43:06 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] a575466d04 Update CHANGELOG.md (#18645)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:42:49 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 2cb5956e8c Update CHANGELOG.md (#18644)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 18:42:19 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 453d1a4699 Update CHANGELOG.md (#18643)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 17:15:14 +00:00
CanbiZ (MickLesk) 851f9622ac OnlyOffice: change ct port to 8083 (#18642) 2026-11-12 18:14:42 +01:00
Rögl-Brunner Michel 5fb1baca11 pr-script-test: fit the check summary into 65535 bytes
GitHub limits a check run summary to 65535 bytes, not characters. The
shopware report (DevScripts PR 2478) had emoji-heavy logs and failed with
422. Cut by bytes at a character boundary and close an open log block.
2026-11-12 16:12:46 +01:00
Rögl-Brunner Michel 0d095e0435 pr-script-test: keep the version series when faking an old version
Version files were set back to 0.0.0-ci for the update test. Shopware's
update_script reads ~/.shopware to pin composer to the installed series
(~6.7.0), so it got ~0.0.0 and failed. Append -dev instead (6.7.16.0-dev):
the engine ranks it below the release, so the update still runs. Commit
hashes keep 0.0.0-ci.
2026-11-12 15:54:33 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] c4493b2671 Update CHANGELOG.md (#18641)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 14:48:41 +00:00
CanbiZ (MickLesk) 445ecd6da7 dotnetaspwebapi: start the service only once an app is deployed (#18639) 2026-11-12 15:47:59 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 1b21c6a585 Update CHANGELOG.md (#18638)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 13:53:20 +00:00
CanbiZ (MickLesk) 1d119abb23 Bump workflow actions to their current majors (#18637) 2026-11-12 14:52:54 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 0f00ec055f Update CHANGELOG.md (#18636)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 13:36:36 +00:00
Kristian SkovandCanbiZ 005f52a2ac Updated ASP.NET Web App script to Debian and adds a .Net selection (#18628)
* Updated scripts to Debian and adds a .Net selection

* Uses `fetch_and_deploy_from_url` instead of wget

* Cleanup

* Sets defaults for variables

* Removed unused error message for missing project name

* Unbound variable fix

* Improve .NET SDK installation script

Refactor .NET SDK installation process and update prompts.

* Update Microsoft repository keys and Nginx config

---------

Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-12 14:35:38 +01:00
Rögl-Brunner Michel 8144ce5d2a pr-script-test: no container noise in the journal note, real disk use on Incus 2026-11-12 12:31:25 +01:00
Rögl-Brunner Michel 548a6d92ec pr-script-test: upgrade test only in ProxmoxVE, and only for a changed update_script 2026-11-12 12:04:56 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 52b9a5df26 Update CHANGELOG.md (#18632)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 10:58:35 +00:00
CanbiZ (MickLesk) c870b5620d pocketbase-bot: apply website metadata requests from Accept/Decline boxes (#18631)
The website now posts each requested change as its own comment with the
command and two checkboxes. Ticking one edits the comment: Accept runs
the command as if a maintainer had typed it, Decline records the
refusal, and the issue closes once every change is decided. Only users
with write access can decide. app_vars is checked field by field before
it is saved.
2026-11-12 11:58:10 +01:00
Rögl-Brunner Michel b6123811be pr-script-test: upgrade path, second update, stability and resources 2026-11-12 11:42:26 +01:00
Rögl-Brunner Michel 3e9ce91252 pr-script-test: test the update of apps deployed from a branch too 2026-11-12 11:29:12 +01:00
Rögl-Brunner Michel 03ea5280e6 CI hosts: single-use, reset before every job 2026-11-12 11:11:19 +01:00
61 changed files with 4671 additions and 88 deletions

No files matched your search

+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 120
steps:
# The harness from the default branch, as in pr-script-test.yml.
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
sparse-checkout: .github/workflows/scripts
persist-credentials: false
@@ -106,7 +106,7 @@ jobs:
} >>"$GITHUB_STEP_SUMMARY"
- if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result-arm
path: ${{ runner.temp }}/result
+149 -4
View File
@@ -2,7 +2,7 @@ name: PocketBase Bot
on:
issue_comment:
types: [created]
types: [created, edited]
permissions:
issues: write
@@ -25,6 +25,9 @@ jobs:
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
EVENT_ACTION: ${{ github.event.action }}
SENDER: ${{ github.event.sender.login }}
PREVIOUS_BODY: ${{ github.event.changes.body.from }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || 'false' }}
REPO_OWNER: ${{ github.repository_owner }}
@@ -87,7 +90,7 @@ jobs:
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
const isPrComment = process.env.IS_PR_COMMENT === 'true';
const commentId = parseInt(process.env.COMMENT_ID, 10);
const actor = process.env.ACTOR;
let actor = process.env.ACTOR;
function ghRequest(path, method, body) {
const headers = {
@@ -278,7 +281,65 @@ jobs:
// Accept /pocketbase at the start of ANY line (leading whitespace ok),
// so the command works even when preceded by other text. Mid-sentence
// mentions and blockquoted ("> ...") examples are ignored.
const commentBody = process.env.COMMENT_BODY || '';
let commentBody = process.env.COMMENT_BODY || '';
// ── Metadata requests from the website ─────────────────────────────
// One comment per requested change, each with Accept/Decline boxes.
// Ticking one edits the comment; that edit is the only event handled.
const REQUEST_MARKER = '<!-- pb-request -->';
const DONE_MARKER = '<!-- pb-request:done -->';
const isRequest = commentBody.includes(REQUEST_MARKER);
let decidedBy = null;
if (process.env.EVENT_ACTION === 'edited' && !isRequest) process.exit(0);
if (isRequest) {
if (process.env.EVENT_ACTION !== 'edited' || commentBody.includes(DONE_MARKER)) process.exit(0);
const ticked = (body, word) =>
new RegExp('^[ \\t]*[-*] \\[[xX]\\] ' + word + '[ \\t]*\\r?$', 'm').test(body);
const previous = process.env.PREVIOUS_BODY || '';
const accept = ticked(commentBody, 'Accept') && !ticked(previous, 'Accept');
const decline = ticked(commentBody, 'Decline') && !ticked(previous, 'Decline');
if (accept === decline) process.exit(0);
const sender = process.env.SENDER;
const permRes = await ghRequest(
'/repos/' + owner + '/' + repo + '/collaborators/' + encodeURIComponent(sender) + '/permission'
);
const level = permRes.ok ? JSON.parse(permRes.body).permission : 'none';
if (level !== 'admin' && level !== 'write') {
await postComment(
'❌ **PocketBase Bot**: @' + sender + ' is not authorized to decide metadata requests.\n' +
'Only maintainers with write access can accept or decline them.'
);
process.exit(0);
}
const heading = (commentBody.match(/^### (.+)$/m) || [])[1] || 'this change';
const command = commentBody.match(/<!-- pb-command -->\r?\n([\s\S]*?)\r?\n<!-- \/pb-command -->/);
const decided = commentBody
.replace(/^[ \t]*[-*] \[[ xX]\] (Accept|Decline)[ \t]*\r?\n?/gm, '')
.replace(
REQUEST_MARKER,
'**' + (accept ? '✅ Accepted' : '❌ Declined') + '** by @' + sender + '\n\n' +
REQUEST_MARKER + '\n' + DONE_MARKER
);
const markRes = await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId, 'PATCH', { body: decided }
);
if (!markRes.ok) console.warn('Could not mark the request as decided:', markRes.body);
if (decline) {
await postComment('❌ **PocketBase Bot**: **' + heading + '** declined by @' + sender + '.');
process.exit(0);
}
if (!command) {
await postComment('❌ **PocketBase Bot**: no command found in the request for **' + heading + '**.');
process.exit(0);
}
commentBody = command[1];
decidedBy = sender;
actor = sender;
}
const cmdLine = commentBody
.split('\n')
.map(l => l.trim())
@@ -292,7 +353,7 @@ jobs:
// ── Permission check ───────────────────────────────────────────────
const association = process.env.ACTOR_ASSOCIATION;
if (association !== 'OWNER' && association !== 'MEMBER') {
if (!decidedBy && association !== 'OWNER' && association !== 'MEMBER') {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
@@ -957,6 +1018,35 @@ jobs:
);
process.exit(0);
}
if (fieldName === 'app_vars') {
const TYPES = ['text', 'password', 'number', 'boolean', 'select'];
const problems = [];
if (!Array.isArray(parsedJson)) problems.push('must be a JSON array');
else parsedJson.forEach(function (v, i) {
const at = 'entry ' + (i + 1) + ': ';
if (!v || typeof v !== 'object' || Array.isArray(v)) { problems.push(at + 'must be an object'); return; }
if (typeof v.name !== 'string' || !/^var_[a-z0-9_]+$/.test(v.name)) problems.push(at + '`name` must look like `var_example`');
if (typeof v.label !== 'string' || !v.label.trim()) problems.push(at + '`label` is required');
if (TYPES.indexOf(v.type) === -1) problems.push(at + '`type` must be one of ' + TYPES.join(', '));
if (v.default !== undefined && typeof v.default !== 'string') problems.push(at + '`default` must be a string');
if (v.type === 'select') {
const opts = v.options;
if (!Array.isArray(opts) || opts.length === 0 || opts.some(function (o) { return typeof o !== 'string' || !o; })) {
problems.push(at + 'a select needs `options`, a non-empty array of strings');
} else if (v.default !== undefined && opts.indexOf(v.default) === -1) {
problems.push(at + '`default` must be one of its options');
}
}
['required', 'secret'].forEach(function (k) {
if (v[k] !== undefined && typeof v[k] !== 'boolean') problems.push(at + '`' + k + '` must be true or false');
});
});
if (problems.length > 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `app_vars` was not saved.\n\n- ' + problems.join('\n- '));
process.exit(0);
}
}
setPayload[fieldName] = parsedJson;
} else {
setPayload[fieldName] = codeBlockValue;
@@ -1236,3 +1326,58 @@ jobs:
});
ENDSCRIPT
shell: bash
- name: Close a metadata request once every change is decided
if: always() && github.event.action == 'edited' && contains(github.event.comment.body, '<!-- pb-request -->')
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node << 'ENDSCRIPT'
(async function () {
const api = 'https://api.github.com/repos/' + process.env.REPO + '/issues/' + process.env.ISSUE_NUMBER;
const headers = {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json',
'User-Agent': 'pocketbase-bot'
};
const https = require('https');
function call(method, url, body) {
return new Promise(function (resolve, reject) {
const req = https.request(url, { method: method, headers: headers }, function (res) {
let data = '';
res.on('data', function (d) { data += d; });
res.on('end', function () { resolve({ ok: res.statusCode < 300, status: res.statusCode, body: data }); });
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
const requests = [];
for (let page = 1; page <= 10; page++) {
const res = await call('GET', api + '/comments?per_page=100&page=' + page);
if (!res.ok) throw new Error('Could not list comments: ' + res.status);
const comments = JSON.parse(res.body);
for (const c of comments) {
if ((c.body || '').includes('<!-- pb-request -->')) requests.push(c.body);
}
if (comments.length < 100) break;
}
const open = requests.filter(function (b) { return !b.includes('<!-- pb-request:done -->'); });
if (requests.length === 0 || open.length > 0) {
console.log(open.length + ' of ' + requests.length + ' changes still undecided.');
return;
}
const accepted = requests.filter(function (b) { return b.includes('✅ Accepted'); }).length;
const res = await call('PATCH', api, { state: 'closed', state_reason: accepted > 0 ? 'completed' : 'not_planned' });
if (!res.ok) throw new Error('Could not close the issue: ' + res.status);
console.log('Closed: ' + accepted + ' accepted, ' + (requests.length - accepted) + ' declined.');
})().catch(function (e) {
console.error(e.message || e);
process.exit(1);
});
ENDSCRIPT
shell: bash
+125 -20
View File
@@ -34,6 +34,10 @@ name: PR script test
on:
issue_comment:
types: [created]
# The "Ready For Testing" label on a script issue builds that app from the
# default branch, as `/test <app>` in the issue would.
issues:
types: [labeled]
# Every PR, see above. pull_request_target runs this file from main, never
# the PR's copy of it, and does not wait for "approve and run" on forks.
pull_request_target:
@@ -62,11 +66,13 @@ permissions: {}
# One queue per PR. A newer push replaces a run that is still waiting (its
# gate would skip anyway: the head moved), but never kills one that is
# building: a cancelled job leaves its guest behind until the sweeper. Any
# comment starts this workflow; one that is not a /test gets a group of its
# own, so it cannot push a waiting test out of the queue.
# comment and any label starts this workflow; one that is not a /test (or
# not the "Ready For Testing" label) gets a group of its own, so it cannot
# push a waiting test out of the queue.
concurrency:
group: >-
pr-script-test-${{ github.event_name == 'issue_comment' && !startsWith(github.event.comment.body, '/test')
pr-script-test-${{ ((github.event_name == 'issue_comment' && !startsWith(github.event.comment.body, '/test'))
|| (github.event_name == 'issues' && github.event.label.name != 'Ready For Testing'))
&& format('noop-{0}', github.run_id)
|| github.event.pull_request.number || github.event.issue.number || inputs.pr }}
cancel-in-progress: false
@@ -76,6 +82,7 @@ jobs:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/test')) ||
(github.event_name == 'issues' && github.event.label.name == 'Ready For Testing' && !github.event.issue.pull_request) ||
(github.event_name == 'pull_request_target' && !github.event.pull_request.draft)
runs-on: ubuntu-latest
permissions:
@@ -95,9 +102,10 @@ jobs:
trusted: ${{ steps.plan.outputs.trusted }}
kind: ${{ steps.plan.outputs.kind }}
platforms: ${{ steps.plan.outputs.platforms }}
base_sha: ${{ steps.plan.outputs.base_sha }}
steps:
- id: plan
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { owner, repo } = context.repo;
@@ -119,6 +127,35 @@ jobs:
} else if (context.eventName === 'workflow_dispatch') {
prNumber = Number(context.payload.inputs.pr);
requested = `${context.payload.inputs.sha || ''} ${context.payload.inputs.scripts || ''}`;
} else if (context.eventName === 'issues') {
// "Ready For Testing" on a script issue: the app the issue form
// names, as `/test <app>` would. Only people who can triage set a
// label, and the default branch is what gets built.
const issue = context.payload.issue;
prNumber = issue.number;
// Triage can label too; building needs write, as for /test. The
// merge workflow labels through an app, i.e. a bot.
const sender = context.payload.sender;
if (sender.type !== 'Bot') {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: sender.login });
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
core.notice(`${sender.login} has ${perm.permission} access; a test needs write`);
return;
}
}
const body = (issue.body || '').replace(/\r/g, '');
const field = name => {
const m = body.match(new RegExp(`^###[ \t]*${name}[ \t]*\n+([^\n#][^\n]*)`, 'm'));
return m && m[1].trim() !== '_No response_' ? m[1].trim() : '';
};
const type = field('Script Type');
if (type && !/^(CT|VM)\b/.test(type)) { core.notice(`${type}: only ct/ and vm/ scripts are built`); return; }
const path = field('Script path').replace(/^\/+/, '');
// Same slug rule as pr-merged-ready-for-testing.yml: the "Name of
// the Script" field, else the title, lowercased, spaces -> dashes.
const slug = (field('Name of the Script') || issue.title).trim().toLowerCase().replace(/\s+/g, '-');
requested = /^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(path) ? path : slug;
core.info(`Ready For Testing: building ${requested}`);
} else {
prNumber = context.payload.issue.number;
const body = context.payload.comment.body.trim();
@@ -139,7 +176,7 @@ jobs:
// `/test <app>` in an issue (not a PR) builds that app from the
// default branch as it is right now, e.g. to see whether a reported
// error reproduces. No PR, so no check run, no review, no approval.
const isIssue = context.eventName === 'issue_comment' && !context.payload.issue.pull_request;
const isIssue = ['issue_comment', 'issues'].includes(context.eventName) && !context.payload.issue.pull_request;
let pr = null, head, headSha;
if (isIssue) {
const { data: info } = await github.rest.repos.get({ owner, repo });
@@ -258,6 +295,31 @@ jobs:
}
const include = [];
// Upgrade test: install the ct/ script from the PR's base branch,
// then update with the PR, as a user who installed before the merge
// would. Only in ProxmoxVE, where users have installed it, and only
// when the PR changes update_script(): with the same update code an
// existing install updates exactly as it does today. New scripts
// have nothing to upgrade from. Proxmox only, once per script.
const UPGRADE_REPO = 'community-scripts/ProxmoxVE';
let baseSha = '';
if (!isIssue && `${owner}/${repo}` === UPGRADE_REPO) {
const { data: b } = await github.rest.repos.getBranch({ owner, repo, branch: pr.base.ref });
baseSha = b.commit.sha;
}
// From `function update_script() {` to the first `}` at the start of
// a line; null without one.
const updateCode = t => ((t || '').match(/^(function[ \t]+)?update_script[ \t]*\(\)[ \t]*\{[\s\S]*?^\}/m) || [null])[0];
const updateChanged = async (p, headText) => {
if (!baseSha) return false;
let baseText;
try {
const res = await github.rest.repos.getContent({ owner, repo, path: p, ref: baseSha });
baseText = Buffer.from(res.data.content || '', 'base64').toString('utf8');
} catch (e) { return false; }
const before = updateCode(baseText), after = updateCode(headText);
return before !== null && after !== null && before !== after;
};
const fetchScript = async p => {
try {
const res = await github.rest.repos.getContent({ owner: head.owner.login, repo: head.name, path: p, ref: headSha });
@@ -302,7 +364,11 @@ jobs:
platforms = platforms.filter(p => only.has(p));
}
for (const platform of platforms) {
for (const os of oses) include.push({ script, os, platform, arm64: platform === 'arm' ? arm : '', name: `${os ? `${name}-${os}` : name}-${platform}` });
for (const os of oses) include.push({ script, os, platform, arm64: platform === 'arm' ? arm : '', mode: '', name: `${os ? `${name}-${os}` : name}-${platform}` });
}
if (!isVm && platforms.includes('pve') && await updateChanged(script, text)) {
const os = oses[0];
include.push({ script, os, platform: 'pve', arm64: '', mode: 'upgrade', name: `${os ? `${name}-${os}` : name}-pve-upgrade` });
}
}
for (const script of entries.slice(MAX)) notBuilt.push(`\`${script}\`: at most ${MAX} scripts per run, name it with \`/test ${script}\``);
@@ -327,7 +393,7 @@ jobs:
// full test, e.g. as a required check.
owner, repo, name: `Proxmox script test${onlyList.length ? ` (${onlyList.join(', ')} only)` : ''}`, head_sha: headSha, status: 'in_progress',
details_url: runUrl,
output: { title: `Building ${include.length}`, summary: include.map(i => `- \`${i.script}\` ${[i.platform, i.os].filter(Boolean).join(' · ')}`).join('\n') },
output: { title: `Building ${include.length}`, summary: include.map(i => `- \`${i.script}\` ${[i.platform, i.os, i.mode].filter(Boolean).join(' · ')}`).join('\n') },
});
checkId = String(check.id);
}
@@ -339,13 +405,14 @@ jobs:
core.setOutput('check_id', checkId);
core.setOutput('not_built', JSON.stringify(notBuilt));
core.setOutput('platforms', onlyList.join(' '));
core.setOutput('base_sha', baseSha);
// Who asked: the /test commenter, or whoever dispatched the run.
core.setOutput('actor', context.actor);
await sticky([
MARK, '### Proxmox script test', '',
`⏳ Building ${include.length} at ${headSha.slice(0, 7)}${isIssue ? ' (default branch)' : ''}${onlyList.length ? `, only on ${onlyList.join(', ')}` : ''} — [watch it live](${runUrl}) (open the \`test\` job)`, '',
...include.map(i => `- \`${i.script}\` (${[i.platform, i.os].filter(Boolean).join(' · ')})`),
...include.map(i => `- \`${i.script}\` (${[i.platform, i.os, i.mode && 'upgrade from the base branch'].filter(Boolean).join(' · ')})`),
...(notBuilt.length ? ['', '**Not built:**', ...notBuilt.map(n => `- ${n}`)] : []),
].join('\n'));
@@ -363,7 +430,7 @@ jobs:
continue-on-error: ${{ matrix.platform == 'arm' }}
permissions:
contents: read
timeout-minutes: 90 # install (45) + update (30) + checks
timeout-minutes: 120 # install (45) + update (30) + second update + checks
strategy:
fail-fast: false
matrix:
@@ -371,7 +438,7 @@ jobs:
name: test (${{ matrix.name }})
steps:
# The default branch, never the PR: the harness itself must be trusted.
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
sparse-checkout: .github/workflows/scripts
persist-credentials: false
@@ -392,6 +459,9 @@ jobs:
# ARM is the Incus harness on another machine.
PLATFORM: ${{ matrix.platform == 'arm' && 'incus' || matrix.platform }}
VAR_ARM64: ${{ matrix.platform == 'arm' && 'yes' || '' }}
# upgrade: install from the base branch, update with the PR.
TEST_MODE: ${{ matrix.mode || '' }}
BASE_SHA: ${{ needs.gate.outputs.base_sha }}
UPSTREAM: ${{ github.repository }}
CTID_BASE: ${{ github.repository == 'community-scripts/ProxmoxVE' && '9050' || '9000' }}
# Only so release lookups are not rate limited: unauthenticated, all
@@ -402,14 +472,14 @@ jobs:
# The self-hosted runners run as root; the GitHub one needs sudo (pct
# and incus do).
run: >-
${{ matrix.platform == 'arm' && 'sudo --preserve-env=PR_REPO,PR_SHA,SCRIPT,OS,PLATFORM,VAR_ARM64,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT' || '' }}
${{ matrix.platform == 'arm' && 'sudo --preserve-env=PR_REPO,PR_SHA,SCRIPT,OS,PLATFORM,VAR_ARM64,TEST_MODE,BASE_SHA,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT' || '' }}
bash .github/workflows/scripts/pve-run-script.sh
- if: always() && matrix.platform == 'arm'
run: sudo chmod -R a+rX "$RUNNER_TEMP/result" 2>/dev/null || true
- if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: result-${{ matrix.name }}
path: ${{ runner.temp }}/result
@@ -427,12 +497,12 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v8
with:
pattern: result-*
path: results
- uses: actions/github-script@v8
- uses: actions/github-script@v9
env:
MATRIX: ${{ needs.gate.outputs.matrix }}
PR: ${{ needs.gate.outputs.pr }}
@@ -477,6 +547,12 @@ jobs:
if (r.status === 'pass' && r.update_status === 'error') { r.status = 'error'; r.step = `update ${r.update_step}`; }
const ulog = path.join(dir, 'update.log');
r.update_log = fs.existsSync(ulog) ? fs.readFileSync(ulog, 'utf8') : '';
const u2log = path.join(dir, 'update2.log');
r.update2_log = fs.existsSync(u2log) ? fs.readFileSync(u2log, 'utf8') : '';
if (!['upgrade', ''].includes(r.mode || '')) r.mode = '';
r.mode = r.mode || '';
r.notes = Array.isArray(r.notes) ? r.notes.filter(n => typeof n === 'string').slice(0, 8) : [];
r.resources = typeof r.resources === 'string' ? r.resources : '';
const ushot = path.join(dir, 'screenshot-update.png');
r.shot_update_file = fs.existsSync(ushot) ? ushot : '';
// ARM runs the Incus harness on an arm64 machine; result.json
@@ -484,7 +560,7 @@ jobs:
// unknown one would never match a matrix row anyway.
if (r.arch === 'aarch64') r.platform = 'arm';
else if (!['pve', 'incus'].includes(r.platform)) r.platform = 'pve';
found.set(`${r.script}|${r.platform}|${r.os}`, r);
found.set(`${r.script}|${r.platform}|${r.os}|${r.mode}`, r);
} catch (err) { core.warning(`unreadable ${p}: ${err.message}`); }
}
}
@@ -492,19 +568,20 @@ jobs:
walk('results');
// A job that died before writing result.json still gets a row.
const rows = matrix.map(m => ({ ...(found.get(`${m.script}|${m.platform || 'pve'}|${m.os || 'default'}`) || {
script: m.script, os: m.os || 'default', platform: m.platform || 'pve',
const rows = matrix.map(m => ({ ...(found.get(`${m.script}|${m.platform || 'pve'}|${m.os || 'default'}|${m.mode || ''}`) || {
script: m.script, os: m.os || 'default', platform: m.platform || 'pve', mode: m.mode || '', notes: [], resources: '', update2_log: '',
status: process.env.TEST_RESULT === 'cancelled' ? 'cancelled' : 'error',
step: 'no result (runner or job failure)', duration: 0, log_tail: '', install_log: '', shot_file: '',
update_status: '', update_log: '', shot_update_file: '',
}), name: m.name }));
// Where a row ran: "pve", "incus · alpine", "arm64", or "VM" (Proxmox only).
// Where a row ran: "pve", "incus · alpine", "arm64", "pve · upgrade", or "VM" (Proxmox only).
const isVm = r => r.script.startsWith('vm/');
const isArm = r => r.platform === 'arm';
const where = r => {
if (isVm(r)) return 'VM';
const p = isArm(r) ? 'arm64' : r.platform;
return r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
const w = r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
return r.mode === 'upgrade' ? `${w} · upgrade` : w;
};
// ARM is information only: shown, never counted. The result, the
// check, the review and the approval come from Proxmox and Incus.
@@ -616,6 +693,15 @@ jobs:
// and a log that does not fit loses its beginning: the end is where
// a failure shows.
const budget = Math.floor(52000 / rows.length);
// A check run summary holds 65535 BYTES, not characters, and log
// emoji take 3-4 bytes each. Cut at a character boundary and close
// a ~~~~ block left open.
const fitBytes = (text, max) => {
if (Buffer.byteLength(text) <= max) return text;
let cut = Buffer.from(text).subarray(0, max - 300).toString('utf8').replace(/�+$/, '');
if ((cut.match(/^~~~~/gm) || []).length % 2) cut += '\n~~~~';
return `${cut}\n\n… cut to fit GitHub's size limit; the full report is in the PR comment and the logs in the run artifacts.`;
};
const tail = (text, max) => {
text = fence(text).trimEnd();
if (text.length <= max) return text;
@@ -647,6 +733,9 @@ jobs:
const lines = r.update_log.trimEnd().split('\n').length;
parts.push(details(`🔄 ${r.script} (${where(r)}) — update log (${lines} lines)`, r.update_log, updateShare));
}
if (r.update2_log) {
parts.push(details(`🔁 ${r.script} (${where(r)}) — second update log`, r.update2_log, Math.min(r.update2_log.length, Math.floor(budget / 8))));
}
return parts.join('\n\n');
});
@@ -661,6 +750,22 @@ jobs:
`${headline} at \`${process.env.SHA.slice(0, 7)}\``, '',
table, '',
...(armLine ? [`<sub>${armLine}</sub>`, ''] : []),
...(rows.some(r => r.mode === 'upgrade')
? ['<sub>`upgrade`: installed from the base branch as it is now, then updated with this PR, as a user who installed before the merge would.</sub>', '']
: []),
...(() => {
const noted = rows.filter(r => r.notes.length);
return noted.length
? ['**Notes** (information only, not counted):', ...noted.flatMap(r => r.notes.map(n => `- \`${r.script}\` (${where(r)}): ${clean(n)}`)), '']
: [];
})(),
...(() => {
const measured = rows.filter(r => r.resources);
return measured.length
? ['<details><summary>📊 Resources</summary>', '', '| Script | Platform | Usage |', '|---|---|---|',
...measured.map(r => `| \`${r.script}\` | ${where(r)} | ${clean(r.resources)} |`), '', '</details>', '']
: [];
})(),
...(partial ? [`<sub>Built only on ${process.env.PLATFORMS.split(' ').join(', ')}; comment \`/test\` for every platform.</sub>`, ''] : []),
...shots, '',
...(() => { try { const n = JSON.parse(process.env.NOT_BUILT || '[]'); return n.length ? ['**Not built:**', ...n.map(x => `- ${x}`), ''] : []; } catch (e) { return []; } })(),
@@ -679,7 +784,7 @@ jobs:
title: armOnly
? `ARM64 ${armPassed}/${armRows.length} passed (information only)`
: `${passed}/${coreRows.length} passed${armRows.length ? ` (+ ARM ${armPassed}/${armRows.length}, not counted)` : ''}`,
summary: summary.slice(0, 65000),
summary: fitBytes(summary, 65000),
},
});
}
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Installs ci-vm-controller.sh on the hypervisor (prox1): the script, the
# token reader, a systemd unit per CI VM, and each VM's config. Run as root
# on prox1 from a directory that also holds ci-vm-controller.sh:
#
# scp ci-controller-setup.sh ci-vm-controller.sh root@prox1:/root/ci-controller/
# ssh root@prox1 'cd /root/ci-controller && bash ci-controller-setup.sh'
#
# It does not start anything: a VM goes into service with
# systemctl enable --now ci-vm-controller@<vmid>
# once it has a 'clean' snapshot without any runner registered (see
# docs/guides/pr-script-testing.md, "Single-use hosts").
#
# The token (/etc/ci-controller/token) is put there by hand: a fine-grained
# token for the two repositories with Administration: read and write only.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f ci-vm-controller.sh ]] || { echo "ci-vm-controller.sh missing next to this script" >&2; exit 1; }
command -v jq >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq >/dev/null
install -m 755 ci-vm-controller.sh /usr/local/sbin/ci-vm-controller.sh
install -d -m 700 /etc/ci-controller
cat >/usr/local/sbin/ci-gh-token <<'EOF'
#!/bin/sh
# Prints the token ci-vm-controller.sh uses (Administration: write on the CI
# repositories). Swap this for a GitHub App token later without touching
# the controller.
exec cat /etc/ci-controller/token
EOF
chmod 755 /usr/local/sbin/ci-gh-token
cat >/etc/systemd/system/ci-vm-controller@.service <<'EOF'
[Unit]
Description=Single-use CI runner host, VM %i
After=network-online.target pve-cluster.service pvedaemon.service
Wants=network-online.target
# A missing token is a setup error, not something to retry every 30 s.
ConditionPathExists=/etc/ci-controller/token
[Service]
ExecStart=/usr/local/sbin/ci-vm-controller.sh %i
Restart=always
RestartSec=30
# Stopping the controller leaves the VM as it is; the next start resets it,
# which also ends a job that may still be running there.
KillMode=mixed
[Install]
WantedBy=multi-user.target
EOF
# One repository per host: see ci-vm-controller.sh.
conf() { # conf <vmid> <ip> <name> <label> <repo>
cat >"/etc/ci-controller/$1.conf" <<EOF
VM_IP=$2
HOST_NAME=$3
LABEL=$4
REPOS=$5
EOF
}
conf 200 10.201.146.232 pve-ci proxmox-test community-scripts/DevScripts
conf 201 10.201.146.231 pve-ci-2 proxmox-test community-scripts/ProxmoxVE
conf 204 10.201.146.235 pve-ci-3 proxmox-test community-scripts/ProxmoxVE
conf 202 10.201.146.233 incus-ci incus-test community-scripts/DevScripts
conf 203 10.201.146.234 incus-ci-2 incus-test community-scripts/ProxmoxVE
systemctl daemon-reload
echo "Installed. Token: $([[ -s /etc/ci-controller/token ]] && echo present || echo 'MISSING (/etc/ci-controller/token)')"
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bash
# Keeps one CI VM (pve-ci, incus-ci, ...) as a single-use GitHub runner host.
# Runs on the hypervisor (prox1), which PR code never touches.
#
# Every cycle:
# 1. roll the VM back to its clean snapshot and boot it;
# 2. register a just-in-time runner on it (generate-jitconfig) for the
# host's repository. Such a runner takes exactly one job and is deleted
# by GitHub afterwards, so its credentials are worthless once that job
# is over;
# 3. (with several repositories in REPOS: as soon as one runner is busy,
# delete the others that are still idle, so no second job starts on a
# host that is about to be reset);
# 4. once every runner of this cycle is gone, start over at 1.
#
# PR code runs as root on the VM, so nothing the VM says is trusted: when a
# job is over is read from GitHub's API, never from the VM.
#
# Config: /etc/ci-controller/<vmid>.conf (or the environment), see below.
# Usage: ci-vm-controller.sh <vmid> run forever (the systemd unit)
# ONE_CYCLE=1 ci-vm-controller.sh <vmid> one job, then stop (testing)
set -euo pipefail
VMID=${1:?usage: ci-vm-controller.sh <vmid>}
CONF=${CONF:-/etc/ci-controller/$VMID.conf}
# shellcheck source=/dev/null
[[ -f "$CONF" ]] && . "$CONF"
: "${VM_IP:?VM_IP}" # address of the CI VM
: "${HOST_NAME:?HOST_NAME}" # runner name prefix, e.g. pve-ci
: "${LABEL:?LABEL}" # proxmox-test or incus-test
# One repository per host. Several would each have a runner waiting on the
# VM, and a job of one could read the idle runner's JIT config of the other
# and use it elsewhere before it is withdrawn: one repository's PR code
# faking the other's results. (Several are fine within one trust domain.)
: "${REPOS:?REPOS, e.g. community-scripts/DevScripts}"
EXTRA_LABELS=${EXTRA_LABELS:-} # space-separated, e.g. for a test
SNAPSHOT=${SNAPSHOT:-clean}
TOKEN_CMD=${TOKEN_CMD:-/usr/local/sbin/ci-gh-token} # prints a token with repo Administration: write
PVE=${PVE:-} # prefix for qm, e.g. "ssh root@prox1" when not run on it
GUEST_SSH=${GUEST_SSH:-"ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new root@$VM_IP"}
POLL=${POLL:-20}
MAX_BUSY_MIN=${MAX_BUSY_MIN:-180} # the test job's own limit is 90
MAX_IDLE_MIN=${MAX_IDLE_MIN:-720} # recycle an idle host twice a day
ONE_CYCLE=${ONE_CYCLE:-}
log() { echo "[$(date -u +%FT%TZ)] vm $VMID: $*"; }
qm() { if [[ -n "$PVE" ]]; then $PVE qm "$@"; else command qm "$@"; fi; }
# Not in a subshell, so the status and the cached token survive the call:
# api METHOD PATH [JSON] -> body in $API_BODY, HTTP status in $API_STATUS.
API_FILE=$(mktemp)
trap 'rm -f "$API_FILE"' EXIT
TOKEN="" TOKEN_AT=0
api() {
local method=$1 path=$2 data=${3:-}
# Cached for 30 minutes (an App installation token lives 60).
if [[ -z "$TOKEN" || $(($(date +%s) - TOKEN_AT)) -gt 1800 ]]; then
TOKEN=$($TOKEN_CMD) || { log "no GitHub token"; API_STATUS=000 API_BODY=""; return 1; }
TOKEN_AT=$(date +%s)
fi
API_STATUS=$(curl -sS -o "$API_FILE" -w '%{http_code}' -X "$method" \
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' ${data:+-d "$data"} "https://api.github.com$path") || API_STATUS=000
API_BODY=$(<"$API_FILE")
[[ "$API_STATUS" != 000 ]]
}
reset_vm() {
log "rolling back to '$SNAPSHOT'"
[[ "$(qm status "$VMID")" == *running* ]] && qm stop "$VMID" --timeout 60 >/dev/null
qm rollback "$VMID" "$SNAPSHOT" >/dev/null
qm start "$VMID" >/dev/null
local i
for i in $(seq 1 60); do
$GUEST_SSH true 2>/dev/null && { log "booted"; return 0; }
sleep 5
done
log "no SSH after 5 minutes"
return 1
}
# Parallel arrays: one entry per runner registered in this cycle.
R_REPO=() R_ID=() R_GONE=()
register() { # register <owner/repo>
local repo=$1 name dir labels body cfg id
name="$HOST_NAME-${repo#*/}-$(date +%s)"
dir=/opt/actions-runner/${repo#*/}
labels=$(printf '%s\n' "$LABEL" $EXTRA_LABELS | jq -R . | jq -sc .)
api POST "/repos/$repo/actions/runners/generate-jitconfig" \
"$(jq -nc --arg n "$name" --argjson l "$labels" '{name: $n, runner_group_id: 1, labels: $l, work_folder: "_work"}')" || return 1
body=$API_BODY
[[ "$API_STATUS" == 201 ]] || { log "$repo: generate-jitconfig failed ($API_STATUS): $(jq -r .message <<<"$body" 2>/dev/null)"; return 1; }
id=$(jq -r .runner.id <<<"$body")
cfg=$(jq -r .encoded_jit_config <<<"$body")
# The config goes in through stdin, not the command line.
if ! $GUEST_SSH "umask 077; cat >/run/ci-jit-${repo#*/} && systemd-run --quiet --collect --unit=ci-jit-${repo#*/} \
-p WorkingDirectory=$dir -E RUNNER_ALLOW_RUNASROOT=1 \
/bin/sh -c 'cfg=\$(cat /run/ci-jit-${repo#*/}); rm -f /run/ci-jit-${repo#*/}; exec ./run.sh --jitconfig \"\$cfg\"'" <<<"$cfg"; then
log "$repo: could not start the runner on the VM"
api DELETE "/repos/$repo/actions/runners/$id" >/dev/null || true
return 1
fi
R_REPO+=("$repo") R_ID+=("$id") R_GONE+=(0)
log "$repo: runner $name (id $id) started"
}
drop() { # drop <index>: delete an idle runner; GitHub refuses a busy one
local i=$1
api DELETE "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" >/dev/null || return 1
if [[ "$API_STATUS" == 204 || "$API_STATUS" == 404 ]]; then R_GONE[$i]=1; return 0; fi
return 1
}
watch_cycle() {
local start busy_since="" i body status busy any_busy left offline_since=""
start=$(date +%s)
while :; do
sleep "$POLL"
any_busy=0 left=0 offline=0
for i in "${!R_ID[@]}"; do
[[ "${R_GONE[$i]}" == 1 ]] && continue
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || { left=1; continue; }
body=$API_BODY
if [[ "$API_STATUS" == 404 ]]; then
R_GONE[$i]=1
log "${R_REPO[$i]}: runner ${R_ID[$i]} is gone (job done or removed)"
continue
fi
left=1
busy=$(jq -r .busy <<<"$body"); status=$(jq -r .status <<<"$body")
[[ "$busy" == true ]] && any_busy=1
[[ "$status" == offline ]] && offline=1
done
if [[ "$any_busy" == 1 && -z "$busy_since" ]]; then
busy_since=$(date +%s)
log "a job started; withdrawing the idle runners"
fi
# A host that has started a job takes no other one: it will be reset.
if [[ -n "$busy_since" ]]; then
for i in "${!R_ID[@]}"; do
[[ "${R_GONE[$i]}" == 1 ]] && continue
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || continue
body=$API_BODY
[[ "$API_STATUS" == 200 && "$(jq -r .busy <<<"$body")" == false ]] && drop "$i" &&
log "${R_REPO[$i]}: idle runner ${R_ID[$i]} withdrawn"
done
fi
[[ "$left" == 0 ]] && { log "cycle over"; return 0; }
local now; now=$(date +%s)
if [[ -n "$busy_since" && $((now - busy_since)) -gt $((MAX_BUSY_MIN * 60)) ]]; then
log "job still running after $MAX_BUSY_MIN minutes; resetting anyway"
break
fi
if [[ -z "$busy_since" && $((now - start)) -gt $((MAX_IDLE_MIN * 60)) ]]; then
log "idle for $MAX_IDLE_MIN minutes; recycling"
break
fi
# A runner that never comes online (or dies while idle) holds the host
# forever otherwise.
if [[ -z "$busy_since" && "$offline" == 1 ]]; then
[[ -z "$offline_since" ]] && offline_since=$now
if [[ $((now - offline_since)) -gt 300 ]]; then log "a runner is offline for 5 minutes; resetting"; break; fi
else
offline_since=""
fi
done
for i in "${!R_ID[@]}"; do [[ "${R_GONE[$i]}" == 1 ]] || drop "$i" || true; done
}
# Registrations of this host that a stopped or crashed controller left behind:
# their VM was reset, so they are offline and never take a job, but they
# clutter the runner list. Busy or online ones are left alone.
cleanup_stale() {
local repo ids id
for repo in $REPOS; do
api GET "/repos/$repo/actions/runners?per_page=100" || continue
[[ "$API_STATUS" == 200 ]] || continue
ids=$(jq -r --arg p "$HOST_NAME-${repo#*/}-" \
'.runners[] | select((.name | startswith($p)) and .status == "offline" and (.busy | not)) | .id' <<<"$API_BODY")
for id in $ids; do
api DELETE "/repos/$repo/actions/runners/$id" && log "$repo: stale runner $id removed"
done
done
}
while :; do
R_REPO=() R_ID=() R_GONE=()
if reset_vm; then
cleanup_stale
ok=0
for repo in $REPOS; do register "$repo" && ok=1 || true; done
if [[ "$ok" == 1 ]]; then watch_cycle; else log "no runner could be registered"; sleep 60; fi
else
sleep 60
fi
[[ -n "$ONE_CYCLE" ]] && { log "ONE_CYCLE: stopping; the VM is left as the job left it"; break; }
done
+9
View File
@@ -6,6 +6,8 @@
#
# incus-ci-host-setup.sh host only
# incus-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
# incus-ci-host-setup.sh <owner/repo> jit host + runner software only, for
# ci-vm-controller.sh (single-use host)
#
# The token is a runner *registration* token (valid one hour):
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
@@ -176,6 +178,13 @@ if [[ $# -eq 2 ]]; then
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
./bin/installdependencies.sh >/dev/null
fi
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
# a fresh runner for every job and resets the VM afterwards, so only the
# runner software goes on it, and the 'clean' snapshot is taken after this.
if [[ "$token" == jit ]]; then
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
exit 0
fi
if [[ -f .runner ]]; then
./svc.sh stop >/dev/null 2>&1 || true
./svc.sh uninstall >/dev/null 2>&1 || true
+9
View File
@@ -5,6 +5,8 @@
#
# pve-ci-host-setup.sh host only
# pve-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
# pve-ci-host-setup.sh <owner/repo> jit host + runner software only, for
# ci-vm-controller.sh (single-use host)
#
# The token is a runner *registration* token (valid one hour):
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
@@ -196,6 +198,13 @@ if [[ $# -eq 2 ]]; then
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
./bin/installdependencies.sh >/dev/null
fi
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
# a fresh runner for every job and resets the VM afterwards, so only the
# runner software goes on it, and the 'clean' snapshot is taken after this.
if [[ "$token" == jit ]]; then
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
exit 0
fi
# A registered runner is reconfigured from scratch: stop and remove the old
# registration (the token works for removal too).
if [[ -f .runner ]]; then
+260 -40
View File
@@ -48,6 +48,16 @@ URL_WAIT="${URL_WAIT:-120}"
# script that leaves var_arm64 unset; the ARM run builds those anyway, to see.
VAR_ARM64="${VAR_ARM64:-}"
[[ -z "$VAR_ARM64" || "$VAR_ARM64" == "yes" ]] || { echo "bad VAR_ARM64" >&2; exit 2; }
# upgrade: install the script as it is on the base branch (BASE_SHA of
# UPSTREAM), then update with the PR. That is what a user does who installed
# before the PR is merged and runs `update` after it. Empty: install and
# update both from the PR.
TEST_MODE="${TEST_MODE:-}"
[[ -z "$TEST_MODE" || "$TEST_MODE" == "upgrade" ]] || { echo "bad TEST_MODE" >&2; exit 2; }
if [[ -n "$TEST_MODE" ]]; then
[[ "${BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || { echo "bad BASE_SHA" >&2; exit 2; }
[[ "$SCRIPT" == ct/* ]] || { echo "upgrade tests are for ct/ scripts" >&2; exit 2; }
fi
[[ "$PLATFORM" == "pve" || "$SCRIPT" == ct/* ]] || { echo "vm/ scripts run on pve only" >&2; exit 2; }
[[ "$CTID_BASE" =~ ^[0-9]+$ ]] || { echo "bad CTID_BASE" >&2; exit 2; }
@@ -59,12 +69,17 @@ INSTALL_LOG_OUT="$OUT_DIR/install.log"
UPDATE_RAW="$OUT_DIR/update.raw.log"
UPDATE_LOG="$OUT_DIR/update.log"
UPDATE_TIMEOUT="${UPDATE_TIMEOUT:-30m}"
UPDATE2_RAW="$OUT_DIR/update2.raw.log"
UPDATE2_LOG="$OUT_DIR/update2.log"
# How long the app has to keep running without a service restarting.
STABLE_WAIT="${STABLE_WAIT:-60}"
kind="${SCRIPT%%/*}" # ct or vm
app=$(basename "$SCRIPT" .sh)
job="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${PLATFORM}-${kind}-${app}-${OS:-default}"
job="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${PLATFORM}-${kind}-${app}-${OS:-default}${TEST_MODE:+-$TEST_MODE}"
tree="$SERVE_ROOT/$job"
base="$SERVE_URL/$job"
src_tree="$tree" # what gets installed; the base branch's tree in an upgrade test
started=$(date +%s)
status="fail"
@@ -102,6 +117,16 @@ failed_units=""
# fail: the update broke; empty: not attempted (the install failed).
update_status=""
update_step=""
# install: building and checking what was installed; update: from the first
# update on. In an upgrade test a failure while installing is the base
# branch's, not the PR's.
phase="install"
# Findings that do not fail the build, shown under the table on the PR.
notes=()
# Disk, RAM and OOM kills of the container, measured at the end.
resources=""
# Second `update` right after the first: no-update, updated-again, or empty.
update2_status=""
# Spinner frames: the engine redraws "⠋ Starting VM" a few times a second
# without a tty too. An alternation, not a bracket: these are multibyte and
@@ -163,13 +188,61 @@ classify_cpu_failure() {
step="needs a newer CPU than the test host has (no ${feature}): ${line}"
}
# Disk used against var_disk, RAM in use at rest (anonymous memory, without
# the page cache that fills any limit) against the limit, and how often the
# kernel killed something for lack of memory, over the container's whole life,
# the build included. A dir-backed Incus root has no size of its own: df shows
# the host's disk. Then what the container holds is counted with du, and the
# script's var_disk is the size to compare with.
measure_resources() {
local out disk_size disk_used anon limit oom want_disk want_ram line=()
out=$(guest_exec sh -c 'df -Pm / | awk "NR==2 {print \$2, \$3}"
awk "/^anon / {print int(\$2 / 1048576)}" /sys/fs/cgroup/memory.stat 2>/dev/null || echo -
cat /sys/fs/cgroup/memory.max 2>/dev/null || echo -
awk "/^oom_kill / {print \$2}" /sys/fs/cgroup/memory.events 2>/dev/null || echo -' 2>/dev/null) || return 0
read -r disk_size disk_used <<<"$(sed -n 1p <<<"$out")"
anon=$(sed -n 2p <<<"$out"); limit=$(sed -n 3p <<<"$out"); oom=$(sed -n 4p <<<"$out")
want_disk=$(grep -m1 -oE 'var_disk="\$\{var_disk:-[0-9]+\}"' "$src_tree/$SCRIPT" | grep -oE '[0-9]+' | head -n1)
want_ram=$(grep -m1 -oE 'var_ram="\$\{var_ram:-[0-9]+\}"' "$src_tree/$SCRIPT" | grep -oE '[0-9]+' | head -n1)
[[ "$disk_size" =~ ^[0-9]+$ && "$disk_used" =~ ^[0-9]+$ ]] || return 0
if [[ -n "$want_disk" ]] && ((disk_size > want_disk * 1024 * 3 / 2)); then
disk_size=$((want_disk * 1024))
disk_used=$(guest_exec sh -c 'du -sxm / 2>/dev/null | cut -f1' 2>/dev/null)
[[ "$disk_used" =~ ^[0-9]+$ ]] || return 0
fi
line+=("disk $(awk -v u="$disk_used" 'BEGIN {printf "%.1f", u / 1024}') of $(awk -v s="$disk_size" 'BEGIN {printf "%.0f", s / 1024}') GB")
if ((disk_used * 100 >= disk_size * 90)); then
notes+=("disk ${disk_used} of ${disk_size} MB used after the install: var_disk may be too small")
fi
[[ "$limit" =~ ^[0-9]+$ ]] && limit=$((limit / 1048576)) || limit="${want_ram:-}"
if [[ "$anon" =~ ^[0-9]+$ ]]; then
line+=("RAM at rest ${anon}${limit:+ of ${limit}} MB")
if [[ "$limit" =~ ^[0-9]+$ ]] && ((anon * 100 >= limit * 80)); then
notes+=("${anon} of ${limit} MB RAM in use at rest: var_ram may be too small")
fi
fi
if [[ "$oom" =~ ^[0-9]+$ ]]; then
line+=("OOM kills ${oom}")
((oom > 0)) && notes+=("the kernel killed ${oom} process(es) for lack of memory: var_ram is too small for the build or the app")
fi
resources=$(
IFS='|'
echo "${line[*]}"
)
resources=${resources//|/ · }
}
finish() {
[[ -f "$RAW_LOG" ]] && strip_ansi <"$RAW_LOG" >"$LOG"
[[ -f "$RAW_LOG" ]] && collect_install_log
[[ -f "$UPDATE_RAW" ]] && strip_ansi <"$UPDATE_RAW" >"$UPDATE_LOG"
rm -f "$UPDATE_RAW"
[[ -f "$UPDATE2_RAW" ]] && strip_ansi <"$UPDATE2_RAW" >"$UPDATE2_LOG"
rm -f "$UPDATE_RAW" "$UPDATE2_RAW"
[[ "$kind" == ct && -n "$ctid" && -z "$resources" ]] && guest_running && measure_resources
jq -n \
--arg script "$SCRIPT" --arg kind "$kind" --arg platform "$PLATFORM" --arg os "${OS:-default}" --arg status "$status" \
--arg mode "$TEST_MODE" --arg resources "$resources" --arg update2_status "$update2_status" \
--arg notes "$(printf '%s\n' "${notes[@]}")" \
--arg step "$step" --arg exit_code "$exit_code" --arg sha "$PR_SHA" \
--arg ctid "$ctid" --arg url "$url" --arg http_code "$http_code" \
--arg failed_units "$failed_units" --arg ip "$ct_ip" --arg arch "$(uname -m)" \
@@ -179,6 +252,8 @@ finish() {
'{script: $script, kind: $kind, platform: $platform, arch: $arch, os: $os, status: $status, step: $step, exit_code: $exit_code,
sha: $sha, ctid: $ctid, ip: $ip, url: $url, http_code: $http_code,
failed_units: $failed_units, update_status: $update_status, update_step: $update_step,
mode: $mode, resources: $resources, update2_status: $update2_status,
notes: ($notes | split("\n") | map(select(. != ""))),
duration: $duration, log_tail: $tail}' >"$RESULT"
echo "::group::result"
jq 'del(.log_tail)' "$RESULT"
@@ -262,7 +337,7 @@ cleanup() {
done
fi
rm -f "$RAW_LOG.start"
rm -rf -- "$tree"
rm -rf -- "$tree" "$tree-base"
}
trap cleanup EXIT
# A cancelled job sends TERM; without this the EXIT trap would not run.
@@ -281,6 +356,10 @@ fail() {
step="$1"
status="${2:-fail}"
[[ "$status" == "unsupported" ]] || status="fail"
if [[ "$TEST_MODE" == "upgrade" && "$phase" == "install" && "$status" == "fail" ]]; then
status="error"
step="the install before this PR (${UPSTREAM}@${BASE_SHA:0:7}) failed, so the upgrade was not tested: $1"
fi
finish
echo "::warning title=${SCRIPT}::${step}"
exit 0
@@ -294,6 +373,20 @@ setup_error() {
exit 1
}
# Any answer means the app is up, unless it says it isn't: nothing at all,
# 404/410 (the printed URL leads nowhere), 5xx (server error, proxy without
# backend), or a 3xx left after 5 redirects (a loop). 401/403 are a login
# wall (warpgate answers 401), 400 a quirk of the app: both pass.
url_ok() { [[ "$1" =~ ^[1-4][0-9][0-9]$ && ! "$1" =~ ^(3[0-9][0-9]|404|410)$ ]]; }
url_why() {
case "$1" in
404 | 410) echo "the printed URL leads nowhere" ;;
5??) echo "server error" ;;
3??) echo "still redirecting after 5 hops" ;;
*) echo "the app never answered" ;;
esac
}
# --- Reserve a container ID ---------------------------------------------------
# A lock file per ID, not just "first free one", because the engine only creates
# the container minutes later and two jobs would otherwise pick the same ID.
@@ -335,20 +428,37 @@ upstream_re="https://raw\.githubusercontent\.com/${UPSTREAM//./\\.}/(refs/heads/
grep -rlE "$upstream_re" "$tree" --include='*.sh' --include='*.func' |
xargs -r sed -i -E "s#${upstream_re}#${base}#g"
# What gets installed: the PR, or in an upgrade test the base branch.
src_tree="$tree"
src_base="$base"
if [[ "$TEST_MODE" == "upgrade" ]]; then
src_tree="$tree-base"
src_base="$base-base"
mkdir -p "$src_tree"
curl -fsSL --retry 3 "https://codeload.github.com/${UPSTREAM}/tar.gz/${BASE_SHA}" |
tar -xz -C "$src_tree" --strip-components=1 || setup_error "could not download ${UPSTREAM}@${BASE_SHA:0:7}"
[[ -f "$src_tree/$SCRIPT" ]] || fail "${SCRIPT} does not exist at ${UPSTREAM}@${BASE_SHA:0:7}"
grep -rlE "$upstream_re" "$src_tree" --include='*.sh' --include='*.func' |
xargs -r sed -i -E "s#${upstream_re}#${src_base}#g"
fi
if [[ "$kind" == "vm" ]]; then
vm_build_and_check
exit 0
fi
# --- Build ----------------------------------------------------------------------
echo "Building ${SCRIPT} (${OS:-default OS}) on ${PLATFORM} as CT ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
if [[ "$TEST_MODE" == "upgrade" ]]; then
echo "Upgrade test: installing ${SCRIPT} from ${UPSTREAM}@${BASE_SHA:0:7} (before this PR), then updating with ${PR_REPO}@${PR_SHA:0:7}"
fi
echo "Building ${SCRIPT} (${OS:-default OS}) on ${PLATFORM} as CT ${ctid} from $([[ "$TEST_MODE" == upgrade ]] && echo "${UPSTREAM}@${BASE_SHA:0:7}" || echo "${PR_REPO}@${PR_SHA:0:7}")"
hostname_safe="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-50)"
build_env=(
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/root TERM=dumb LANG=C.UTF-8
mode=default MODE=default PHS_SILENT=1 DIAGNOSTICS=no
var_ctid="$ctid" var_hostname="$hostname_safe" var_brg="$BRIDGE"
"dev_mode=timing,keep" COMMUNITY_SCRIPTS_URL="$base"
"dev_mode=timing,keep" COMMUNITY_SCRIPTS_URL="$src_base"
)
if [[ "$PLATFORM" == "incus" ]]; then
build_env+=(var_container_storage=default)
@@ -358,8 +468,19 @@ fi
touch "$RAW_LOG.start"
[[ -n "$OS" ]] && build_env+=(var_os="$OS")
[[ -n "$VAR_ARM64" ]] && build_env+=(var_arm64=yes)
# A script that hands over to upstream's own installer (run_external_installer)
# asks first; nobody can answer in CI, and testing the script means running it.
build_env+=(var_external_installer=yes)
# Both engines turn var_github_token into GITHUB_TOKEN inside the container.
[[ -n "${GH_API_TOKEN:-}" ]] && build_env+=(var_github_token="$GH_API_TOKEN")
# How much API quota this build starts with, with the token and without (the
# lab's one address). /rate_limit itself costs nothing. A "rate limit" in the
# build is then easy to place.
gh_quota() {
curl -s --max-time 10 ${1:+-H "Authorization: Bearer $1"} https://api.github.com/rate_limit 2>/dev/null |
jq -r '.rate | "\(.remaining) of \(.limit) left, resets in \(((.reset - now) / 60) | floor) min"' 2>/dev/null || echo "unknown"
}
echo "GitHub API quota: token $(gh_quota "${GH_API_TOKEN:-}") · no token $(gh_quota "")"
# --- Answers for the script's questions ------------------------------------------
# Nobody types into CI, so anything the install asks has to be answered up front.
@@ -387,7 +508,7 @@ ci_value() {
esac
}
declare -A answers=()
json="$tree/json/${app}.json"
json="$src_tree/json/${app}.json"
if [[ -f "$json" ]]; then
while IFS=$'\t' read -r name default; do
answers[$name]="$default"
@@ -419,8 +540,8 @@ expand_templates() {
# The engine names the install script after APP (lower case, no spaces), which
# need not match the ct/ file name: ct/bluesky-pds.sh -> blueskypds-install.sh.
nsapp=$(grep -m1 -oE '^APP="[^"]+"' "$tree/$SCRIPT" | cut -d'"' -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]')
for f in "$tree/install/${app}-install.sh" "$tree/install/${nsapp}-install.sh" "$tree/$SCRIPT"; do
nsapp=$(grep -m1 -oE '^APP="[^"]+"' "$src_tree/$SCRIPT" | cut -d'"' -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]')
for f in "$src_tree/install/${app}-install.sh" "$src_tree/install/${nsapp}-install.sh" "$src_tree/$SCRIPT"; do
[[ -f "$f" ]] || continue
while read -r name; do
[[ -v "answers[$name]" ]] || answers[$name]=""
@@ -443,7 +564,7 @@ done
# In the background so that a TERM from a cancelled job is handled at once:
# bash defers traps until a foreground command returns.
env -i "${build_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 &
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$src_base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 &
build_pid=$!
start_streams
wait "$build_pid"
@@ -478,7 +599,8 @@ if ((exit_code != 0)); then
fi
# With dev_mode=keep (needed to read the install log) the engine carries on
# after a failed install and still prints "Completed Successfully", exit 0.
install_exit=$(strip_ansi <"$RAW_LOG" | grep -oE 'Installation failed in container [0-9]+ \(exit code: [0-9]+\)' |
# The container is a number on Proxmox (9000) and a name on Incus (ci-9000).
install_exit=$(strip_ansi <"$RAW_LOG" | grep -oE 'Installation failed in container [A-Za-z0-9._-]+ \(exit code: [0-9]+\)' |
grep -oE '[0-9]+\)$' | tr -d ')' | tail -n 1)
if [[ -n "$install_exit" ]]; then
last=$(failure_line)
@@ -519,11 +641,15 @@ take_screenshot() {
}
# Is the app up? No failed services, and the URL the script tells the user to
# open, e.g. http://${IP}:1337/admin, answers 200 (after redirects, so a jump
# to /login is fine). A script that prints no URL has no web UI to check.
# open, e.g. http://${IP}:1337/admin, answers without an error (see url_ok;
# redirects are followed, so a jump to /login is fine). A script that prints no URL has no web UI to check.
# Run after the install and again after the update. Sets health_problem.
pattern=$(grep -oE 'https?://\$\{IP\}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?' "$tree/$SCRIPT" | head -n1)
[[ -n "$pattern" && -n "$ct_ip" ]] && url="${pattern/\$\{IP\}/$ct_ip}"
app_url() { # the URL the script at $1 tells the user to open
local pattern
pattern=$(grep -oE 'https?://\$\{IP\}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?' "$1" | head -n1)
[[ -n "$pattern" && -n "$ct_ip" ]] && echo "${pattern/\$\{IP\}/$ct_ip}"
}
url=$(app_url "$src_tree/$SCRIPT")
health_problem=""
check_health() {
health_problem=""
@@ -539,13 +665,13 @@ check_health() {
local waited
for ((waited = 0; waited < URL_WAIT; waited += 5)); do
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
url_ok "$http_code" && break
sleep 5
done
# Also when the check fails: an error page explains more than a status code.
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot "$1"
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] ||
{ health_problem="${url} answered ${http_code/#000/nothing} instead of 200 within ${URL_WAIT}s"; return 1; }
url_ok "$http_code" ||
{ health_problem="${url} answered ${http_code/#000/nothing} within ${URL_WAIT}s ($(url_why "$http_code"))"; return 1; }
}
check_health screenshot.png || fail "$health_problem"
@@ -554,8 +680,10 @@ check_health screenshot.png || fail "$health_problem"
# A fresh install is already on the latest release, so a plain `update` would
# only say "No update available" and never run update_script's real path. The
# version files the install wrote (~/.<app>, one per fetch_and_deploy_gh_release)
# that update_script checks are set back to 0.0.0-ci first; check_for_gh_release then sees an update and
# runs it all. `update` is the /usr/bin/update the install generated: it pulls
# that update_script checks are set back first (see below); check_for_gh_release
# then sees an update and runs it all. Apps deployed from a branch
# (fetch_and_deploy_gh_branch, checked with check_for_gh_branch) store a commit
# hash instead of a version. `update` is the /usr/bin/update the install generated: it pulls
# ct/<app>.sh from the URL this container was built from, i.e. the PR's tree.
UPDATE_FAKE="0.0.0-ci"
update_fail() {
@@ -568,7 +696,7 @@ version_files=$(guest_exec sh -c '
for f in /root/.*; do
[ -f "$f" ] || continue
case "$f" in */.install-* | *.pending | *.bak | */.bash* | */.profile | */.hushlogin | */.viminfo | */.lesshst | */.wget-hsts | */.selected_editor) continue ;; esac
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^v?[0-9]+(\.[0-9A-Za-z_+-]+)+$" "$f" && echo "$f"
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^(v?[0-9]+(\.[0-9A-Za-z_+-]+)+|[0-9a-f]{7,40})$" "$f" && echo "$f"
done' 2>/dev/null | grep -E '^/root/\.[A-Za-z0-9._-]+$')
# Only set back what update_script checks: the names it passes to
# check_for_*release. The install can also deploy helpers the update never
@@ -579,30 +707,59 @@ tracked=$(grep -oE 'check_for_[a-z]*_?release[[:space:]]+("[^"]*"|[^[:space:]]+)
if [[ -n "$tracked" && "$tracked" != *'$'* ]]; then
version_files=$(grep -xF -f <(sed 's|^|/root/.|' <<<"$tracked") <<<"$version_files" || true)
fi
# A version keeps its digits and gets "-dev" (6.7.16.0 -> 6.7.16.0-dev): the
# engine ranks that below the release, so the update runs, and an
# update_script that reads the installed series (shopware: composer
# "~6.7.0" from ~/.shopware) still gets the real one. 0.0.0-ci broke that.
# A commit hash (or a bare number) has no series and gets 0.0.0-ci.
declare -A update_fake=()
for f in $version_files; do
guest_exec sh -c "echo '$UPDATE_FAKE' > '$f'; rm -f '$f.pending'" ||
cur=$(guest_exec cat "$f" 2>/dev/null | tr -d '\r\n')
if [[ "$cur" =~ ^v?[0-9]+(\.[0-9A-Za-z_+-]+)+$ ]]; then
update_fake[$f]="${cur}-dev"
else
update_fake[$f]="$UPDATE_FAKE"
fi
guest_exec sh -c "echo '${update_fake[$f]}' > '$f'; rm -f '$f.pending'" ||
update_fail "could not reset $f"
echo "Update test: set back $f from ${cur} to ${update_fake[$f]}"
done
[[ -n "$version_files" ]] && echo "Update test: set back $(tr '\n' ' ' <<<"$version_files")to ${UPDATE_FAKE}"
: >"$UPDATE_RAW"
update_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
echo "──────── update test (live) ────────"
echo "::stop-commands::${update_token}"
# timeout runs a program, not a shell function, so spell out the exec here.
if [[ "$PLATFORM" == "incus" ]]; then guest_cmd=(incus exec "$ctid" --); else guest_cmd=(pct exec "$ctid" --); fi
timeout --kill-after=1m "$UPDATE_TIMEOUT" "${guest_cmd[@]}" env PHS_SILENT=1 mode=default MODE=default \
TERM=dumb DIAGNOSTICS=no var_ignore_breaking_changes=yes ${GH_API_TOKEN:+GITHUB_TOKEN="$GH_API_TOKEN"} update </dev/null \
> >(tee "$UPDATE_RAW" | live_filter) 2>&1 &
update_pid=$!
wait "$update_pid"
update_rc=$?
update_pid=""
sleep 1
echo "::${update_token}::"
phase="update"
# Upgrade test: the container pulls its update from where it was installed
# from (COMMUNITY_SCRIPTS_URL in /usr/bin/update); point that at the PR, as
# merging it would.
if [[ "$TEST_MODE" == "upgrade" ]]; then
guest_exec sed -i -E "s#^export COMMUNITY_SCRIPTS_URL=.*#export COMMUNITY_SCRIPTS_URL=\"${base}\"#" /usr/bin/update &&
guest_exec grep -qxF "export COMMUNITY_SCRIPTS_URL=\"${base}\"" /usr/bin/update ||
setup_error "could not point /usr/bin/update at the PR"
echo "Upgrade test: /usr/bin/update now pulls from ${PR_REPO}@${PR_SHA:0:7}"
fi
# run_update <raw log> <title>: runs `update` in the container, live in the
# job log; sets update_rc.
run_update() {
local raw=$1 token
: >"$raw"
token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
echo "──────── $2 (live) ────────"
echo "::stop-commands::${token}"
# timeout runs a program, not a shell function, so spell out the exec here.
if [[ "$PLATFORM" == "incus" ]]; then guest_cmd=(incus exec "$ctid" --); else guest_cmd=(pct exec "$ctid" --); fi
timeout --kill-after=1m "$UPDATE_TIMEOUT" "${guest_cmd[@]}" env PHS_SILENT=1 mode=default MODE=default \
TERM=dumb DIAGNOSTICS=no var_ignore_breaking_changes=yes var_external_installer=yes ${GH_API_TOKEN:+GITHUB_TOKEN="$GH_API_TOKEN"} update </dev/null \
> >(tee "$raw" | live_filter) 2>&1 &
update_pid=$!
wait "$update_pid"
update_rc=$?
update_pid=""
sleep 1
echo "::${token}::"
}
run_update "$UPDATE_RAW" "update test"
update_failure_line() {
strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]|error:' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180
strip_ansi <"${1:-$UPDATE_RAW}" | grep -E '✖|\[ERROR\]|error:' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180
}
if ((update_rc == 124 || update_rc == 137)); then
update_fail "timed out after ${UPDATE_TIMEOUT}"
@@ -616,6 +773,7 @@ fi
update_error=$(strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
if [[ -n "$update_error" ]]; then
if [[ "${update_error,,}" == *"rate limit"* ]]; then
echo "GitHub API quota now: token $(gh_quota "${GH_API_TOKEN:-}") · no token $(gh_quota "")"
update_status="error"
update_step="could not be tested: ${update_error}"
status="pass"
@@ -627,8 +785,8 @@ fi
if strip_ansi <"$UPDATE_RAW" | grep -qi 'updated successfully'; then
update_status="updated"
for f in $version_files; do
[[ "$(guest_exec cat "$f" 2>/dev/null)" != "$UPDATE_FAKE" ]] ||
update_fail "reported success, but $f is still ${UPDATE_FAKE}"
[[ "$(guest_exec cat "$f" 2>/dev/null)" != "${update_fake[$f]}" ]] ||
update_fail "reported success, but $f is still ${update_fake[$f]}"
done
else
# Ran cleanly without updating anything: no version file to fake (apt/npm
@@ -637,7 +795,69 @@ else
update_step=$(strip_ansi <"$UPDATE_RAW" | grep -iE 'no update|up to date|up-to-date|already' | tail -n 1 | sed -E 's/^\s*(✔️?|ℹ️?|\s)*//' | cut -c1-160)
fi
url=$(app_url "$tree/$SCRIPT")
check_health screenshot-update.png || update_fail "after the update: $health_problem"
# --- Second update ------------------------------------------------------------------
# Right after an update, `update` has nothing to do: it must say so and leave
# the app alone. One that breaks on a second run breaks for every user on
# their next update; one that updates again although nothing changed
# rebuilds on every run (often a version file it never writes).
if [[ "$update_status" == "updated" ]]; then
run_update "$UPDATE2_RAW" "second update (must have nothing to do)"
if ((update_rc == 124 || update_rc == 137)); then
update_fail "second run timed out after ${UPDATE_TIMEOUT}"
elif ((update_rc != 0)); then
last=$(update_failure_line "$UPDATE2_RAW")
update_fail "second run exited ${update_rc}${last:+: $last}"
fi
update_error=$(strip_ansi <"$UPDATE2_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
if [[ -n "$update_error" && "${update_error,,}" != *"rate limit"* ]]; then
update_fail "second run: $update_error"
fi
if strip_ansi <"$UPDATE2_RAW" | grep -qi 'updated successfully'; then
update2_status="updated-again"
notes+=("a second \`update\` right after the first updated again: update_script does not notice it is current (e.g. a version file it never writes)")
else
update2_status="no-update"
fi
check_health screenshot-update.png || update_fail "after the second update: $health_problem"
fi
# --- Does it stay up? -----------------------------------------------------------
# A service that crash-loops can be "active" at every single look. Count the
# restarts of every service (and Docker container) over STABLE_WAIT seconds:
# any that went up was restarted while nobody touched the container.
restart_counts() {
guest_exec sh -c '
if command -v systemctl >/dev/null; then
systemctl list-units --type=service --all --no-legend --plain | awk "{print \$1}" |
while read -r u; do echo "$u $(systemctl show -p NRestarts --value "$u" 2>/dev/null)"; done
fi
if command -v docker >/dev/null; then
docker ps -aq | xargs -r docker inspect -f "docker:{{.Name}} {{.RestartCount}}" 2>/dev/null
fi' 2>/dev/null | awk '$2 ~ /^[0-9]+$/' | sort
}
before=$(restart_counts)
sleep "$STABLE_WAIT"
after=$(restart_counts)
restarting=$(join <(echo "$before") <(echo "$after") | awk '$3 > $2 {printf "%s%s (+%d)", sep, $1, $3 - $2; sep=", "}')
[[ -z "$restarting" ]] || fail "not stable: restarted within ${STABLE_WAIT}s without anyone touching it: ${restarting}"
check_health screenshot-update.png || fail "not stable: ${STABLE_WAIT}s later, ${health_problem}"
# Error lines in the journal since boot: often harmless, sometimes the reason
# a feature is broken. Information only.
if guest_exec sh -c 'command -v journalctl' &>/dev/null; then
# Without the noise every container produces: udev may not trigger devices
# in an unprivileged container, and logind finds no VTs.
journal_errors=$(guest_exec journalctl -b -p err -q -o cat --no-pager 2>/dev/null | grep -vE '^\s*$' |
grep -vE "Failed to write 'add' to '/sys/.*/uevent'|Seat has no VTs but VT number not 0" | sort | uniq -c | sort -rn)
if [[ -n "$journal_errors" ]]; then
n=$(awk '{s += $1} END {print s}' <<<"$journal_errors")
top=$(head -n 2 <<<"$journal_errors" | sed -E 's/^\s*[0-9]+\s+//' | cut -c1-120 | paste -sd'|' - | sed 's/|/; /g')
notes+=("${n} error line(s) in the journal since boot, e.g. ${top}")
fi
fi
status="pass"
finish
+3 -3
View File
@@ -121,7 +121,7 @@ vm_build_and_check() {
for ((waited = 0; waited < VM_URL_WAIT; waited += 10)); do
vm_running || fail "VM ${ctid} stopped by itself while its web UI was awaited"
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
url_ok "$http_code" && break
sleep 10
done
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot screenshot.png
@@ -130,8 +130,8 @@ vm_build_and_check() {
sleep 20
fi
[[ -s "$OUT_DIR/screenshot.png" ]] || vm_console_shot
if [[ -n "$url" && ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
fail "${url} answered ${http_code/#000/nothing} instead of 200 within ${VM_URL_WAIT}s"
if [[ -n "$url" ]] && ! url_ok "$http_code"; then
fail "${url} answered ${http_code/#000/nothing} within ${VM_URL_WAIT}s"
fi
vm_running || fail "VM ${ctid} stopped by itself"
+60
View File
@@ -581,6 +581,43 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-11-13
### 🆕 New Scripts
- marimo ([#18672](https://github.com/community-scripts/ProxmoxVE/pull/18672))
- GoatCounter ([#18662](https://github.com/community-scripts/ProxmoxVE/pull/18662))
- Unsloth ([#18697](https://github.com/community-scripts/ProxmoxVE/pull/18697))
- colanode ([#18685](https://github.com/community-scripts/ProxmoxVE/pull/18685))
- SimpleLogin ([#18680](https://github.com/community-scripts/ProxmoxVE/pull/18680))
- Nightingale ([#18674](https://github.com/community-scripts/ProxmoxVE/pull/18674))
- Temporal ([#18670](https://github.com/community-scripts/ProxmoxVE/pull/18670))
- TrailBase ([#18669](https://github.com/community-scripts/ProxmoxVE/pull/18669))
- Weaviate ([#18671](https://github.com/community-scripts/ProxmoxVE/pull/18671))
- Spacebot ([#18668](https://github.com/community-scripts/ProxmoxVE/pull/18668))
- New-API ([#18667](https://github.com/community-scripts/ProxmoxVE/pull/18667))
- Martin ([#18666](https://github.com/community-scripts/ProxmoxVE/pull/18666))
- Langflow ([#18665](https://github.com/community-scripts/ProxmoxVE/pull/18665))
- Inngest ([#18664](https://github.com/community-scripts/ProxmoxVE/pull/18664))
- Hatchet ([#18663](https://github.com/community-scripts/ProxmoxVE/pull/18663))
- Engram ([#18661](https://github.com/community-scripts/ProxmoxVE/pull/18661))
- Euro-Office ([#18660](https://github.com/community-scripts/ProxmoxVE/pull/18660))
- Cypht ([#18659](https://github.com/community-scripts/ProxmoxVE/pull/18659))
- AirConnect ([#18653](https://github.com/community-scripts/ProxmoxVE/pull/18653))
- Bulwark ([#18656](https://github.com/community-scripts/ProxmoxVE/pull/18656))
- Convex ([#18658](https://github.com/community-scripts/ProxmoxVE/pull/18658))
- Chroma ([#18657](https://github.com/community-scripts/ProxmoxVE/pull/18657))
- Bugsink ([#18655](https://github.com/community-scripts/ProxmoxVE/pull/18655))
- 9Router ([#18654](https://github.com/community-scripts/ProxmoxVE/pull/18654))
### 💾 Core
- telemetry: always on for DevScripts, with a one-time notice [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([core#245](https://github.com/community-scripts/core/pull/245))
- Shield pct start and the template check from a closed console [@MickLesk](https://github.com/MickLesk) ([core#239](https://github.com/community-scripts/core/pull/239))
- core: say why a container stopped by itself during its setup [@MickLesk](https://github.com/MickLesk) ([core#234](https://github.com/community-scripts/core/pull/234))
- Download a corrupt local template again instead of keeping it [@MickLesk](https://github.com/MickLesk) ([core#235](https://github.com/community-scripts/core/pull/235))
- fetch the install script before the container is created [@MickLesk](https://github.com/MickLesk) ([core#236](https://github.com/community-scripts/core/pull/236))
## 2026-11-12
### 🆕 New Scripts
@@ -591,8 +628,31 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- #### 🐞 Bug Fixes
- OnlyOffice: change ct port to 8083 [@MickLesk](https://github.com/MickLesk) ([#18642](https://github.com/community-scripts/ProxmoxVE/pull/18642))
- dotnetaspwebapi: start the service only once an app is deployed [@MickLesk](https://github.com/MickLesk) ([#18639](https://github.com/community-scripts/ProxmoxVE/pull/18639))
- Whisparr-eros: set the first login and save it [@MickLesk](https://github.com/MickLesk) ([#18617](https://github.com/community-scripts/ProxmoxVE/pull/18617))
- #### 💥 Breaking Changes
- Updated ASP.NET Web App script to Debian and adds a .Net selection [@kris701](https://github.com/kris701) ([#18628](https://github.com/community-scripts/ProxmoxVE/pull/18628))
### 💾 Core
- github: bump workflow actions to their current majors [@MickLesk](https://github.com/MickLesk) ([core#240](https://github.com/community-scripts/core/pull/240))
- Say what the host sees when a container gets no address [@MickLesk](https://github.com/MickLesk) ([core#230](https://github.com/community-scripts/core/pull/230))
- Fall back to public DNS for Alpine containers too [@MickLesk](https://github.com/MickLesk) ([core#231](https://github.com/community-scripts/core/pull/231))
- Do not fail a finished install over the monitoring service [@MickLesk](https://github.com/MickLesk) ([core#232](https://github.com/community-scripts/core/pull/232))
- Deploy a release by moving it, and stop on a full disk² [@MickLesk](https://github.com/MickLesk) ([core#233](https://github.com/community-scripts/core/pull/233))
- core: say when pct create fails because timedatectl does not answer [@MickLesk](https://github.com/MickLesk) ([core#237](https://github.com/community-scripts/core/pull/237))
- ref: stop calling every exit 255 a dpkg error [@MickLesk](https://github.com/MickLesk) ([core#238](https://github.com/community-scripts/core/pull/238))
- Write uv's shell completions without STD [@MickLesk](https://github.com/MickLesk) ([core#241](https://github.com/community-scripts/core/pull/241))
- Generate database passwords with random_password / Keep the database helpers' credentials files private [@MickLesk](https://github.com/MickLesk) ([core#242](https://github.com/community-scripts/core/pull/242))
### 📚 Documentation
- github: bump workflow actions to their current majors [@MickLesk](https://github.com/MickLesk) ([#18637](https://github.com/community-scripts/ProxmoxVE/pull/18637))
- pocketbase-bot: apply website metadata requests from Accept/Decline boxes [@MickLesk](https://github.com/MickLesk) ([#18631](https://github.com/community-scripts/ProxmoxVE/pull/18631))
## 2026-11-11
### 🚀 Updated Scripts
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/decolua/9router
APP="9Router"
var_tags="${var_tags:-ai;llm;gateway}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /usr/lib/node_modules/9router ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
CURRENT=$(jq -r '.version' /usr/lib/node_modules/9router/package.json)
RELEASE=$(npm view 9router version)
if [[ "$CURRENT" == "$RELEASE" ]]; then
msg_ok "No update available: 9Router (${CURRENT})"
exit
fi
NODE_VERSION="22" setup_nodejs
msg_info "Stopping 9Router"
systemctl stop 9router
msg_ok "Stopped 9Router"
msg_info "Updating 9Router to ${RELEASE}"
$STD npm install -g --ignore-scripts "9router@${RELEASE}"
msg_ok "Updated 9Router to ${RELEASE}"
msg_info "Starting 9Router"
systemctl start 9router
msg_ok "Started 9Router"
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:20128/dashboard${CL}"
echo -e "${INFO}${YW}Dashboard password: INITIAL_PASSWORD in /opt/9router_data/.env (inside the container)${CL}"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Chris Moriarty (cwmoriarty)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/philippe44/AirConnect
APP="AirConnect"
var_tags="${var_tags:-airplay;audio;streaming}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/airconnect ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "airconnect" "philippe44/AirConnect"; then
msg_info "Stopping AirConnect"
systemctl stop airupnp aircast
msg_ok "Stopped AirConnect"
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="airupnp.xml aircast.xml" fetch_and_deploy_gh_release "airconnect" "philippe44/AirConnect" "prebuild" "latest" "/opt/airconnect" "AirConnect-*.zip"
msg_info "Configuring AirConnect"
chmod +x /opt/airconnect/airupnp-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/aircast-linux-$(arch_resolve x86_64 aarch64)
ln -sf /opt/airconnect/airupnp-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/airupnp
ln -sf /opt/airconnect/aircast-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/aircast
find /opt/airconnect -maxdepth 1 -type f ! -name "air*linux-$(arch_resolve x86_64 aarch64)*" ! -name "airupnp" ! -name "aircast" ! -name "*.xml" -delete
msg_ok "Configured AirConnect"
msg_info "Starting AirConnect"
systemctl start airupnp aircast
msg_ok "Started AirConnect"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}AirConnect is running and bridging AirPlay to discovered players:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}airupnp${CL} ${YW}(UPnP / Sonos bridge)${CL}"
echo -e "${TAB}${GATEWAY}${BGN}aircast${CL} ${YW}(Chromecast bridge)${CL}"
echo -e "${INFO}${YW}Ensure your container is on the same LAN subnet as your devices.${CL}"
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/bugsink/bugsink
APP="Bugsink"
var_tags="${var_tags:-monitoring;error-tracking;sentry}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_email="${var_admin_email:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/bugsink ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "bugsink" "bugsink/bugsink"; then
msg_info "Stopping Bugsink"
systemctl stop bugsink bugsink-snappea
msg_ok "Stopped Bugsink"
PYTHON_VERSION="3.12" setup_uv
msg_info "Updating Bugsink"
$STD uv pip install --upgrade --python /opt/bugsink/.venv/bin/python "bugsink==${CHECK_UPDATE_RELEASE}"
cd /opt/bugsink_data
$STD /opt/bugsink/.venv/bin/bugsink-manage migrate
$STD /opt/bugsink/.venv/bin/bugsink-manage migrate snappea --database=snappea
cat <<EOF >~/.bugsink
${CHECK_UPDATE_RELEASE}
EOF
msg_ok "Updated Bugsink"
msg_info "Starting Bugsink"
systemctl start bugsink bugsink-snappea
msg_ok "Started Bugsink"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
echo -e "${INFO}${YW}Admin login: /opt/bugsink_data/.env (inside the container)${CL}"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/bulwarkmail/webmail
APP="Bulwark"
var_tags="${var_tags:-mail;webmail;jmap}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_jmap_url="${var_jmap_url:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/bulwark ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "bulwark" "bulwarkmail/webmail"; then
msg_info "Stopping Bulwark"
systemctl stop bulwark
msg_ok "Stopped Bulwark"
NODE_VERSION="24" setup_nodejs
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bulwark" "bulwarkmail/webmail" "prebuild" "latest" "/opt/bulwark" "bulwark-standalone-*-linux-$(arch_resolve).tar.gz"
msg_info "Starting Bulwark"
systemctl start bulwark
msg_ok "Started Bulwark"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
echo -e "${INFO}${YW}Setup wizard token: journalctl -u bulwark (inside the container)${CL}"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/chroma-core/chroma
APP="Chroma"
var_tags="${var_tags:-database;vector;ai}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/chroma ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
CURRENT=$(cat ~/.chroma)
RELEASE=$(curl -fsSL https://pypi.org/pypi/chromadb/json | jq -r '.info.version')
if [[ "$CURRENT" == "$RELEASE" ]]; then
msg_ok "No update available: Chroma (${CURRENT})"
exit
fi
msg_info "Stopping Chroma"
systemctl stop chroma
msg_ok "Stopped Chroma"
PYTHON_VERSION="3.12" setup_uv
msg_info "Updating Chroma to ${RELEASE}"
$STD uv pip install --upgrade --python /opt/chroma/.venv/bin/python "chromadb==${RELEASE}"
cat <<EOF >~/.chroma
${RELEASE}
EOF
msg_ok "Updated Chroma to ${RELEASE}"
msg_info "Starting Chroma"
systemctl start chroma
msg_ok "Started Chroma"
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8000/docs${CL}"
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://colanode.com/
APP="Colanode"
var_tags="${var_tags:-collaboration;notes;chat}"
var_cpu="${var_cpu:-4}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-16}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/colanode ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "colanode" "colanode/colanode"; then
msg_info "Stopping Services"
systemctl stop colanode-server
msg_ok "Stopped Services"
create_backup /opt/colanode/.env
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "colanode" "colanode/colanode" "tarball"
msg_info "Rebuilding Application"
cd /opt/colanode
export NODE_OPTIONS="--max-old-space-size=4096"
$STD npm install
$STD npm run build -w @colanode/core
$STD npm run build -w @colanode/crdt
$STD npm run build -w @colanode/server
$STD npm run build -w @colanode/client
$STD npm run build -w @colanode/ui
$STD npm run build -w @colanode/web
cp -r /opt/colanode/apps/web/dist/. /var/www/colanode/
$STD npm prune --production
unset NODE_OPTIONS
msg_ok "Rebuilt Application"
restore_backup
msg_info "Starting Services"
systemctl start colanode-server
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URLs:${CL}"
echo -e "${GATEWAY}${BGN}https://${IP}:4000${CL} (Web UI)"
echo -e "${INFO}${YW} Before using: import the self-signed cert into your browser:${CL}"
echo -e "${GATEWAY}${BGN}https://${IP}:4000/colanode.crt${CL}"
echo -e "${INFO}${YW} Server URL to use inside the app:${CL}"
echo -e "${GATEWAY}${BGN}https://${IP}:4000/config${CL}"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/get-convex/convex-backend
APP="Convex"
var_tags="${var_tags:-backend;database;realtime}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/convex ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "convex" "get-convex/convex-backend"; then
msg_info "Stopping Convex"
systemctl stop convex
msg_ok "Stopped Convex"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "convex" "get-convex/convex-backend" "prebuild" "latest" "/opt/convex" "convex-local-backend-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.zip"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "convex-dashboard" "get-convex/convex-backend" "prebuild" "$(cat ~/.convex)" "/opt/convex-dashboard" "dashboard.zip"
msg_info "Starting Convex"
systemctl start convex
msg_ok "Started Convex"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access the dashboard using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:6791${CL}"
echo -e "${INFO}${YW}Deployment URL: http://${IP}:3210, admin key in /opt/convex_data/.env${CL}"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/cypht-org/cypht
APP="Cypht"
var_tags="${var_tags:-email;webmail}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_user="${var_admin_user:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/cypht ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "cypht" "cypht-org/cypht"; then
msg_info "Stopping Cypht"
systemctl stop nginx php8.4-fpm
msg_ok "Stopped Cypht"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cypht" "cypht-org/cypht" "prebuild" "latest" "/opt/cypht" "cypht.tar.gz"
msg_info "Updating Cypht"
ln -sf /opt/cypht_data/.env /opt/cypht/.env
cd /opt/cypht
$STD php scripts/setup_database.php
$STD php scripts/config_gen.php
chown -R www-data:www-data /opt/cypht_data/users /opt/cypht_data/attachments
msg_ok "Updated Cypht"
msg_info "Starting Cypht"
systemctl start php8.4-fpm nginx
msg_ok "Started Cypht"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
echo -e "${INFO}${YW}Login: /opt/cypht_data/.env (inside the container)${CL}"
+4 -4
View File
@@ -4,17 +4,17 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Kristian Skov
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-9.0&tabs=linux-ubuntu
# Source: https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian
APP="Dotnet ASP Web API"
var_tags="${var_tags:-web}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-8}"
var_os="${var_os:-Debian}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-0}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
@@ -43,4 +43,4 @@ description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following IP:${CL}"
echo -e "${GATEWAY}${BGN}${IP}:80${CL}"
echo -e "${GATEWAY}${BGN}${IP}:80${CL}"
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/Gentleman-Programming/engram
APP="Engram"
var_tags="${var_tags:-ai;mcp;memory}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_cloud_token="${var_cloud_token:-}"
export var_admin_token="${var_admin_token:-}"
export var_allowed_projects="${var_allowed_projects:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/engram/engram ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "engram" "Gentleman-Programming/engram"; then
msg_info "Stopping Engram"
systemctl stop engram
msg_ok "Stopped Engram"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "engram" "Gentleman-Programming/engram" "prebuild" "latest" "/opt/engram" "engram_*_linux_$(arch_resolve).tar.gz"
msg_info "Starting Engram"
systemctl start engram
msg_ok "Started Engram"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}https://${IP}/dashboard/${CL}"
echo -e "${INFO}${YW}Sync and admin tokens: /opt/engram_data/.env (inside the container)${CL}"
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/Euro-Office/DocumentServer
APP="Euro-Office"
var_tags="${var_tags:-office;documents;collaboration}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /var/www/euro-office/documentserver ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "euro-office" "Euro-Office/DocumentServer"; then
msg_info "Stopping Euro-Office"
$STD documentserver-prepare4shutdown.sh
systemctl stop ds-docservice ds-converter ds-metrics ds-example
msg_ok "Stopped Euro-Office"
fetch_and_deploy_gh_release "euro-office" "Euro-Office/DocumentServer" "binary" "latest" "" "euro-office-documentserver_*_$(arch_resolve).deb"
msg_info "Starting Euro-Office"
systemctl start ds-metrics ds-converter ds-docservice
if systemctl is-enabled -q ds-example; then
systemctl start ds-example
fi
systemctl reload nginx
msg_ok "Started Euro-Office"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
echo -e "${INFO}${YW}JWT secret: documentserver-jwt-status.sh (inside the container)${CL}"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/arp242/goatcounter
APP="GoatCounter"
var_tags="${var_tags:-analytics;privacy}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-2}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_email="${var_admin_email:-}"
export var_admin_pass="${var_admin_pass:-}"
export var_goatcounter_domain="${var_goatcounter_domain:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/goatcounter/goatcounter ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "goatcounter" "arp242/goatcounter"; then
msg_info "Stopping GoatCounter"
systemctl stop goatcounter
msg_ok "Stopped GoatCounter"
fetch_and_deploy_gh_release "goatcounter" "arp242/goatcounter" "singlefile" "latest" "/opt/goatcounter" "goatcounter-v*-linux-$(arch_resolve).gz"
mv /opt/goatcounter/goatcounter /opt/goatcounter/goatcounter.gz
gunzip -f /opt/goatcounter/goatcounter.gz
msg_info "Starting GoatCounter"
systemctl start goatcounter
msg_ok "Started GoatCounter"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8080${CL}"
echo -e "${INFO}${YW}Admin login: /opt/goatcounter_data/.env (inside the container)${CL}"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/hatchet-dev/hatchet
APP="Hatchet"
var_tags="${var_tags:-workflow;queue;automation}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
export var_admin_email="${var_admin_email:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/hatchet ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "hatchet" "hatchet-dev/hatchet"; then
msg_info "Stopping Hatchet"
systemctl stop hatchet
msg_ok "Stopped Hatchet"
setup_go
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "hatchet" "hatchet-dev/hatchet" "tarball"
NODE_VERSION="24" NODE_MODULE="$(jq -r '.packageManager' /opt/hatchet/frontend/app/package.json)" setup_nodejs
msg_info "Building Hatchet"
cd /opt/hatchet/frontend/snippets
$STD python3 generate.py
cd /opt/hatchet/frontend/app
$STD pnpm install --frozen-lockfile
$STD pnpm run build
cd /opt/hatchet
for target in lite admin migrate; do
CGO_ENABLED=0 $STD go build -ldflags="-w -s -X main.Version=v$(cat ~/.hatchet)" -o "bin/hatchet-${target}" "./cmd/hatchet-${target}"
done
rm -rf /opt/hatchet/frontend/app/node_modules ~/.local/share/pnpm/store
$STD go clean -cache -modcache
msg_ok "Built Hatchet"
msg_info "Starting Hatchet"
systemctl start hatchet
msg_ok "Started Hatchet"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8888${CL}"
echo -e "${INFO}${YW}Admin login: /opt/hatchet_data/.env (inside the container)${CL}"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/inngest/inngest
APP="Inngest"
var_tags="${var_tags:-automation;queue;workflow}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/inngest ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "inngest" "inngest/inngest"; then
msg_info "Stopping Inngest"
systemctl stop inngest
msg_ok "Stopped Inngest"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "inngest" "inngest/inngest" "prebuild" "latest" "/opt/inngest" "inngest_*_linux_$(arch_resolve).tar.gz"
msg_info "Starting Inngest"
systemctl start inngest
msg_ok "Started Inngest"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8288${CL}"
echo -e "${INFO}${YW}Event and signing keys: /opt/inngest_data/.env (inside the container)${CL}"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/langflow-ai/langflow
APP="Langflow"
var_tags="${var_tags:-ai;llm;agents}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
export var_admin_user="${var_admin_user:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/langflow ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "langflow" "langflow-ai/langflow"; then
msg_info "Stopping Langflow"
systemctl stop langflow
msg_ok "Stopped Langflow"
PYTHON_VERSION="3.14" USE_UVX="YES" setup_uv
msg_info "Updating Langflow"
$STD uv venv --clear --python 3.14 /opt/langflow/.venv
$STD uv pip install --python /opt/langflow/.venv/bin/python "langflow==${CHECK_UPDATE_RELEASE#v}"
cat <<EOF >~/.langflow
${CHECK_UPDATE_RELEASE#v}
EOF
msg_ok "Updated Langflow"
msg_info "Starting Langflow"
systemctl start langflow
msg_ok "Started Langflow"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:7860${CL}"
echo -e "${INFO}${YW}Login: LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD in /opt/langflow_data/.env (inside the container)${CL}"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/marimo-team/marimo
APP="marimo"
var_tags="${var_tags:-notebook;python;data}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-6}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_token_password="${var_token_password:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/marimo ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "marimo" "marimo-team/marimo"; then
msg_info "Stopping marimo"
systemctl stop marimo
msg_ok "Stopped marimo"
PYTHON_VERSION="3.13" setup_uv
msg_info "Updating marimo"
$STD uv pip install --upgrade --python /opt/marimo/.venv/bin/python "marimo[recommended]==${CHECK_UPDATE_RELEASE}" pandas
cat <<EOF >~/.marimo
${CHECK_UPDATE_RELEASE}
EOF
msg_ok "Updated marimo"
msg_info "Starting marimo"
systemctl start marimo
msg_ok "Started marimo"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:2718${CL}"
echo -e "${INFO}${YW}Password: MARIMO_TOKEN_PASSWORD in /opt/marimo_data/.env (inside the container)${CL}"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/maplibre/martin
APP="Martin"
var_tags="${var_tags:-maps;gis;tiles}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -x /opt/martin/martin ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "martin" "maplibre/martin"; then
msg_info "Stopping Martin"
systemctl stop martin
msg_ok "Stopped Martin"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "martin" "maplibre/martin" "prebuild" "latest" "/opt/martin" "martin-$(arch_resolve "x86_64" "aarch64")-unknown-linux-musl.tar.gz"
msg_info "Starting Martin"
systemctl start martin
msg_ok "Started Martin"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/QuantumNous/new-api
APP="New-API"
var_tags="${var_tags:-ai;llm;gateway}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_user="${var_admin_user:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/new-api/new-api ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "new-api" "QuantumNous/new-api"; then
msg_info "Stopping New API"
systemctl stop new-api
msg_ok "Stopped New API"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "new-api" "QuantumNous/new-api" "singlefile" "latest" "/opt/new-api" "$(arch_resolve "new-api-v" "new-api-arm64-v")*[0-9]"
msg_info "Starting New API"
systemctl start new-api
msg_ok "Started New API"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
echo -e "${INFO}${YW}Admin login: /opt/new-api_data/.env (inside the container)${CL}"
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/ccfos/nightingale
APP="Nightingale"
var_tags="${var_tags:-monitoring;alerting}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/nightingale ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "nightingale" "ccfos/nightingale"; then
msg_info "Stopping Nightingale"
systemctl stop nightingale
msg_ok "Stopped Nightingale"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "nightingale" "ccfos/nightingale" "prebuild" "latest" "/opt/nightingale" "n9e-v*-linux-$(arch_resolve).tar.gz"
msg_info "Starting Nightingale"
systemctl start nightingale
msg_ok "Started Nightingale"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:17000${CL}"
+1 -1
View File
@@ -52,4 +52,4 @@ description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8083${CL}"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/simple-login/app
APP="SimpleLogin"
var_tags="${var_tags:-email;privacy;alias}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/simplelogin ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "simplelogin" "simple-login/app"; then
msg_info "Stopping Services"
systemctl stop simplelogin-webapp simplelogin-email simplelogin-job
msg_ok "Stopped Services"
create_backup /opt/simplelogin/.env \
/opt/simplelogin/dkim \
/opt/simplelogin/openid-rsa.key \
/opt/simplelogin/openid-rsa.pub \
/opt/simplelogin/uploads \
/opt/simplelogin/.gnupg
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "simplelogin" "simple-login/app"
restore_backup
grep -q '^METRICS_BACKENDS=' /opt/simplelogin/.env || echo "METRICS_BACKENDS=noop" >>/opt/simplelogin/.env
msg_info "Installing Dependencies"
cd /opt/simplelogin
$STD uv venv
$STD uv pip install setuptools hatchling editables
export CMAKE_POLICY_VERSION_MINIMUM=3.5
$STD uv sync --locked --no-dev --no-build-isolation --no-install-package newrelic
if [[ -f /opt/simplelogin/static/package.json ]]; then
cd /opt/simplelogin/static
$STD npm ci
fi
msg_ok "Installed Dependencies"
msg_info "Running Database Migrations"
cd /opt/simplelogin
$STD .venv/bin/alembic upgrade head
msg_ok "Ran Database Migrations"
msg_info "Starting Services"
systemctl start simplelogin-webapp simplelogin-email simplelogin-job
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/spacedriveapp/spacebot
APP="Spacebot"
var_tags="${var_tags:-ai;agent;chatops}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-6}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_llm_base_url="${var_llm_base_url:-}"
export var_llm_api_key="${var_llm_api_key:-}"
export var_llm_model="${var_llm_model:-}"
if [[ -n "$var_llm_base_url" && -z "$var_llm_model" ]]; then
msg_error "var_llm_model is required with var_llm_base_url."
exit 1
fi
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/spacebot ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "spacebot" "spacedriveapp/spacebot"; then
msg_info "Stopping Spacebot"
systemctl stop spacebot
msg_ok "Stopped Spacebot"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "spacebot" "spacedriveapp/spacebot" "prebuild" "latest" "/opt/spacebot" "spacebot-v*-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz"
msg_info "Starting Spacebot"
systemctl start spacebot
msg_ok "Started Spacebot"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:19898${CL}"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Renan Bastos (renanbastos93)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/temporalio/temporal
APP="Temporal"
var_tags="${var_tags:-workflow;automation}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-no}" # unset = ask the user; server, UI and CLI all ship linux_arm64 assets, but not yet run on arm64
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/temporal ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "temporal" "temporalio/temporal"; then
msg_info "Stopping Temporal"
systemctl stop temporal-ui temporal
msg_ok "Stopped Temporal"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "temporal" "temporalio/temporal" "prebuild" "latest" "/opt/temporal" "temporal_*_linux_$(arch_resolve).tar.gz"
msg_info "Migrating Temporal Database"
set -a && source /etc/temporal/temporal.env && set +a
$STD /opt/temporal/temporal-sql-tool --db temporal update-schema --schema-name postgresql/v12/temporal
$STD /opt/temporal/temporal-sql-tool --db temporal_visibility update-schema --schema-name postgresql/v12/visibility
msg_ok "Migrated Temporal Database"
msg_info "Starting Temporal"
systemctl start temporal temporal-ui
msg_ok "Started Temporal"
msg_ok "Updated Temporal successfully!"
fi
if check_for_gh_release "temporal-ui" "temporalio/ui-server"; then
msg_info "Stopping Temporal UI"
systemctl stop temporal-ui
msg_ok "Stopped Temporal UI"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "temporal-ui" "temporalio/ui-server" "prebuild" "latest" "/opt/temporal-ui" "ui-server_*_linux_$(arch_resolve).tar.gz"
msg_info "Starting Temporal UI"
systemctl start temporal-ui
msg_ok "Started Temporal UI"
msg_ok "Updated Temporal UI successfully!"
fi
if check_for_gh_release "temporal-cli" "temporalio/cli"; then
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "temporal-cli" "temporalio/cli" "prebuild" "latest" "/opt/temporal-cli" "temporal_cli_*_linux_$(arch_resolve).tar.gz"
msg_ok "Updated Temporal CLI successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8080${CL}"
echo -e "${INFO}${YW}gRPC endpoint for workers and SDKs: ${IP}:7233${CL}"
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/trailbaseio/trailbase
APP="TrailBase"
var_tags="${var_tags:-backend;database;api}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_email="${var_admin_email:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/trailbase ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "trailbase" "trailbaseio/trailbase"; then
msg_info "Stopping TrailBase"
systemctl stop trailbase
msg_ok "Stopped TrailBase"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "trailbase" "trailbaseio/trailbase" "prebuild" "latest" "/opt/trailbase" "trailbase_v*_$(arch_resolve "x86_64" "aarch64")_linux.zip"
msg_info "Starting TrailBase"
systemctl start trailbase
msg_ok "Started TrailBase"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:4000/_/admin/${CL}"
echo -e "${INFO}${YW}Admin login: /opt/trailbase_data/.env (inside the container)${CL}"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/unslothai/unsloth
APP="Unsloth"
var_tags="${var_tags:-ai;llm}"
var_cpu="${var_cpu:-4}"
var_ram="${var_ram:-8192}"
var_disk="${var_disk:-40}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_gpu="${var_gpu:-yes}"
var_arm64="${var_arm64:-no}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/unsloth ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Stopping Service"
systemctl stop unsloth
msg_ok "Stopped Service"
if [[ -d /dev/dri || -e /dev/nvidia0 || -e /dev/kfd ]]; then
ENABLE_GPU="yes" setup_hwaccel
fi
msg_info "Updating ${APP} (Patience)"
curl -fsSL https://unsloth.ai/install.sh -o /tmp/unsloth-install.sh
export UNSLOTH_STUDIO_HOME=/opt/unsloth
export UNSLOTH_SKIP_AUTOSTART=1
export UNSLOTH_PYTHON=3.12
$STD sh /tmp/unsloth-install.sh
unset UNSLOTH_SKIP_AUTOSTART UNSLOTH_PYTHON
rm -f /tmp/unsloth-install.sh
msg_ok "Updated ${APP}"
msg_info "Starting Service"
systemctl start unsloth
msg_ok "Started Service"
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8888${CL}"
echo -e "${INFO}${YW}Login credentials: ~/unsloth.creds (inside the container)${CL}"
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/weaviate/weaviate
APP="Weaviate"
var_tags="${var_tags:-database;vector;ai}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_api_key="${var_api_key:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/weaviate ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "weaviate" "weaviate/weaviate"; then
msg_info "Stopping Weaviate"
systemctl stop weaviate
msg_ok "Stopped Weaviate"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "weaviate" "weaviate/weaviate" "prebuild" "latest" "/opt/weaviate" "weaviate-v*-linux-$(arch_resolve).tar.gz"
msg_info "Starting Weaviate"
systemctl start weaviate
msg_ok "Started Weaviate"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8080${CL}"
echo -e "${INFO}${YW}API key (user admin): /opt/weaviate_data/.env (inside the container)${CL}"
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/decolua/9router
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
NODE_VERSION="22" setup_nodejs
setup_ffmpeg
msg_info "Installing 9Router"
$STD npm install -g --ignore-scripts "9router@${var_appversion:-latest}"
msg_ok "Installed 9Router"
msg_info "Configuring 9Router"
mkdir -p /opt/9router_data
cat <<EOF >/opt/9router_data/.env
DATA_DIR=/opt/9router_data
PORT=20128
HOSTNAME=0.0.0.0
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
BASE_URL=http://localhost:20128
INITIAL_PASSWORD=$(random_password)
JWT_SECRET=$(openssl rand -hex 32)
API_KEY_SECRET=$(openssl rand -hex 32)
MACHINE_ID_SALT=$(openssl rand -hex 16)
EOF
chmod 600 /opt/9router_data/.env
msg_ok "Configured 9Router"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/9router.service
[Unit]
Description=9Router
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/usr/lib/node_modules/9router/app
EnvironmentFile=/opt/9router_data/.env
ExecStart=/usr/bin/node /usr/lib/node_modules/9router/app/custom-server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now 9router
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Chris Moriarty (cwmoriarty)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/philippe44/AirConnect
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "airconnect" "philippe44/AirConnect" "prebuild" "latest" "/opt/airconnect" "AirConnect-*.zip"
msg_info "Configuring AirConnect"
chmod +x /opt/airconnect/airupnp-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/aircast-linux-$(arch_resolve x86_64 aarch64)
ln -sf /opt/airconnect/airupnp-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/airupnp
ln -sf /opt/airconnect/aircast-linux-$(arch_resolve x86_64 aarch64) /opt/airconnect/aircast
find /opt/airconnect -maxdepth 1 -type f ! -name "air*linux-$(arch_resolve x86_64 aarch64)*" ! -name "airupnp" ! -name "aircast" ! -name "*.xml" -delete
msg_ok "Configured AirConnect"
msg_info "Creating Services"
cat <<EOF >/etc/systemd/system/airupnp.service
[Unit]
Description=AirUPnP bridge
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/airconnect
ExecStart=/opt/airconnect/airupnp -l 1000:2000 -Z -x /opt/airconnect/airupnp.xml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now airupnp
cat <<EOF >/etc/systemd/system/aircast.service
[Unit]
Description=AirCast bridge
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/airconnect
ExecStart=/opt/airconnect/aircast -Z -x /opt/airconnect/aircast.xml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now aircast
msg_ok "Created Services"
motd_ssh
customize
cleanup_lxc
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/bugsink/bugsink
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
PYTHON_VERSION="3.12" setup_uv
msg_info "Installing Bugsink"
BUGSINK_VERSION="${var_appversion:-$(get_latest_github_release "bugsink/bugsink")}"
$STD uv venv --python 3.12 /opt/bugsink/.venv
$STD uv pip install --python /opt/bugsink/.venv/bin/python "bugsink==${BUGSINK_VERSION}"
cat <<EOF >~/.bugsink
${BUGSINK_VERSION}
EOF
msg_ok "Installed Bugsink"
msg_info "Configuring Bugsink"
ADMIN_EMAIL="${var_admin_email:-admin@localhost}"
ADMIN_PASS="${var_admin_pass:-$(random_password)}"
mkdir -p /opt/bugsink_data
cd /opt/bugsink_data
$STD /opt/bugsink/.venv/bin/bugsink-create-conf --template=singleserver --host="${LOCAL_IP}" --base-dir=/opt/bugsink_data
sed -i \
-e "s|\"BASE_URL\": \"https://${LOCAL_IP}\"|\"BASE_URL\": \"http://${LOCAL_IP}:8000\"|" \
-e 's|^SESSION_COOKIE_SECURE = True|SESSION_COOKIE_SECURE = False|' \
-e 's|^CSRF_COOKIE_SECURE = True|CSRF_COOKIE_SECURE = False|' \
-e 's|^USE_X_REAL_IP = True|USE_X_REAL_IP = False|' \
/opt/bugsink_data/bugsink_conf.py
$STD /opt/bugsink/.venv/bin/bugsink-manage migrate
$STD /opt/bugsink/.venv/bin/bugsink-manage migrate snappea --database=snappea
DJANGO_SUPERUSER_PASSWORD="$ADMIN_PASS" $STD /opt/bugsink/.venv/bin/bugsink-manage createsuperuser --noinput --username "$ADMIN_EMAIL" --email "$ADMIN_EMAIL"
cat <<EOF >/opt/bugsink_data/.env
BUGSINK_ADMIN_EMAIL=${ADMIN_EMAIL}
BUGSINK_ADMIN_PASSWORD=${ADMIN_PASS}
EOF
chmod 600 /opt/bugsink_data/.env /opt/bugsink_data/bugsink_conf.py
msg_ok "Configured Bugsink"
msg_info "Creating Services"
cat <<EOF >/etc/systemd/system/bugsink.service
[Unit]
Description=Bugsink
After=network.target
[Service]
Type=notify
WorkingDirectory=/opt/bugsink_data
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/bugsink/.venv/bin/gunicorn --bind=0.0.0.0:8000 --workers=4 --max-requests=1000 --max-requests-jitter=100 --access-logfile - bugsink.wsgi
Restart=always
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/bugsink-snappea.service
[Unit]
Description=Bugsink Snappea
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/bugsink_data
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/bugsink/.venv/bin/bugsink-runsnappea
Restart=always
RuntimeMaxSec=1d
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now bugsink bugsink-snappea
msg_ok "Created Services"
motd_ssh
customize
cleanup_lxc
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/bulwarkmail/webmail
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "bulwark" "bulwarkmail/webmail" "prebuild" "latest" "/opt/bulwark" "bulwark-standalone-*-linux-$(arch_resolve).tar.gz"
msg_info "Configuring Bulwark"
mkdir -p /opt/bulwark_data
cat <<EOF >/opt/bulwark_data/.env
NODE_ENV=production
HOSTNAME=0.0.0.0
PORT=3000
JMAP_SERVER_URL=${var_jmap_url:-}
COOKIE_SECURE=false
ADMIN_CONFIG_DIR=/opt/bulwark_data/admin
ADMIN_STATE_DIR=/opt/bulwark_data/admin-state
SETTINGS_DATA_DIR=/opt/bulwark_data/settings
PLUGIN_SECRETS_DIR=/opt/bulwark_data/plugin-secrets
TELEMETRY_DATA_DIR=/opt/bulwark_data/telemetry
VERSION_CHECK_DATA_DIR=/opt/bulwark_data/version-check
EOF
chmod 600 /opt/bulwark_data/.env
msg_ok "Configured Bulwark"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/bulwark.service
[Unit]
Description=Bulwark Webmail
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/bulwark
EnvironmentFile=/opt/bulwark_data/.env
ExecStart=/usr/bin/node /opt/bulwark/server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now bulwark
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/chroma-core/chroma
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
PYTHON_VERSION="3.12" setup_uv
CHROMA_VERSION="${var_appversion:-$(curl -fsSL https://pypi.org/pypi/chromadb/json | jq -r '.info.version')}"
msg_info "Installing Chroma ${CHROMA_VERSION}"
$STD uv venv --python 3.12 /opt/chroma/.venv
$STD uv pip install --python /opt/chroma/.venv/bin/python "chromadb==${CHROMA_VERSION}"
cat <<EOF >~/.chroma
${CHROMA_VERSION}
EOF
msg_ok "Installed Chroma ${CHROMA_VERSION}"
msg_info "Configuring Chroma"
mkdir -p /opt/chroma_data
cat <<EOF >/opt/chroma_data/config.yaml
persist_path: "/opt/chroma_data"
listen_address: "0.0.0.0"
port: 8000
EOF
msg_ok "Configured Chroma"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/chroma.service
[Unit]
Description=Chroma
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/chroma_data
ExecStart=/opt/chroma/.venv/bin/chroma run /opt/chroma_data/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now chroma
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://colanode.com/
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
redis-server \
nginx
msg_ok "Installed Dependencies"
PG_VERSION="17" PG_MODULES="pgvector" setup_postgresql
PG_DB_NAME="colanode_db" PG_DB_USER="colanode" PG_DB_EXTENSIONS="vector" setup_postgresql_db
NODE_VERSION="22" setup_nodejs
fetch_and_deploy_gh_release "colanode" "colanode/colanode" "tarball"
msg_info "Building Application"
cd /opt/colanode
export NODE_OPTIONS="--max-old-space-size=4096"
$STD npm install
$STD npm run build -w @colanode/core
$STD npm run build -w @colanode/crdt
$STD npm run build -w @colanode/server
$STD npm run build -w @colanode/client
$STD npm run build -w @colanode/ui
$STD npm run build -w @colanode/web
$STD npm prune --production
unset NODE_OPTIONS
msg_ok "Built Application"
msg_info "Configuring Application"
mkdir -p /var/lib/colanode/storage /var/www/colanode
cp -r /opt/colanode/apps/web/dist/. /var/www/colanode/
cat <<EOF >/opt/colanode/.env
POSTGRES_URL=postgresql://${PG_DB_USER}:${PG_DB_PASS}@127.0.0.1:5432/${PG_DB_NAME}
REDIS_URL=redis://127.0.0.1:6379
NODE_ENV=production
EOF
msg_ok "Configured Application"
msg_info "Configuring Nginx"
create_self_signed_cert "colanode"
# Make cert available for browser import (required for Service Worker to work)
cp /etc/ssl/colanode/colanode.crt /var/www/colanode/colanode.crt
cat <<EOF >/etc/nginx/sites-available/colanode
server {
listen 4000 ssl;
server_name _;
root /var/www/colanode;
index index.html;
ssl_certificate /etc/ssl/colanode/colanode.crt;
ssl_certificate_key /etc/ssl/colanode/colanode.key;
# Required for SharedArrayBuffer / OPFS SQLite (WASM)
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# Proxy API and WebSocket traffic to the Node.js server
location ~ ^/(config|client)(/.*)?$ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# Serve self-signed cert for browser import
location = /colanode.crt {
default_type application/x-x509-ca-cert;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
ln -sf /etc/nginx/sites-available/colanode /etc/nginx/sites-enabled/colanode
rm -f /etc/nginx/sites-enabled/default
systemctl reload nginx
msg_ok "Configured Nginx"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/colanode-server.service
[Unit]
Description=Colanode Server
After=network.target postgresql.service redis-server.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/colanode
EnvironmentFile=/opt/colanode/.env
ExecStart=/usr/bin/node apps/server/dist/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now colanode-server
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/get-convex/convex-backend
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y nginx
msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "convex" "get-convex/convex-backend" "prebuild" "latest" "/opt/convex" "convex-local-backend-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.zip"
fetch_and_deploy_gh_release "convex-dashboard" "get-convex/convex-backend" "prebuild" "$(cat ~/.convex)" "/opt/convex-dashboard" "dashboard.zip"
msg_info "Configuring Convex"
mkdir -p /opt/convex_data/storage /opt/convex_data/tmp
INSTANCE_SECRET=$(openssl rand -hex 32)
ADMIN_KEY=$(/opt/convex/convex-local-backend keygen admin-key --instance-name convex-self-hosted --instance-secret "$INSTANCE_SECRET")
cat <<EOF >/opt/convex_data/.env
INSTANCE_NAME=convex-self-hosted
INSTANCE_SECRET=${INSTANCE_SECRET}
CONVEX_SELF_HOSTED_ADMIN_KEY="${ADMIN_KEY}"
CONVEX_CLOUD_ORIGIN=http://${LOCAL_IP}:3210
CONVEX_SITE_ORIGIN=http://${LOCAL_IP}:3211
TMPDIR=/opt/convex_data/tmp
RUST_LOG=info
DOCUMENT_RETENTION_DELAY=172800
DISABLE_METRICS_ENDPOINT=true
APPLICATION_MAX_CONCURRENT_MUTATIONS=16
APPLICATION_MAX_CONCURRENT_QUERIES=16
APPLICATION_MAX_CONCURRENT_V8_ACTIONS=16
APPLICATION_MAX_CONCURRENT_NODE_ACTIONS=16
EOF
chmod 600 /opt/convex_data/.env
msg_ok "Configured Convex"
msg_info "Configuring Nginx"
cat <<EOF >/etc/nginx/sites-available/convex-dashboard
server {
listen 6791;
server_name _;
root /opt/convex-dashboard;
index index.html;
location / {
try_files \$uri \$uri.html \$uri/ =404;
}
}
EOF
nginx_enable_site "convex-dashboard"
msg_ok "Configured Nginx"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/convex.service
[Unit]
Description=Convex Backend
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/convex_data
EnvironmentFile=/opt/convex_data/.env
ExecStart=/opt/convex/convex-local-backend --instance-name \${INSTANCE_NAME} --instance-secret \${INSTANCE_SECRET} --port 3210 --site-proxy-port 3211 --convex-origin \${CONVEX_CLOUD_ORIGIN} --convex-site \${CONVEX_SITE_ORIGIN} --local-storage /opt/convex_data/storage /opt/convex_data/db.sqlite3
KillSignal=SIGINT
TimeoutStopSec=10
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now convex
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/cypht-org/cypht
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y nginx
msg_ok "Installed Dependencies"
PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="soap,ldap" setup_php
fetch_and_deploy_gh_release "cypht" "cypht-org/cypht" "prebuild" "latest" "/opt/cypht" "cypht.tar.gz"
msg_info "Configuring Cypht"
var_admin_user="${var_admin_user:-admin}"
var_admin_pass="${var_admin_pass:-$(random_password)}"
mkdir -p /opt/cypht_data/{users,attachments}
cp /opt/cypht/.env /opt/cypht_data/.env
sed -i \
-e 's|^DB_DRIVER=.*|DB_DRIVER=sqlite|' \
-e 's|^DB_NAME=.*|DB_NAME=/opt/cypht_data/users/cypht.db|' \
-e 's|^SESSION_TYPE=.*|SESSION_TYPE=DB|' \
-e 's|^AUTH_TYPE=.*|AUTH_TYPE=DB|' \
-e 's|^USER_CONFIG_TYPE=.*|USER_CONFIG_TYPE=DB|' \
-e 's|^USER_SETTINGS_DIR=.*|USER_SETTINGS_DIR=/opt/cypht_data/users|' \
-e 's|^ATTACHMENT_DIR=.*|ATTACHMENT_DIR=/opt/cypht_data/attachments|' \
-e "s|^ADMIN_USERS=.*|ADMIN_USERS=${var_admin_user}|" \
-e 's|^ENABLE_REDIS=.*|ENABLE_REDIS=false|' \
-e 's|^ENABLE_MEMCACHED=.*|ENABLE_MEMCACHED=false|' \
/opt/cypht_data/.env
cat <<EOF >>/opt/cypht_data/.env
CYPHT_ADMIN_USER='${var_admin_user}'
CYPHT_ADMIN_PASS='${var_admin_pass}'
EOF
ln -sf /opt/cypht_data/.env /opt/cypht/.env
cd /opt/cypht
$STD php scripts/setup_database.php
$STD php scripts/create_account.php "$var_admin_user" "$var_admin_pass"
$STD php scripts/config_gen.php
chown -R www-data:www-data /opt/cypht_data/users /opt/cypht_data/attachments
chown root:www-data /opt/cypht_data/.env
chmod 640 /opt/cypht_data/.env
msg_ok "Configured Cypht"
msg_info "Configuring Nginx"
PHP_SOCK=$(get_php_fpm_socket)
cat <<EOF >/etc/nginx/sites-available/cypht
server {
listen 80;
server_name _;
root /opt/cypht/site;
index index.php;
client_max_body_size 60M;
location / {
try_files \$uri /index.php\$is_args\$args;
}
location ~ \.php\$ {
try_files \$uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)\$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_index index.php;
fastcgi_pass unix:${PHP_SOCK};
}
}
EOF
nginx_enable_site "cypht"
msg_ok "Configured Nginx"
motd_ssh
customize
cleanup_lxc
+40 -14
View File
@@ -3,7 +3,7 @@
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Kristian Skov
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-9.0&tabs=linux-ubuntu
# Source: https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
@@ -14,21 +14,36 @@ network_check
update_os
msg_info "Installing Dependencies"
apt_update_safe
$STD apt install -y \
ssh \
software-properties-common
$STD add-apt-repository -y ppa:dotnet/backports
$STD apt install -y \
dotnet-sdk-9.0 \
vsftpd \
nginx
msg_ok "Installed Dependencies"
var_project_name="${var_project_name:-}"
[[ -n "$var_project_name" ]] || read -r -p "${TAB3}Type the assembly name of the project: " var_project_name || true
[[ -n "$var_project_name" ]] || { msg_error "No assembly name given; set var_project_name or answer the prompt"; exit 64; }
setup_deb822_repo \
"microsoft" \
"https://packages.microsoft.com/keys/microsoft-2025.asc" \
"https://packages.microsoft.com/debian/13/prod/" \
"trixie" \
"main"
if [[ -z "${var_dotnet_version:-}" && -t 0 ]]; then
read -r -t 60 -p "${TAB3}.NET SDK version, 10 (LTS) or 11 [10]: " var_dotnet_version || echo
fi
var_dotnet_version="${var_dotnet_version:-10}"
var_dotnet_version="${var_dotnet_version%%.*}"
if ! apt-cache show "dotnet-sdk-${var_dotnet_version}.0" &>/dev/null; then
msg_warn ".NET SDK ${var_dotnet_version} is not available for this system, using 10"
var_dotnet_version="10"
fi
if [[ -z "${var_project_name:-}" && -t 0 ]]; then
read -r -t 60 -p "${TAB3}Assembly name of the project [testproject]: " var_project_name || echo
fi
var_project_name="${var_project_name:-testproject}"
msg_info "Installing .NET SDK ${var_dotnet_version}"
$STD apt install -y "dotnet-sdk-${var_dotnet_version}.0"
msg_ok "Installed .NET SDK ${var_dotnet_version}"
msg_info "Setting up FTP Server"
useradd ftpuser
@@ -55,7 +70,7 @@ msg_ok "FTP server setup completed"
msg_info "Setting up Nginx Server"
rm -f /var/www/html/index.nginx-debian.html
sed "s/\$var_project_name/$var_project_name/g" >myfile <<'EOF' >/etc/nginx/sites-available/default
sed "s/\$var_project_name/$var_project_name/g" <<'EOF' >/etc/nginx/sites-available/default
map $http_connection $connection_upgrade {
"~*Upgrade" $http_connection;
default keep-alive;
@@ -82,12 +97,12 @@ msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/kestrel-aspnetapi.service
[Unit]
Description=.NET Web API App running on Linux
ConditionPathExists=/var/www/html/$var_project_name.dll
[Service]
WorkingDirectory=/var/www/html
ExecStart=/usr/bin/dotnet /var/www/html/$var_project_name.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-${var_project_name}
@@ -98,7 +113,18 @@ Environment=DOTNET_NOLOGO=true
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now kestrel-aspnetapi
cat <<EOF >/etc/systemd/system/kestrel-aspnetapi.path
[Unit]
Description=Start the .NET Web API App once it is deployed
[Path]
PathExists=/var/www/html/$var_project_name.dll
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q kestrel-aspnetapi
systemctl enable -q --now kestrel-aspnetapi.path
msg_ok "Created Service"
motd_ssh
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/Gentleman-Programming/engram
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y nginx
msg_ok "Installed Dependencies"
PG_VERSION="16" setup_postgresql
PG_DB_NAME="engram" PG_DB_USER="engram" setup_postgresql_db
fetch_and_deploy_gh_release "engram" "Gentleman-Programming/engram" "prebuild" "latest" "/opt/engram" "engram_*_linux_$(arch_resolve).tar.gz"
msg_info "Configuring Engram Cloud"
ln -sf /opt/engram/engram /usr/local/bin/engram
mkdir -p /opt/engram_data
cat <<EOF >/opt/engram_data/.env
ENGRAM_DATABASE_URL=postgres://engram:${PG_DB_PASS}@127.0.0.1:5432/engram?sslmode=disable
ENGRAM_CLOUD_HOST=127.0.0.1
ENGRAM_PORT=18080
ENGRAM_CLOUD_TOKEN=${var_cloud_token:-$(random_password 48)}
ENGRAM_CLOUD_ADMIN=${var_admin_token:-$(random_password 48)}
ENGRAM_JWT_SECRET=$(random_password 64)
ENGRAM_CLOUD_TOKEN_PEPPER=$(random_password 64)
ENGRAM_CLOUD_ALLOWED_PROJECTS=${var_allowed_projects:-*}
EOF
chmod 600 /opt/engram_data/.env
msg_ok "Configured Engram Cloud"
create_self_signed_cert "engram"
msg_info "Configuring Nginx"
cat <<'EOF' >/etc/nginx/sites-available/engram
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl default_server;
server_name _;
client_max_body_size 32m;
ssl_certificate /etc/ssl/engram/engram.crt;
ssl_certificate_key /etc/ssl/engram/engram.key;
location / {
proxy_pass http://127.0.0.1:18080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
}
}
EOF
nginx_enable_site "engram"
msg_ok "Configured Nginx"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/engram.service
[Unit]
Description=Engram Cloud
After=network-online.target postgresql.service
Wants=network-online.target postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/engram_data
EnvironmentFile=/opt/engram_data/.env
ExecStart=/opt/engram/engram cloud serve
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now engram
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/Euro-Office/DocumentServer
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
setup_nonfree
msg_info "Installing Dependencies"
$STD apt install -y \
nginx-extras \
rabbitmq-server
msg_ok "Installed Dependencies"
PG_VERSION="17" setup_postgresql
PG_DB_NAME="euro_office" PG_DB_USER="euro_office" setup_postgresql_db
msg_info "Configuring RabbitMQ"
RMQ_PASS=$(random_password)
$STD rabbitmqctl add_user euro_office "$RMQ_PASS"
$STD rabbitmqctl set_permissions -p / euro_office ".*" ".*" ".*"
msg_ok "Configured RabbitMQ"
msg_info "Preseeding Euro-Office"
debconf-set-selections <<EOF
ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true
euro-office-documentserver ds/db-type string postgres
euro-office-documentserver ds/db-host string localhost
euro-office-documentserver ds/db-port string 5432
euro-office-documentserver ds/db-name string euro_office
euro-office-documentserver ds/db-user string euro_office
euro-office-documentserver ds/db-pwd password ${PG_DB_PASS}
euro-office-documentserver ds/rabbitmq-host string localhost
euro-office-documentserver ds/rabbitmq-user string euro_office
euro-office-documentserver ds/rabbitmq-pwd password ${RMQ_PASS}
euro-office-documentserver ds/jwt-enabled boolean true
euro-office-documentserver ds/jwt-secret string $(random_password 32)
EOF
msg_ok "Preseeded Euro-Office"
fetch_and_deploy_gh_release "euro-office" "Euro-Office/DocumentServer" "binary" "latest" "" "euro-office-documentserver_*_$(arch_resolve).deb"
msg_info "Starting Euro-Office"
systemctl restart ds-metrics ds-converter ds-docservice nginx
for _ in {1..60}; do
[[ "$(curl -fs http://127.0.0.1/healthcheck)" == "true" ]] && break
sleep 2
done
msg_ok "Started Euro-Office"
motd_ssh
customize
cleanup_lxc
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/arp242/goatcounter
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "goatcounter" "arp242/goatcounter" "singlefile" "latest" "/opt/goatcounter" "goatcounter-v*-linux-$(arch_resolve).gz"
mv /opt/goatcounter/goatcounter /opt/goatcounter/goatcounter.gz
gunzip -f /opt/goatcounter/goatcounter.gz
msg_info "Configuring GoatCounter"
ADMIN_EMAIL="${var_admin_email:-admin@goatcounter.local}"
ADMIN_PASS="${var_admin_pass:-$(random_password)}"
mkdir -p /opt/goatcounter_data
cat <<EOF >/opt/goatcounter_data/.env
GOATCOUNTER_DB=sqlite+/opt/goatcounter_data/db.sqlite3
GOATCOUNTER_LISTEN=:8080
GOATCOUNTER_TLS=http
ADMIN_EMAIL=${ADMIN_EMAIL}
ADMIN_PASSWORD=${ADMIN_PASS}
EOF
chmod 600 /opt/goatcounter_data/.env
$STD /opt/goatcounter/goatcounter db create site -createdb \
-db sqlite+/opt/goatcounter_data/db.sqlite3 \
-vhost "${var_goatcounter_domain:-$LOCAL_IP}" \
-user.email "$ADMIN_EMAIL" \
-user.password "$ADMIN_PASS"
msg_ok "Configured GoatCounter"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/goatcounter.service
[Unit]
Description=GoatCounter
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/goatcounter_data
EnvironmentFile=/opt/goatcounter_data/.env
ExecStart=/opt/goatcounter/goatcounter serve -automigrate
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now goatcounter
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/hatchet-dev/hatchet
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
var_admin_email="${var_admin_email:-admin@example.com}"
var_admin_pass="${var_admin_pass:-}"
until [[ ${#var_admin_pass} -ge 8 && ${#var_admin_pass} -le 64 && "$var_admin_pass" =~ [A-Z] && "$var_admin_pass" =~ [a-z] && "$var_admin_pass" =~ [0-9] ]]; do
var_admin_pass=$(random_password)
done
msg_info "Installing Dependencies"
$STD apt install -y python3
msg_ok "Installed Dependencies"
setup_postgresql
PG_DB_NAME="hatchet" PG_DB_USER="hatchet" setup_postgresql_db
setup_go
fetch_and_deploy_gh_release "hatchet" "hatchet-dev/hatchet" "tarball"
NODE_VERSION="24" NODE_MODULE="$(jq -r '.packageManager' /opt/hatchet/frontend/app/package.json)" setup_nodejs
msg_info "Building Hatchet"
cd /opt/hatchet/frontend/snippets
$STD python3 generate.py
cd /opt/hatchet/frontend/app
$STD pnpm install --frozen-lockfile
$STD pnpm run build
cd /opt/hatchet
for target in lite admin migrate; do
CGO_ENABLED=0 $STD go build -ldflags="-w -s -X main.Version=v$(cat ~/.hatchet)" -o "bin/hatchet-${target}" "./cmd/hatchet-${target}"
done
rm -rf /opt/hatchet/frontend/app/node_modules ~/.local/share/pnpm/store
$STD go clean -cache -modcache
msg_ok "Built Hatchet"
msg_info "Configuring Hatchet"
mkdir -p /opt/hatchet_data/config
cat <<EOF >/opt/hatchet_data/.env
DATABASE_URL=postgresql://hatchet:${PG_DB_PASS}@localhost:5432/hatchet?sslmode=disable
SERVER_URL=http://${LOCAL_IP}:8888
SERVER_AUTH_COOKIE_DOMAIN=${LOCAL_IP}
SERVER_AUTH_COOKIE_INSECURE=t
SERVER_AUTH_SET_EMAIL_VERIFIED=t
SERVER_ALLOW_SIGNUP=f
SERVER_GRPC_BIND_ADDRESS=0.0.0.0
SERVER_GRPC_PORT=7077
SERVER_GRPC_INSECURE=t
SERVER_GRPC_BROADCAST_ADDRESS=${LOCAL_IP}:7077
SERVER_INTERNAL_CLIENT_INTERNAL_GRPC_BROADCAST_ADDRESS=localhost:7077
SERVER_MSGQUEUE_KIND=postgres
LITE_STATIC_ASSET_DIR=/opt/hatchet/frontend/app/dist
LITE_FRONTEND_BASE_PATH=/
LITE_FRONTEND_PORT=8081
LITE_RUNTIME_PORT=8888
ADMIN_EMAIL=${var_admin_email}
ADMIN_PASSWORD=${var_admin_pass}
EOF
chmod 600 /opt/hatchet_data/.env
cd /opt/hatchet_data
set -a && source /opt/hatchet_data/.env && set +a
$STD /opt/hatchet/bin/hatchet-migrate
$STD /opt/hatchet/bin/hatchet-admin quickstart --config /opt/hatchet_data/config --generated-config-dir /opt/hatchet_data/config --skip certs --overwrite=false
msg_ok "Configured Hatchet"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/hatchet.service
[Unit]
Description=Hatchet
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
WorkingDirectory=/opt/hatchet_data
EnvironmentFile=/opt/hatchet_data/.env
ExecStartPre=/opt/hatchet/bin/hatchet-migrate
ExecStart=/opt/hatchet/bin/hatchet-lite --config /opt/hatchet_data/config
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now hatchet
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/inngest/inngest
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y redis-server
sed -i 's/^appendonly no/appendonly yes/' /etc/redis/redis.conf
systemctl restart redis-server
msg_ok "Installed Dependencies"
PG_VERSION="17" setup_postgresql
PG_DB_NAME="inngest" PG_DB_USER="inngest" setup_postgresql_db
fetch_and_deploy_gh_release "inngest" "inngest/inngest" "prebuild" "latest" "/opt/inngest" "inngest_*_linux_$(arch_resolve).tar.gz"
msg_info "Configuring Inngest"
mkdir -p /opt/inngest_data
cat <<EOF >/opt/inngest_data/.env
INNGEST_PORT=8288
INNGEST_POSTGRES_URI=postgres://inngest:${PG_DB_PASS}@127.0.0.1:5432/inngest?sslmode=disable
INNGEST_REDIS_URI=redis://127.0.0.1:6379
INNGEST_EVENT_KEY=$(openssl rand -hex 32)
INNGEST_SIGNING_KEY=$(openssl rand -hex 32)
DO_NOT_TRACK=1
EOF
chmod 600 /opt/inngest_data/.env
msg_ok "Configured Inngest"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/inngest.service
[Unit]
Description=Inngest
After=network.target postgresql.service redis-server.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/inngest_data
EnvironmentFile=/opt/inngest_data/.env
ExecStart=/opt/inngest/inngest start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now inngest
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/langflow-ai/langflow
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
NODE_VERSION="24" setup_nodejs
PYTHON_VERSION="3.14" USE_UVX="YES" setup_uv
LANGFLOW_VERSION="${var_appversion:-$(get_latest_github_release "langflow-ai/langflow")}"
msg_info "Installing Langflow ${LANGFLOW_VERSION#v}"
$STD uv venv --python 3.14 /opt/langflow/.venv
$STD uv pip install --python /opt/langflow/.venv/bin/python "langflow==${LANGFLOW_VERSION#v}"
cat <<EOF >~/.langflow
${LANGFLOW_VERSION#v}
EOF
msg_ok "Installed Langflow ${LANGFLOW_VERSION#v}"
msg_info "Configuring Langflow"
mkdir -p /opt/langflow_data/logs
cat <<EOF >/opt/langflow_data/.env
LANGFLOW_HOST=0.0.0.0
LANGFLOW_PORT=7860
LANGFLOW_CONFIG_DIR=/opt/langflow_data
LANGFLOW_DATABASE_URL=sqlite:////opt/langflow_data/langflow.db
LANGFLOW_KNOWLEDGE_BASES_DIR=/opt/langflow_data/knowledge_bases
LANGFLOW_LOG_FILE=/opt/langflow_data/logs/langflow.log
LANGFLOW_SECRET_KEY=$(/opt/langflow/.venv/bin/python -c "from secrets import token_urlsafe; print(token_urlsafe(32))")
LANGFLOW_AUTO_LOGIN=False
LANGFLOW_SUPERUSER='${var_admin_user:-langflow}'
LANGFLOW_SUPERUSER_PASSWORD='${var_admin_pass:-$(random_password)}'
EOF
chmod 600 /opt/langflow_data/.env
msg_ok "Configured Langflow"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/langflow.service
[Unit]
Description=Langflow
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/langflow_data
EnvironmentFile=/opt/langflow_data/.env
ExecStart=/opt/langflow/.venv/bin/langflow run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now langflow
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/marimo-team/marimo
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
PYTHON_VERSION="3.13" setup_uv
MARIMO_VERSION="${var_appversion:-$(get_latest_github_release "marimo-team/marimo")}"
msg_info "Installing marimo ${MARIMO_VERSION}"
$STD uv venv --python 3.13 /opt/marimo/.venv
$STD uv pip install --python /opt/marimo/.venv/bin/python "marimo[recommended]==${MARIMO_VERSION}" pandas
cat <<EOF >~/.marimo
${MARIMO_VERSION}
EOF
msg_ok "Installed marimo ${MARIMO_VERSION}"
msg_info "Configuring marimo"
mkdir -p /opt/marimo_data/notebooks /root/.config/marimo
cat <<EOF >/opt/marimo_data/.env
MARIMO_TOKEN_PASSWORD=${var_token_password:-$(random_password)}
EOF
chmod 600 /opt/marimo_data/.env
cat <<EOF >/root/.config/marimo/marimo.toml
[package_management]
manager = "uv"
EOF
msg_ok "Configured marimo"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/marimo.service
[Unit]
Description=marimo
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/marimo_data/notebooks
EnvironmentFile=/opt/marimo_data/.env
Environment=MARIMO_SKIP_UPDATE_CHECK=1
ExecStart=/opt/marimo/.venv/bin/marimo -y edit --headless --host 0.0.0.0 --port 2718 --token-password \${MARIMO_TOKEN_PASSWORD} /opt/marimo_data/notebooks
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now marimo
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/maplibre/martin
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "martin" "maplibre/martin" "prebuild" "latest" "/opt/martin" "martin-$(arch_resolve "x86_64" "aarch64")-unknown-linux-musl.tar.gz"
msg_info "Configuring Martin"
mkdir -p /opt/martin_data/tiles
cat <<EOF >/opt/martin_data/config.yaml
listen_addresses: 0.0.0.0:3000
web_ui: enable-for-all
mbtiles:
paths:
- /opt/martin_data/tiles
pmtiles:
paths:
- /opt/martin_data/tiles
# postgres:
# connection_string: postgres://user:password@host:5432/database
EOF
msg_ok "Configured Martin"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/martin.service
[Unit]
Description=Martin Tile Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/martin_data
ExecStart=/opt/martin/martin --config /opt/martin_data/config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now martin
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/QuantumNous/new-api
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "new-api" "QuantumNous/new-api" "singlefile" "latest" "/opt/new-api" "$(arch_resolve "new-api-v" "new-api-arm64-v")*[0-9]"
msg_info "Configuring New API"
ADMIN_USER="${var_admin_user:-admin}"
ADMIN_PASS="${var_admin_pass:-$(random_password)}"
mkdir -p /opt/new-api_data/logs
cat <<EOF >/opt/new-api_data/.env
PORT=3000
SQLITE_PATH=/opt/new-api_data/new-api.db
SESSION_SECRET=$(openssl rand -hex 32)
CRYPTO_SECRET=$(openssl rand -hex 32)
BATCH_UPDATE_ENABLED=true
NEW_API_ADMIN_USER=${ADMIN_USER}
NEW_API_ADMIN_PASSWORD=${ADMIN_PASS}
EOF
chmod 600 /opt/new-api_data/.env
msg_ok "Configured New API"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/new-api.service
[Unit]
Description=New API
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/new-api_data
EnvironmentFile=/opt/new-api_data/.env
ExecStart=/opt/new-api/new-api --log-dir /opt/new-api_data/logs
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now new-api
msg_ok "Created Service"
msg_info "Creating New API Admin Account"
for _ in {1..60}; do
curl -fsS http://127.0.0.1:3000/api/status &>/dev/null && break
sleep 1
done
jq -nc --arg u "$ADMIN_USER" --arg p "$ADMIN_PASS" \
'{username: $u, password: $p, confirmPassword: $p, SelfUseModeEnabled: false, DemoSiteEnabled: false}' |
curl -fsS -X POST http://127.0.0.1:3000/api/setup -H "Content-Type: application/json" --data-binary @- |
jq -e '.success' >/dev/null
msg_ok "Created New API Admin Account"
motd_ssh
customize
cleanup_lxc
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/ccfos/nightingale
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y redis-server
msg_ok "Installed Dependencies"
setup_mariadb
MARIADB_DB_NAME="nightingale" MARIADB_DB_USER="nightingale" setup_mariadb_db
fetch_and_deploy_gh_release "nightingale" "ccfos/nightingale" "prebuild" "latest" "/opt/nightingale" "n9e-v*-linux-$(arch_resolve).tar.gz"
msg_info "Configuring Nightingale"
mkdir -p /opt/nightingale_data
cat <<EOF >/opt/nightingale_data/config.toml
[Global]
RunMode = "release"
[Log]
Dir = "/opt/nightingale_data/logs"
Level = "INFO"
Output = "stdout"
[HTTP]
Host = "0.0.0.0"
Port = 17000
CertFile = ""
KeyFile = ""
PrintAccessLog = false
PProf = false
ExposeMetrics = true
ShutdownTimeout = 30
MaxContentLength = 67108864
ReadTimeout = 20
WriteTimeout = 40
IdleTimeout = 120
[HTTP.ShowCaptcha]
Enable = false
[HTTP.APIForAgent]
Enable = true
[HTTP.APIForService]
Enable = false
[HTTP.JWTAuth]
AccessExpired = 1500
RefreshExpired = 10080
RedisKeyPrefix = "/jwt/"
[HTTP.ProxyAuth]
Enable = false
HeaderUserNameKey = "X-User-Name"
DefaultRoles = ["Standard"]
[HTTP.TokenAuth]
Enable = true
[HTTP.RSA]
OpenRSA = false
[DB]
DBType = "mysql"
DSN = "nightingale:${MARIADB_DB_PASS}@tcp(127.0.0.1:3306)/nightingale?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=True&loc=Local&allowNativePasswords=true"
Debug = false
MaxLifetime = 7200
MaxOpenConns = 150
MaxIdleConns = 50
[Redis]
Address = "127.0.0.1:6379"
RedisType = "standalone"
[Alert]
[Alert.Heartbeat]
IP = ""
Interval = 1000
EngineName = "default"
[Alert.EvalLog]
MaxDiskGB = 1
[Center]
MetricsYamlFile = "/opt/nightingale/etc/metrics.yaml"
I18NHeaderKey = "X-Language"
[Center.AnonymousAccess]
PromQuerier = false
AlertDetail = true
[EmbeddedTSDB]
Enable = true
Dir = "/opt/nightingale_data/tsdb"
RetentionDuration = "15d"
MaxBytes = "4GiB"
OutOfOrderTimeWindow = "10m"
QueryTimeout = "1m"
QueryMaxSamples = 50000000
LookbackDelta = "5m"
BasicAuthUser = ""
BasicAuthPass = ""
[Pushgw]
LabelRewrite = true
ForceUseServerTS = true
[Ibex]
Enable = true
RPCListen = "0.0.0.0:20090"
EOF
chmod 600 /opt/nightingale_data/config.toml
msg_ok "Configured Nightingale"
msg_info "Creating Service"
# integrations/, agents/ and the AI skill cache resolve against the working directory, so it stays the release dir
cat <<EOF >/etc/systemd/system/nightingale.service
[Unit]
Description=Nightingale
After=network.target mariadb.service redis-server.service
Requires=mariadb.service redis-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/nightingale
ExecStart=/opt/nightingale/n9e --configs /opt/nightingale_data
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now nightingale
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/simple-login/app
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
echo "postfix postfix/mailname string $(hostname -f)" | debconf-set-selections
echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections
$STD apt install -y \
build-essential \
libre2-dev \
pkg-config \
libpq-dev \
cmake \
ninja-build \
clang \
redis-server \
nginx \
postfix \
postfix-pgsql \
opendkim-tools
msg_ok "Installed Dependencies"
PG_VERSION="17" setup_postgresql
APPLICATION="simplelogin" PG_DB_NAME="simplelogin" PG_DB_USER="simplelogin" setup_postgresql_db
PYTHON_VERSION="3.12" setup_uv
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "simplelogin" "simple-login/app"
msg_info "Installing SimpleLogin (Patience)"
cd /opt/simplelogin
$STD uv venv
$STD uv pip install setuptools hatchling editables
export CMAKE_POLICY_VERSION_MINIMUM=3.5
$STD uv sync --locked --no-dev --no-build-isolation --no-install-package newrelic
if [[ -f /opt/simplelogin/static/package.json ]]; then
cd /opt/simplelogin/static
$STD npm ci
fi
msg_ok "Installed SimpleLogin"
msg_info "Configuring SimpleLogin"
FLASK_SECRET=$(openssl rand -hex 32)
mkdir -p /opt/simplelogin/dkim
$STD openssl genrsa -traditional -out /opt/simplelogin/dkim/dkim.private 2048
$STD openssl rsa -in /opt/simplelogin/dkim/dkim.private -pubout -out /opt/simplelogin/dkim/dkim.public.key
DKIM_PUB=$(openssl rsa -in /opt/simplelogin/dkim/dkim.private -pubout 2>/dev/null | grep -v '^-----' | tr -d '\n')
cat <<EOF >/opt/simplelogin/dkim/dkim.dns.txt
dkim._domainkey.EMAIL_DOMAIN. IN TXT ( "v=DKIM1; k=rsa; p=${DKIM_PUB}" )
EOF
$STD openssl genrsa -out /opt/simplelogin/openid-rsa.key 2048
$STD openssl rsa -in /opt/simplelogin/openid-rsa.key -pubout -out /opt/simplelogin/openid-rsa.pub
mkdir -p /opt/simplelogin/uploads /opt/simplelogin/.gnupg
chmod 700 /opt/simplelogin/.gnupg
{
echo "URL=http://${LOCAL_IP}"
echo "EMAIL_DOMAIN=example.com"
echo "SUPPORT_EMAIL=support@example.com"
echo 'EMAIL_SERVERS_WITH_PRIORITY=[(10, "localhost.")]'
echo "POSTFIX_SERVER=localhost"
echo "DB_URI=postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost/${PG_DB_NAME}"
echo "FLASK_SECRET=${FLASK_SECRET}"
echo "DKIM_PRIVATE_KEY_PATH=/opt/simplelogin/dkim/dkim.private"
echo "GNUPGHOME=/opt/simplelogin/.gnupg"
echo "LOCAL_FILE_UPLOAD=true"
echo "UPLOAD_DIR=/opt/simplelogin/uploads"
echo "DISABLE_ALIAS_SUFFIX=1"
echo "WORDS_FILE_PATH=/opt/simplelogin/local_data/words.txt"
echo "NAMESERVERS=1.1.1.1"
echo "MEM_STORE_URI=redis://localhost:6379/1"
echo "OPENID_PRIVATE_KEY_PATH=/opt/simplelogin/openid-rsa.key"
echo "OPENID_PUBLIC_KEY_PATH=/opt/simplelogin/openid-rsa.pub"
echo "METRICS_BACKENDS=noop"
} >/opt/simplelogin/.env
cd /opt/simplelogin
export FLASK_APP=server
export URL="http://${LOCAL_IP}"
export EMAIL_DOMAIN="example.com"
export SUPPORT_EMAIL="support@example.com"
export EMAIL_SERVERS_WITH_PRIORITY='[(10, "localhost.")]'
export POSTFIX_SERVER="localhost"
export DB_URI="postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost/${PG_DB_NAME}"
export FLASK_SECRET="${FLASK_SECRET}"
export DKIM_PRIVATE_KEY_PATH="/opt/simplelogin/dkim/dkim.private"
export GNUPGHOME="/opt/simplelogin/.gnupg"
export LOCAL_FILE_UPLOAD="true"
export UPLOAD_DIR="/opt/simplelogin/uploads"
export DISABLE_ALIAS_SUFFIX="1"
export WORDS_FILE_PATH="/opt/simplelogin/local_data/words.txt"
export NAMESERVERS="1.1.1.1"
export MEM_STORE_URI="redis://localhost:6379/1"
export OPENID_PRIVATE_KEY_PATH="/opt/simplelogin/openid-rsa.key"
export OPENID_PUBLIC_KEY_PATH="/opt/simplelogin/openid-rsa.pub"
export METRICS_BACKENDS="noop"
$STD .venv/bin/alembic upgrade head
$STD .venv/bin/python init_app.py
msg_ok "Configured SimpleLogin"
msg_info "Configuring Postfix"
cat <<EOF >/etc/postfix/pgsql-relay-domains.cf
hosts = localhost
dbname = ${PG_DB_NAME}
user = ${PG_DB_USER}
password = ${PG_DB_PASS}
query = SELECT domain FROM custom_domain WHERE domain='%s' AND verified=true
EOF
cat <<EOF >/etc/postfix/pgsql-transport-maps.cf
hosts = localhost
dbname = ${PG_DB_NAME}
user = ${PG_DB_USER}
password = ${PG_DB_PASS}
query = SELECT 'smtp:[127.0.0.1]:20381' FROM custom_domain WHERE domain='%s' AND verified=true
EOF
chmod 640 /etc/postfix/pgsql-*.cf
cat <<EOF >/etc/postfix/transport
example.com smtp:[127.0.0.1]:20381
EOF
$STD postmap /etc/postfix/transport
postconf -e "relay_domains = example.com, pgsql:/etc/postfix/pgsql-relay-domains.cf"
postconf -e "transport_maps = hash:/etc/postfix/transport, pgsql:/etc/postfix/pgsql-transport-maps.cf"
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination"
postconf -e "inet_protocols = ipv4"
$STD systemctl restart postfix
msg_ok "Configured Postfix"
msg_info "Creating Services"
cat <<'EOF' >/etc/systemd/system/simplelogin-webapp.service
[Unit]
Description=SimpleLogin Web Application
After=network.target postgresql.service redis-server.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/simplelogin
EnvironmentFile=/opt/simplelogin/.env
ExecStart=/opt/simplelogin/.venv/bin/gunicorn wsgi:app -b 127.0.0.1:7777 -w 2 --timeout 120
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<'EOF' >/etc/systemd/system/simplelogin-email.service
[Unit]
Description=SimpleLogin Email Handler
After=network.target postgresql.service redis-server.service postfix.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/simplelogin
EnvironmentFile=/opt/simplelogin/.env
ExecStart=/opt/simplelogin/.venv/bin/python email_handler.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<'EOF' >/etc/systemd/system/simplelogin-job.service
[Unit]
Description=SimpleLogin Job Runner
After=network.target postgresql.service redis-server.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/simplelogin
EnvironmentFile=/opt/simplelogin/.env
ExecStart=/opt/simplelogin/.venv/bin/python job_runner.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now redis-server simplelogin-webapp simplelogin-email simplelogin-job
msg_ok "Created Services"
msg_info "Configuring Nginx"
cat <<'EOF' >/etc/nginx/sites-available/simplelogin.conf
server {
listen 80 default_server;
server_name _;
client_max_body_size 10M;
location / {
proxy_pass http://127.0.0.1:7777;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
EOF
ln -sf /etc/nginx/sites-available/simplelogin.conf /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
$STD nginx -t
$STD systemctl enable --now nginx
$STD systemctl reload nginx
msg_ok "Configured Nginx"
motd_ssh
customize
cleanup_lxc
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/spacedriveapp/spacebot
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
bubblewrap \
fonts-liberation \
libnss3 \
libatk-bridge2.0-0t64 \
libdrm2 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgbm1 \
libasound2t64 \
libpango-1.0-0 \
libcairo2 \
libcups2t64 \
libxkbcommon0 \
libxss1 \
libxtst6 \
libxfixes3
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "spacebot" "spacedriveapp/spacebot" "prebuild" "latest" "/opt/spacebot" "spacebot-v*-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.tar.gz"
msg_info "Configuring Spacebot"
mkdir -p /opt/spacebot_data
cat <<EOF >/opt/spacebot_data/.env
SPACEBOT_DIR=/opt/spacebot_data
EOF
cat <<EOF >/opt/spacebot_data/config.toml
[api]
bind = "0.0.0.0"
port = 19898
[[agents]]
id = "main"
default = true
EOF
if [[ -n "${var_llm_base_url:-}" ]]; then
cat <<EOF >>/opt/spacebot_data/.env
LLM_API_KEY=${var_llm_api_key:-none}
EOF
cat <<EOF >>/opt/spacebot_data/config.toml
[llm.provider.custom]
api_type = "openai_chat_completions"
base_url = "${var_llm_base_url%/}"
api_key = "env:LLM_API_KEY"
name = "OpenAI-compatible"
[defaults.routing]
channel = "custom/${var_llm_model}"
branch = "custom/${var_llm_model}"
worker = "custom/${var_llm_model}"
compactor = "custom/${var_llm_model}"
cortex = "custom/${var_llm_model}"
EOF
fi
chmod 600 /opt/spacebot_data/.env /opt/spacebot_data/config.toml
msg_ok "Configured Spacebot"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/spacebot.service
[Unit]
Description=Spacebot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/spacebot_data
EnvironmentFile=/opt/spacebot_data/.env
ExecStart=/opt/spacebot/spacebot start --foreground
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now spacebot
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+206
View File
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Renan Bastos (renanbastos93)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/temporalio/temporal
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
PG_VERSION="17" setup_postgresql
PG_DB_NAME="temporal" PG_DB_USER="temporal" PG_DB_SCHEMA_PERMS="true" setup_postgresql_db
fetch_and_deploy_gh_release "temporal" "temporalio/temporal" "prebuild" "latest" "/opt/temporal" "temporal_*_linux_$(arch_resolve).tar.gz"
fetch_and_deploy_gh_release "temporal-ui" "temporalio/ui-server" "prebuild" "latest" "/opt/temporal-ui" "ui-server_*_linux_$(arch_resolve).tar.gz"
fetch_and_deploy_gh_release "temporal-cli" "temporalio/cli" "prebuild" "latest" "/opt/temporal-cli" "temporal_cli_*_linux_$(arch_resolve).tar.gz"
msg_info "Configuring Temporal"
mkdir -p /etc/temporal/config/dynamicconfig /etc/temporal/ui
ln -sf /opt/temporal-cli/temporal /usr/bin/temporal
cat <<EOF >/etc/temporal/temporal.env
SQL_PLUGIN=postgres12
SQL_HOST=127.0.0.1
SQL_PORT=5432
SQL_USER=temporal
SQL_PASSWORD=${PG_DB_PASS}
EOF
chmod 600 /etc/temporal/temporal.env
cat <<'EOF' >/etc/temporal/config/production.yaml
# enable-template
log:
stdout: true
level: info
persistence:
defaultStore: postgres-default
visibilityStore: postgres-visibility
numHistoryShards: 4
datastores:
postgres-default:
sql:
pluginName: {{ env "SQL_PLUGIN" | quote }}
databaseName: "temporal"
connectAddr: {{ printf "%s:%s" (env "SQL_HOST") (env "SQL_PORT") | quote }}
connectProtocol: "tcp"
user: {{ env "SQL_USER" | quote }}
password: {{ env "SQL_PASSWORD" | quote }}
maxConns: 20
maxIdleConns: 20
maxConnLifetime: "1h"
postgres-visibility:
sql:
pluginName: {{ env "SQL_PLUGIN" | quote }}
databaseName: "temporal_visibility"
connectAddr: {{ printf "%s:%s" (env "SQL_HOST") (env "SQL_PORT") | quote }}
connectProtocol: "tcp"
user: {{ env "SQL_USER" | quote }}
password: {{ env "SQL_PASSWORD" | quote }}
maxConns: 10
maxIdleConns: 10
maxConnLifetime: "1h"
global:
membership:
maxJoinDuration: 30s
broadcastAddress: "127.0.0.1"
services:
frontend:
rpc:
grpcPort: 7233
membershipPort: 6933
bindOnIP: "0.0.0.0"
matching:
rpc:
grpcPort: 7235
membershipPort: 6935
bindOnLocalHost: true
history:
rpc:
grpcPort: 7234
membershipPort: 6934
bindOnLocalHost: true
worker:
rpc:
grpcPort: 7239
membershipPort: 6939
bindOnLocalHost: true
clusterMetadata:
enableGlobalNamespace: false
failoverVersionIncrement: 10
masterClusterName: "active"
currentClusterName: "active"
clusterInformation:
active:
enabled: true
initialFailoverVersion: 1
rpcName: "frontend"
rpcAddress: "127.0.0.1:7233"
dcRedirectionPolicy:
policy: "noop"
archival:
history:
state: "disabled"
visibility:
state: "disabled"
namespaceDefaults:
archival:
history:
state: "disabled"
visibility:
state: "disabled"
dynamicConfigClient:
filepath: "/etc/temporal/config/dynamicconfig/production.yaml"
pollInterval: "60s"
EOF
cat <<EOF >/etc/temporal/config/dynamicconfig/production.yaml
limit.maxIDLength:
- value: 255
constraints: {}
EOF
cat <<EOF >/etc/temporal/ui/production.yaml
temporalGrpcAddress: 127.0.0.1:7233
port: 8080
enableUi: true
defaultNamespace: default
cors:
cookieInsecure: true
allowOrigins:
- http://${LOCAL_IP}:8080
EOF
msg_ok "Configured Temporal"
msg_info "Initializing Temporal Database"
set -a && source /etc/temporal/temporal.env && set +a
$STD /opt/temporal/temporal-sql-tool --db temporal_visibility create-database
$STD /opt/temporal/temporal-sql-tool --db temporal setup-schema -v 0.0
$STD /opt/temporal/temporal-sql-tool --db temporal update-schema --schema-name postgresql/v12/temporal
$STD /opt/temporal/temporal-sql-tool --db temporal_visibility setup-schema -v 0.0
$STD /opt/temporal/temporal-sql-tool --db temporal_visibility update-schema --schema-name postgresql/v12/visibility
msg_ok "Initialized Temporal Database"
msg_info "Creating Services"
cat <<EOF >/etc/systemd/system/temporal.service
[Unit]
Description=Temporal Server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/temporal
EnvironmentFile=/etc/temporal/temporal.env
ExecStart=/opt/temporal/temporal-server --root /etc/temporal --env production --allow-no-auth start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/temporal-ui.service
[Unit]
Description=Temporal UI
After=network.target temporal.service
Wants=temporal.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/temporal-ui
ExecStart=/opt/temporal-ui/ui-server --root /etc/temporal --config ui --env production start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now temporal temporal-ui
msg_ok "Created Services"
msg_info "Creating Default Namespace"
for _ in {1..60}; do
temporal operator cluster health &>/dev/null && break
sleep 1
done
$STD temporal operator namespace create --namespace default
msg_ok "Created Default Namespace"
motd_ssh
customize
cleanup_lxc
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/trailbaseio/trailbase
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "trailbase" "trailbaseio/trailbase" "prebuild" "latest" "/opt/trailbase" "trailbase_v*_$(arch_resolve "x86_64" "aarch64")_linux.zip"
msg_info "Configuring TrailBase"
ADMIN_EMAIL="${var_admin_email:-admin@localhost}"
ADMIN_PASS="${var_admin_pass:-$(random_password)}"
mkdir -p /opt/trailbase_data
cat <<EOF >/opt/trailbase_data/.env
DEPOT=/opt/trailbase_data/traildepot
ADDRESS=0.0.0.0:4000
PUBLIC_URL=http://${LOCAL_IP}:4000
TRAILBASE_ADMIN_EMAIL=${ADMIN_EMAIL}
TRAILBASE_ADMIN_PASSWORD=${ADMIN_PASS}
EOF
chmod 600 /opt/trailbase_data/.env
# The first CLI call creates the depot and the admin user admin@localhost
$STD /opt/trailbase/trail --depot /opt/trailbase_data/traildepot user change-password admin@localhost "$ADMIN_PASS"
if [[ "$ADMIN_EMAIL" != "admin@localhost" ]]; then
$STD /opt/trailbase/trail --depot /opt/trailbase_data/traildepot user change-email admin@localhost "$ADMIN_EMAIL"
fi
msg_ok "Configured TrailBase"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/trailbase.service
[Unit]
Description=TrailBase
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/trailbase_data
EnvironmentFile=/opt/trailbase_data/.env
ExecStart=/opt/trailbase/trail run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now trailbase
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/unslothai/unsloth
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
libgomp1 \
git
msg_ok "Installed Dependencies"
UV_PYTHON="3.12" setup_uv
NODE_VERSION="22" setup_nodejs
setup_hwaccel
msg_info "Setting up Unsloth Studio (Patience)"
mkdir -p /opt/unsloth_data
curl -fsSL https://unsloth.ai/install.sh -o /tmp/unsloth-install.sh
export UNSLOTH_STUDIO_HOME=/opt/unsloth
export UNSLOTH_SKIP_AUTOSTART=1
export UNSLOTH_PYTHON=3.12
$STD sh /tmp/unsloth-install.sh
unset UNSLOTH_SKIP_AUTOSTART UNSLOTH_PYTHON
rm -f /tmp/unsloth-install.sh
[[ -x /opt/unsloth/unsloth_studio/bin/unsloth ]] || {
msg_error "Unsloth Studio was not installed to /opt/unsloth/unsloth_studio"
exit 1
}
msg_ok "Set up Unsloth Studio"
msg_info "Configuring Unsloth Studio"
cat <<EOF >/opt/unsloth.env
UNSLOTH_STUDIO_HOME=/opt/unsloth
HF_HOME=/opt/unsloth_data/huggingface
EOF
chmod 600 /opt/unsloth.env
UNSLOTH_PASSWORD=$(UNSLOTH_STUDIO_HOME=/opt/unsloth \
/opt/unsloth/unsloth_studio/bin/unsloth studio reset-password |
sed -n "s/^New password for 'unsloth': //p")
[[ -n "$UNSLOTH_PASSWORD" ]] || {
msg_error "Could not set the initial Unsloth admin password"
exit 1
}
cat <<EOF >~/unsloth.creds
Unsloth Studio
Username: unsloth
Password: ${UNSLOTH_PASSWORD}
EOF
chmod 600 ~/unsloth.creds
msg_ok "Configured Unsloth Studio"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/unsloth.service
[Unit]
Description=Unsloth Studio
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/unsloth_data
EnvironmentFile=/opt/unsloth.env
ExecStart=/opt/unsloth/unsloth_studio/bin/unsloth studio -H 0.0.0.0 -p 8888
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now unsloth
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/weaviate/weaviate
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "weaviate" "weaviate/weaviate" "prebuild" "latest" "/opt/weaviate" "weaviate-v*-linux-$(arch_resolve).tar.gz"
msg_info "Configuring Weaviate"
mkdir -p /opt/weaviate_data/data
cat <<EOF >/opt/weaviate_data/.env
PERSISTENCE_DATA_PATH=/opt/weaviate_data/data
QUERY_DEFAULTS_LIMIT=25
CLUSTER_HOSTNAME=node1
CLUSTER_BIND_ADDR=127.0.0.1
CLUSTER_BASIC_AUTH_USERNAME=weaviate
CLUSTER_BASIC_AUTH_PASSWORD=$(random_password)
GRPC_PORT=50051
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=false
AUTHENTICATION_APIKEY_ENABLED=true
AUTHENTICATION_APIKEY_ALLOWED_KEYS=${var_api_key:-$(random_password 32)}
AUTHENTICATION_APIKEY_USERS=admin
AUTHENTICATION_DB_USERS_ENABLED=true
AUTHORIZATION_RBAC_ENABLED=true
AUTHORIZATION_RBAC_ROOT_USERS=admin
LIMIT_RESOURCES=true
GO_PROFILING_DISABLE=true
DISABLE_TELEMETRY=true
EOF
chmod 600 /opt/weaviate_data/.env
msg_ok "Configured Weaviate"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/weaviate.service
[Unit]
Description=Weaviate
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/weaviate_data
EnvironmentFile=/opt/weaviate_data/.env
ExecStart=/opt/weaviate/weaviate --host 0.0.0.0 --port 8080 --scheme http
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now weaviate
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc