Compare commits
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
emdash:
|
||||
build:
|
||||
context: https://github.com/emdash-cms/emdash.git#10beb8c5a99e41c3a8554b09ac5123c4dddc8d11
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
EMDASH_SITE_URL: "${EMDASH_SITE_URL:?Set the public HTTPS origin}"
|
||||
EMDASH_ENCRYPTION_KEY: "${EMDASH_ENCRYPTION_KEY:-}"
|
||||
EMDASH_TRUSTED_PROXY_HEADERS: x-forwarded-for
|
||||
HOST: 0.0.0.0
|
||||
PORT: "4321"
|
||||
expose:
|
||||
- "4321"
|
||||
volumes:
|
||||
- emdash-data:/app/data
|
||||
- emdash-uploads:/app/uploads
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://127.0.0.1:4321/_emdash/api/setup/status').then(r => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
|
||||
start_period: 60s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
stop_grace_period: 30s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
emdash-data:
|
||||
emdash-uploads:
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3" width="69" height="69" rx="10.518" stroke="url(#border)" stroke-width="6"/>
|
||||
<rect x="18" y="34" width="39.3661" height="6.56101" fill="url(#dash)"/>
|
||||
<defs>
|
||||
<linearGradient id="border" x1="-43" y1="124" x2="92" y2="-42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0F006B"/><stop offset=".25" stop-color="#911475"/><stop offset=".5" stop-color="#F6821F"/><stop offset=".75" stop-color="#FFE9CB"/><stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="dash" x1="91" y1="27" x2="28" y2="54" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/><stop offset=".35" stop-color="#FFF8EE"/><stop offset=".7" stop-color="#FBAD41"/><stop offset="1" stop-color="#FF6633"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
@@ -0,0 +1,38 @@
|
||||
# Environment variables
|
||||
|
||||
Checked against [EmDash 1.4.0](https://github.com/emdash-cms/emdash/tree/10beb8c5a99e41c3a8554b09ac5123c4dddc8d11), its root Dockerfile, Blog Astro configuration, and runtime environment readers. Compose passes the variables below into the process; editing an unused variable in Dokploy does not change the compiled Astro configuration.
|
||||
|
||||
## Operator settings
|
||||
|
||||
| Variable | Template value | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `EMDASH_SITE_URL` | `https://<generated-domain>` | Required public HTTP(S) origin, without a path. Use the final HTTPS origin for production setup, passkeys, CSRF, and absolute URLs. Keep it aligned with the Dokploy domain. |
|
||||
| `EMDASH_ENCRYPTION_KEY` | Empty | Optional for the stock site. Required for encrypted plugin-secret settings. Use `npx --yes emdash@1.4.0 secrets generate`; the format is `emdash_enc_v1_` plus 43 canonical base64url characters encoding 32 random bytes. For rotation, list the new key first followed by older keys separated by commas. Preserve every key still used by saved secrets. |
|
||||
|
||||
No administrator password, database password, or provider key is generated by this template. The setup wizard creates the administrator's passkey. EmDash generates its preview HMAC secret and commenter-IP salt into SQLite on first use. Sessions and API tokens are generated by the application.
|
||||
|
||||
## Fixed Compose and build values
|
||||
|
||||
| Setting | Value | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `HOST` / `PORT` | `0.0.0.0` / `4321` | Node listener, matching routing and the health check. |
|
||||
| `EMDASH_TRUSTED_PROXY_HEADERS` | `x-forwarded-for` | Trust the controlled Traefik proxy for client IPs. Keep the app isolated from direct/untrusted connections. Change Compose if another proxy policy is needed. |
|
||||
| Database | `file:./data/data.db` | Fixed by the upstream Dockerfile's Blog configuration. `DATABASE_URL` and `DATABASE_PATH` do not switch this build's database. |
|
||||
| Media | `./uploads` | Fixed local adapter with public route `/_emdash/api/media/file`. S3 variables alone do not switch storage. |
|
||||
| `NODE_OPTIONS` | Build-stage `--max-old-space-size=4096` | Upstream compiler heap allowance; not a runtime memory reservation. No secrets are passed as build arguments. |
|
||||
|
||||
The upstream Dockerfile has no argument to supply the public origin during compilation. `EMDASH_SITE_URL` is supplied at runtime; local media therefore uses original files rather than build-configured image optimization. Change the upstream project/build if you need that optimization or another theme.
|
||||
|
||||
## Optional upstream controls
|
||||
|
||||
These are not generated or forwarded by this template. To use one, add an explicit Compose environment mapping and redeploy after reading its upstream contract:
|
||||
|
||||
- `EMDASH_PREVIEW_SECRET`: overrides the database-generated preview secret. Changing it invalidates existing preview links.
|
||||
- `EMDASH_IP_SALT`: overrides the database-generated commenter-IP salt. Changing it resets rate-limit continuity. Legacy `EMDASH_AUTH_SECRET` is only a salt fallback, not a login requirement.
|
||||
- `EMDASH_MIGRATIONS_MODE`: defaults to `auto`; `check` refuses pending migrations, and `manual` leaves migration management to the operator. Do not change it without an explicit migration procedure.
|
||||
- `EMDASH_ALLOWED_ORIGINS`: comma-separated HTTP(S) origins for advanced authentication deployments, subject to the passkey relying-party domain rules. The single-domain template needs none.
|
||||
- `EMDASH_TURNSTILE_SECRET_KEY`: optional Cloudflare Turnstile secret for public comments. Unset means no Turnstile enforcement. Also configure the matching public widget/site key in your theme before enabling it; a secret alone rejects comments that have no valid token.
|
||||
|
||||
Email, OAuth, AI, remote database/storage credentials, and plugin settings are optional integrations, not installation requirements. Many also require installing/configuring the corresponding adapter or plugin and rebuilding. See [instructions.md](instructions.md) before adding them. Cloudflare bindings and Wrangler settings do not apply to this Node build.
|
||||
|
||||
`EMDASH_QUERY_LOG=1` enables diagnostic query logging; `EMDASH_PSEUDO_LOCALE=1` is a development UI test option. Both are unset here. Development bypass/reset/email endpoints are compiled out of production and are not setup mechanisms. `EMDASH_URL`, `EMDASH_TOKEN`, and `EMDASH_HEADERS` are CLI client inputs, not server deployment settings.
|
||||
@@ -0,0 +1,37 @@
|
||||
# EmDash
|
||||
|
||||
This template builds EmDash **1.4.0** from its [release commit](https://github.com/emdash-cms/emdash/tree/10beb8c5a99e41c3a8554b09ac5123c4dddc8d11), using the upstream Dockerfile. It packages the official Blog theme, SQLite, and local media storage in one Node.js service. The scheduler runs inside that service; a separate database or worker is unnecessary.
|
||||
|
||||
## First deployment
|
||||
|
||||
1. Import the template. In Dokploy, assign your final domain to service `emdash`, port `4321`, and enable HTTPS with a valid certificate.
|
||||
2. Set `EMDASH_SITE_URL` to that exact public origin, for example `https://blog.example.com`, without a path. The generated value uses the initial Dokploy domain. Update it if you change the domain.
|
||||
3. Deploy. The first build compiles the upstream monorepo and downloads dependencies and fonts. Allow several minutes and at least 4 GB of build memory, plus memory for Dokploy and other services. Subsequent builds can reuse Docker's cache.
|
||||
4. Open `https://<your-domain>/_emdash/admin/` promptly, complete the site setup, and register your administrator passkey. The first visitor can claim an unconfigured site; keep access restricted until setup is complete. Use your final HTTPS domain from the start: passkeys are bound to it. No default password or email provider is required.
|
||||
5. Create and publish a post, then open its public page. Upload a media file and verify its URL before adding real content.
|
||||
|
||||
Use Dokploy's isolated deployment option. Only Traefik should reach the app: the template trusts its `X-Forwarded-For` header for client-IP rate limiting. No host port or custom network is needed. Run one replica with these local SQLite and filesystem volumes.
|
||||
|
||||
## Optional features
|
||||
|
||||
The stock Blog build works without cloud accounts, AI keys, or email credentials. Email delivery, magic links, and email recovery require a provider plugin; no production console email provider exists. Register another passkey and keep recovery access before relying on the site.
|
||||
|
||||
Plugins with encrypted secret settings require `EMDASH_ENCRYPTION_KEY`. Generate it with the official command on Node.js 22.23.4 or later:
|
||||
|
||||
```sh
|
||||
npx --yes emdash@1.4.0 secrets generate
|
||||
```
|
||||
|
||||
Copy the complete `emdash_enc_v1_...` value into Dokploy's Environment tab and redeploy. Keep it private and back it up separately. The template leaves it empty because Dokploy's generic password/base64 helpers do not produce this application's required key format.
|
||||
|
||||
Adding email or authentication providers, changing the theme, using PostgreSQL/S3, or enabling sandboxed plugins requires a custom Astro project/build. Setting provider credentials alone does not add those integrations to the stock Blog build. Its configuration includes no plugin sandbox runner. See [Node.js deployment](https://docs.emdashcms.com/deployment/nodejs/) and [plugin sandbox](https://docs.emdashcms.com/deployment/plugin-sandbox/).
|
||||
|
||||
## Persistence, backups, and upgrades
|
||||
|
||||
- `emdash-data` stores `/app/data/data.db`, including content, users, passkeys, settings, and automatically generated preview/IP secrets.
|
||||
- `emdash-uploads` stores `/app/uploads`, including media and application-generated backup artifacts.
|
||||
- Node's default filesystem session store is outside these volumes. Container recreation can sign users out; register a passkey so you can sign in again.
|
||||
- Back up both volumes and any encryption key off-server. For a consistent offline SQLite backup, stop writes and copy the database together with any `-wal` and `-shm` files. Restore into an isolated installation and verify content, login, and media. The app's JSON archives are not a full restorable database backup; see [upstream recovery guidance](https://docs.emdashcms.com/guides/backups/).
|
||||
- Before upgrading, take a verified recovery point and read the release notes. Change the pinned Git commit and metadata together, then rebuild. Pending core migrations run on the first request; a code rollback alone does not undo them. Keep the matching old build, database, media, and keys for recovery.
|
||||
|
||||
The upstream Dockerfile runs as root. Named volumes are writable without a separate ownership initializer; changing the user requires testing permissions for the database, uploads, and session directory. The health check queries setup state through the database, but does not prove media writes, passkey login, scheduled publishing, or optional integrations work. The build has no public origin at build time, so local media is served without Astro image optimization; the runtime origin still configures URLs and authentication.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "emdash",
|
||||
"name": "EmDash",
|
||||
"version": "1.4.0",
|
||||
"description": "EmDash is an Astro-native CMS with a visual editor, structured content, media library, and passkey authentication. This template builds the official blog site with persistent SQLite and local media storage.",
|
||||
"logo": "emdash.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/emdash-cms/emdash",
|
||||
"website": "https://emdashcms.com/",
|
||||
"docs": "https://docs.emdashcms.com/"
|
||||
},
|
||||
"tags": ["cms", "blog", "publishing"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"EMDASH_SITE_URL=https://${main_domain}",
|
||||
"EMDASH_ENCRYPTION_KEY=",
|
||||
]
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "emdash"
|
||||
port = 4321
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Verification checklist
|
||||
|
||||
Use disposable data and your own final HTTPS domain. Never run development reset endpoints against an existing installation.
|
||||
|
||||
1. Run metadata, Compose, and TOML validation and build the template catalog. Confirm that the main service, metadata ID, domain target, and logo agree.
|
||||
2. Import into a fresh Dokploy Compose service with isolated deployment enabled. Verify the pinned Git build, port `4321`, both named volumes, and the generated public-origin value. Enable HTTPS and update `EMDASH_SITE_URL` if the domain changes.
|
||||
3. Deploy without optional keys or providers. Wait for a new successful deployment and a healthy container. Check setup status, the version endpoint, logs, and the public site through Traefik.
|
||||
4. In a fresh browser context, complete setup and register a passkey over HTTPS. Verify the setup nonce cookie is Secure, HttpOnly, and SameSite=Strict. Sign out and sign in again with that passkey. Confirm anonymous access to private API routes is refused and production dev-bypass/reset routes cannot create sessions or reset data.
|
||||
5. Create a post, save a draft, reload it, publish, and open the public page. Verify draft content remains private. Upload a disposable image, open its media URL, and compare its bytes. Schedule another post and await actual publication without manual intervention.
|
||||
6. Remove only the application container and redeploy through Dokploy. Sign in again if needed. Verify the administrator/passkey, published and draft content, uploaded media, and settings survived.
|
||||
7. Verify the health check fails when Node cannot serve requests and recovers afterward. Check database/media operations separately; an HTTP health response alone is insufficient.
|
||||
8. Copy the Base64 from the PR preview catalog, decode it, and compare raw Compose/TOML to the committed files. Import that exact payload into a second fresh Dokploy service and repeat setup, publish, media, and persistence checks. Ensure its volumes and administrator are independent.
|
||||
9. Before using plugins, generate a valid encryption key with the official CLI. Verify the integration with its own provider. This stock build includes neither an email provider nor a sandbox runner; do not claim external delivery or sandbox execution from basic CMS tests.
|
||||
|
||||
Record the tested platform, checks, screenshots, and untested boundaries in the PR. Keep credentials, passkey private material, session state, and local fixture details out of public evidence.
|
||||
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
excaliself:
|
||||
image: ghcr.io/maciekzygmunt/excaliself:0.11.1
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
environment:
|
||||
DATABASE_URL: postgresql://excaliself:${POSTGRES_PASSWORD}@db:5432/excaliself
|
||||
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
||||
BASE_URL: ${BASE_URL}
|
||||
# Optional single sign-on, see https://excaliself.com/docs/single-sign-on/
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
|
||||
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
|
||||
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
|
||||
MICROSOFT_TENANT_ID: ${MICROSOFT_TENANT_ID:-}
|
||||
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
||||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||||
OIDC_NAME: ${OIDC_NAME:-}
|
||||
SSO_ALLOWED_DOMAINS: ${SSO_ALLOWED_DOMAINS:-}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: excaliself
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: excaliself
|
||||
volumes:
|
||||
- excaliself-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U excaliself -d excaliself"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
excaliself-db:
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" rx="113" fill="#6F66C9"/>
|
||||
<g transform="translate(129.28 112) scale(11.52)" fill="#FFFFFF">
|
||||
<path d="M1.10614 16.3305C0.382001 16.5374 -0.342137 16.0202 0.175104 14.7788L1.83028 7.33052C2.45097 5.67535 4.62338 6.19259 4.20959 7.84776L2.03717 15.5029C1.93373 16.0202 1.51993 16.3305 1.10614 16.3305Z"/>
|
||||
<path d="M6.79573 19.3305C5.76125 20.1581 4.31297 19.0202 5.14056 17.7788L17.1406 0.813272C18.5888 -1.0488 21.0716 0.606376 19.9337 2.57189L6.79573 19.3305Z"/>
|
||||
<path d="M10.4165 24.0891C9.27854 23.9857 8.7613 22.5374 10.0027 22.0202L19.1061 17.6753C21.0716 16.8478 22.2096 19.3305 20.3475 20.0547L10.4165 24.0891Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 762 B |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "excaliself",
|
||||
"name": "Excaliself",
|
||||
"version": "0.11.1",
|
||||
"description": "Self-hosted, open-source alternative to Excalidraw+ for teams. Shared drawings, folders, live collaboration, comments and SSO, stored in Postgres.",
|
||||
"logo": "excaliself.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/maciekzygmunt/excaliself",
|
||||
"website": "https://excaliself.com/",
|
||||
"docs": "https://excaliself.com/docs/"
|
||||
},
|
||||
"tags": [
|
||||
"whiteboard",
|
||||
"diagrams",
|
||||
"collaboration",
|
||||
"self-hosted"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
postgres_password = "${password:32}"
|
||||
auth_secret = "${password:64}"
|
||||
|
||||
[config]
|
||||
# Use https:// in BASE_URL once the domain has HTTPS. Sign-in only works from this URL.
|
||||
env = [
|
||||
"BASE_URL=http://${main_domain}",
|
||||
"BETTER_AUTH_SECRET=${auth_secret}",
|
||||
"POSTGRES_PASSWORD=${postgres_password}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "excaliself"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
ferret:
|
||||
image: ghcr.io/ferret-project/ferret:2026.11.4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# The address people (and AI assistants over MCP) use: Dokploy's domain for this app.
|
||||
PUBLIC_URL: ${PUBLIC_URL}
|
||||
# Dokploy's Traefik is the proxy in front: believe its X-Forwarded-For.
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES}
|
||||
# AUTH_SECRET, FILES_KEY and the push keys are made by Ferret on first start and kept in /data.
|
||||
# Optional settings (email, AI, backups, single sign-on): https://github.com/ferret-project/ferret/blob/main/apps/docs/hosting/configuration.md
|
||||
volumes:
|
||||
- ferret-data:/data
|
||||
expose:
|
||||
- 8787
|
||||
|
||||
volumes:
|
||||
ferret-data:
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#7c4a2d"/><g fill="#fdf9f4"><ellipse cx="32" cy="42.5" rx="12.5" ry="10"/><ellipse cx="16.5" cy="29.5" rx="4.6" ry="6.2" transform="rotate(-22 16.5 29.5)"/><ellipse cx="26" cy="19.5" rx="5" ry="6.6" transform="rotate(-8 26 19.5)"/><ellipse cx="38" cy="19.5" rx="5" ry="6.6" transform="rotate(8 38 19.5)"/><ellipse cx="47.5" cy="29.5" rx="4.6" ry="6.2" transform="rotate(22 47.5 29.5)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 500 B |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "ferret",
|
||||
"name": "Ferret",
|
||||
"version": "2026.11.4",
|
||||
"description": "Self-hosted home inventory: find anything you own. Photos, barcodes, QR labels, warranties, maintenance and an insurance report, with a REST API, an MCP server and optional AI on your own models.",
|
||||
"logo": "ferret.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/ferret-project/ferret",
|
||||
"website": "https://github.com/ferret-project/ferret",
|
||||
"docs": "https://github.com/ferret-project/ferret/tree/main/apps/docs/hosting"
|
||||
},
|
||||
"tags": [
|
||||
"inventory",
|
||||
"home",
|
||||
"self-hosted",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "ferret"
|
||||
port = 8787
|
||||
host = "${main_domain}"
|
||||
|
||||
[config.env]
|
||||
PUBLIC_URL = "http://${main_domain}"
|
||||
TRUSTED_PROXIES = "172.16.0.0/12"
|
||||
@@ -0,0 +1,63 @@
|
||||
services:
|
||||
frameleaf:
|
||||
image: ghcr.io/frameleaf/frameleaf-server:frameleaf-v1.2.3
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 2283
|
||||
volumes:
|
||||
# Originals, thumbnails and encoded video
|
||||
- frameleaf-library:/data
|
||||
# The Studio render worker enrols from here (read-only on machine learning)
|
||||
- frameleaf-studio-worker:/run/frameleaf/studio-worker
|
||||
environment:
|
||||
DB_HOSTNAME: frameleaf-database
|
||||
DB_USERNAME: ${DB_USERNAME}
|
||||
DB_PASSWORD: ${DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${DB_DATABASE_NAME}
|
||||
FRAMELEAF_MACHINE_LEARNING_URL: http://frameleaf-machine-learning:3003
|
||||
TZ: ${TZ}
|
||||
depends_on:
|
||||
frameleaf-database:
|
||||
condition: service_healthy
|
||||
# Encrypted Locked files are decrypted into /dev/shm for thumbnails, video and ML
|
||||
shm_size: 2gb
|
||||
stop_grace_period: 10s
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
frameleaf-machine-learning:
|
||||
image: ghcr.io/frameleaf/frameleaf-machine-learning:frameleaf-v1.2.3
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- frameleaf-model-cache:/cache
|
||||
- frameleaf-studio-worker:/run/frameleaf/studio-worker:ro
|
||||
# No database credentials here: the inference worker only needs to reach the server
|
||||
environment:
|
||||
FRAMELEAF_STUDIO_WORKER_SERVER_URL: http://frameleaf:2283
|
||||
TZ: ${TZ}
|
||||
healthcheck:
|
||||
disable: false
|
||||
|
||||
frameleaf-database:
|
||||
image: ghcr.io/frameleaf/frameleaf-postgres:19beta4-pgvector0.8.7@sha256:7b155ed85302ab66c1022309f1f70f0b79f98f70bd52af8d7d3611a0cbf08cf0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
POSTGRES_INITDB_ARGS: "--data-checksums"
|
||||
volumes:
|
||||
# PostgreSQL 18+ layout: mount /var/lib/postgresql, not /var/lib/postgresql/data
|
||||
- frameleaf-postgres:/var/lib/postgresql
|
||||
shm_size: 128mb
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready --host=127.0.0.1 --dbname=\"$${POSTGRES_DB}\" --username=\"$${POSTGRES_USER}\" && /usr/local/bin/healthcheck.sh"]
|
||||
interval: 30s
|
||||
start_period: 5m
|
||||
start_interval: 5s
|
||||
|
||||
volumes:
|
||||
frameleaf-library:
|
||||
frameleaf-model-cache:
|
||||
frameleaf-postgres:
|
||||
frameleaf-studio-worker:
|
||||
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"id": "frameleaf",
|
||||
"name": "Frameleaf",
|
||||
"version": "1.2.3",
|
||||
"description": "Frameleaf is a self-hosted photo and video library with native apps, private face and smart search, photo and video editing, and optional cloud backup. It is an open-source (AGPLv3) fork of Immich and is currently in beta.",
|
||||
"logo": "frameleaf.png",
|
||||
"links": {
|
||||
"github": "https://github.com/Frameleaf/frameleaf-app",
|
||||
"website": "https://frameleaf.app/",
|
||||
"docs": "https://frameleaf.app/docs/start/introduction/"
|
||||
},
|
||||
"tags": [
|
||||
"photos",
|
||||
"videos",
|
||||
"media",
|
||||
"backup",
|
||||
"photo-management",
|
||||
"self-hosted"
|
||||
],
|
||||
"seoTitle": "Frameleaf Docker Compose Setup",
|
||||
"seoDescription": "Self-hosted photo and video library (beta, fork of Immich) with face and smart search, photo and video editing. Deploy with Docker Compose on Dokploy in minutes.",
|
||||
"newConfig": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
db_password = "${password:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"DB_USERNAME=frameleaf",
|
||||
"DB_PASSWORD=${db_password}",
|
||||
"DB_DATABASE_NAME=frameleaf",
|
||||
"TZ=Etc/UTC",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "frameleaf"
|
||||
port = 2283
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,301 @@
|
||||
# FreeFrame — collaborative media review and approval for images, audio and
|
||||
# video (a self-hosted Frame.io alternative).
|
||||
#
|
||||
# Adapted from the upstream production compose files:
|
||||
# docker-compose.prod.yml, docker-compose.images.yml and
|
||||
# docker-compose.storage.yml in https://github.com/Techiebutler/freeframe
|
||||
#
|
||||
# Three things differ from upstream, all Dokploy-specific:
|
||||
# * Dokploy's Traefik is the only reverse proxy: no `traefik` service here.
|
||||
# Domains are declared in template.toml, not in this file.
|
||||
# * The API registers its routes WITHOUT a prefix while the web image calls
|
||||
# /api/*, so the API domain needs `path = "/api"` + `stripPath = true` in
|
||||
# template.toml — that is what makes both services share one domain.
|
||||
# * Storage (Silo, a MinIO fork) is exposed on a second domain, because the
|
||||
# browser fetches presigned S3 URLs directly and cannot resolve the
|
||||
# internal `freeframe-silo` hostname.
|
||||
#
|
||||
# No GPU is involved anywhere: the transcoding worker detects none and encodes
|
||||
# in software (x264/x265) on the CPU. Size TRANSCODING_CONCURRENCY to the cores.
|
||||
#
|
||||
# The API and the four Celery containers are the same image and read the same
|
||||
# settings at import, so the same environment block repeats in each of them.
|
||||
|
||||
services:
|
||||
freeframe-postgres:
|
||||
image: postgres:15-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- freeframe-postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-freeframe}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
freeframe-redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
# Password-protected and AOF-persisted: this holds the Celery queues (video
|
||||
# transcodes survive a restart) and the login magic codes.
|
||||
command: >
|
||||
sh -c "redis-server --requirepass \"$$REDIS_PASSWORD\" --appendonly yes"
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- freeframe-redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" --no-auth-warning ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Object storage. FreeFrame has no local-filesystem backend: uploads, the
|
||||
# HLS renditions, posters and avatars all live in a bucket.
|
||||
freeframe-silo:
|
||||
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${S3_ACCESS_KEY}
|
||||
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
|
||||
volumes:
|
||||
- freeframe-silo-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
freeframe-api:
|
||||
image: ghcr.io/techiebutler/freeframe-api:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
FRONTEND_URL: ${FRONTEND_URL}
|
||||
S3_STORAGE: ${S3_STORAGE}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT}
|
||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||
S3_BUCKET: ${S3_BUCKET}
|
||||
S3_REGION: ${S3_REGION}
|
||||
MAIL_PROVIDER: ${MAIL_PROVIDER}
|
||||
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
|
||||
SMTP_HOST: ${SMTP_HOST}
|
||||
SMTP_PORT: ${SMTP_PORT}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||
API_WORKERS: ${API_WORKERS:-4}
|
||||
TRANSCODING_CONCURRENCY: ${TRANSCODING_CONCURRENCY:-2}
|
||||
TRANSCODER_CPU_LIMIT: ${TRANSCODER_CPU_LIMIT:-}
|
||||
TRANSCODER_QUALITIES: ${TRANSCODER_QUALITIES:-1080p,720p,360p}
|
||||
TRANSCODER_SOURCE_COPY: ${TRANSCODER_SOURCE_COPY:-false}
|
||||
DISABLE_DOCS: "true"
|
||||
depends_on:
|
||||
freeframe-postgres:
|
||||
condition: service_healthy
|
||||
freeframe-redis:
|
||||
condition: service_healthy
|
||||
freeframe-silo:
|
||||
condition: service_healthy
|
||||
# The schema is created on first start and upgraded on every deploy. The
|
||||
# bucket and its CORS rules are created by the API itself, from
|
||||
# FRONTEND_URL, so no init container is needed.
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
cd /workspace/apps/api && alembic upgrade head && cd /workspace &&
|
||||
gunicorn apps.api.main:app
|
||||
-w $${API_WORKERS:-4}
|
||||
-k uvicorn.workers.UvicornWorker
|
||||
--bind 0.0.0.0:8000
|
||||
--timeout 120
|
||||
--graceful-timeout 30
|
||||
--access-logfile -
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 40s
|
||||
retries: 3
|
||||
|
||||
# Video transcoding. CPU-bound, and the reason a 4+ core box is recommended.
|
||||
freeframe-worker:
|
||||
image: ghcr.io/techiebutler/freeframe-api:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
FRONTEND_URL: ${FRONTEND_URL}
|
||||
S3_STORAGE: ${S3_STORAGE}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT}
|
||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||
S3_BUCKET: ${S3_BUCKET}
|
||||
S3_REGION: ${S3_REGION}
|
||||
MAIL_PROVIDER: ${MAIL_PROVIDER}
|
||||
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
|
||||
SMTP_HOST: ${SMTP_HOST}
|
||||
SMTP_PORT: ${SMTP_PORT}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||
API_WORKERS: ${API_WORKERS:-4}
|
||||
TRANSCODING_CONCURRENCY: ${TRANSCODING_CONCURRENCY:-2}
|
||||
TRANSCODER_CPU_LIMIT: ${TRANSCODER_CPU_LIMIT:-}
|
||||
TRANSCODER_QUALITIES: ${TRANSCODER_QUALITIES:-1080p,720p,360p}
|
||||
TRANSCODER_SOURCE_COPY: ${TRANSCODER_SOURCE_COPY:-false}
|
||||
depends_on:
|
||||
freeframe-postgres:
|
||||
condition: service_healthy
|
||||
freeframe-redis:
|
||||
condition: service_healthy
|
||||
freeframe-silo:
|
||||
condition: service_healthy
|
||||
command: sh -c "celery -A apps.api.tasks.celery_app worker -Q transcoding -c $${TRANSCODING_CONCURRENCY:-2} --loglevel=INFO"
|
||||
|
||||
freeframe-email-worker:
|
||||
image: ghcr.io/techiebutler/freeframe-api:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
FRONTEND_URL: ${FRONTEND_URL}
|
||||
S3_STORAGE: ${S3_STORAGE}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT}
|
||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||
S3_BUCKET: ${S3_BUCKET}
|
||||
S3_REGION: ${S3_REGION}
|
||||
MAIL_PROVIDER: ${MAIL_PROVIDER}
|
||||
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
|
||||
SMTP_HOST: ${SMTP_HOST}
|
||||
SMTP_PORT: ${SMTP_PORT}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||
API_WORKERS: ${API_WORKERS:-4}
|
||||
TRANSCODING_CONCURRENCY: ${TRANSCODING_CONCURRENCY:-2}
|
||||
TRANSCODER_CPU_LIMIT: ${TRANSCODER_CPU_LIMIT:-}
|
||||
TRANSCODER_QUALITIES: ${TRANSCODER_QUALITIES:-1080p,720p,360p}
|
||||
TRANSCODER_SOURCE_COPY: ${TRANSCODER_SOURCE_COPY:-false}
|
||||
depends_on:
|
||||
freeframe-redis:
|
||||
condition: service_healthy
|
||||
command: sh -c "celery -A apps.api.tasks.celery_app worker -Q email_high,email_low -c 2 --loglevel=INFO"
|
||||
|
||||
# Housekeeping: retention GC, stale-upload reaper, orphan sweep. Kept off the
|
||||
# transcoding worker (these hold a slot for minutes) and off the email worker
|
||||
# (a magic code queued behind a bucket listing is a login someone waits on).
|
||||
# These tasks hard-delete past their retention windows.
|
||||
freeframe-maintenance-worker:
|
||||
image: ghcr.io/techiebutler/freeframe-api:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
FRONTEND_URL: ${FRONTEND_URL}
|
||||
S3_STORAGE: ${S3_STORAGE}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT}
|
||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||
S3_BUCKET: ${S3_BUCKET}
|
||||
S3_REGION: ${S3_REGION}
|
||||
MAIL_PROVIDER: ${MAIL_PROVIDER}
|
||||
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
|
||||
SMTP_HOST: ${SMTP_HOST}
|
||||
SMTP_PORT: ${SMTP_PORT}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||
API_WORKERS: ${API_WORKERS:-4}
|
||||
TRANSCODING_CONCURRENCY: ${TRANSCODING_CONCURRENCY:-2}
|
||||
TRANSCODER_CPU_LIMIT: ${TRANSCODER_CPU_LIMIT:-}
|
||||
TRANSCODER_QUALITIES: ${TRANSCODER_QUALITIES:-1080p,720p,360p}
|
||||
TRANSCODER_SOURCE_COPY: ${TRANSCODER_SOURCE_COPY:-false}
|
||||
depends_on:
|
||||
freeframe-postgres:
|
||||
condition: service_healthy
|
||||
freeframe-redis:
|
||||
condition: service_healthy
|
||||
freeframe-silo:
|
||||
condition: service_healthy
|
||||
command: sh -c "celery -A apps.api.tasks.celery_app worker -Q maintenance -c 1 --loglevel=INFO"
|
||||
|
||||
freeframe-beat:
|
||||
image: ghcr.io/techiebutler/freeframe-api:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
FRONTEND_URL: ${FRONTEND_URL}
|
||||
S3_STORAGE: ${S3_STORAGE}
|
||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||
S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT}
|
||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||
S3_BUCKET: ${S3_BUCKET}
|
||||
S3_REGION: ${S3_REGION}
|
||||
MAIL_PROVIDER: ${MAIL_PROVIDER}
|
||||
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
|
||||
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
|
||||
SMTP_HOST: ${SMTP_HOST}
|
||||
SMTP_PORT: ${SMTP_PORT}
|
||||
SMTP_USER: ${SMTP_USER}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
||||
SMTP_USE_TLS: ${SMTP_USE_TLS}
|
||||
API_WORKERS: ${API_WORKERS:-4}
|
||||
TRANSCODING_CONCURRENCY: ${TRANSCODING_CONCURRENCY:-2}
|
||||
TRANSCODER_CPU_LIMIT: ${TRANSCODER_CPU_LIMIT:-}
|
||||
TRANSCODER_QUALITIES: ${TRANSCODER_QUALITIES:-1080p,720p,360p}
|
||||
TRANSCODER_SOURCE_COPY: ${TRANSCODER_SOURCE_COPY:-false}
|
||||
depends_on:
|
||||
freeframe-redis:
|
||||
condition: service_healthy
|
||||
command: sh -c "celery -A apps.api.tasks.celery_app beat --loglevel=INFO -s /tmp/celerybeat-schedule"
|
||||
|
||||
freeframe:
|
||||
image: ghcr.io/techiebutler/freeframe-web:1.22.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Server-to-server, for the branding Next renders into the first byte. It
|
||||
# cannot reuse NEXT_PUBLIC_API_URL, which is the `/api` path the browser
|
||||
# resolves against this origin: a fetch inside this container has no
|
||||
# origin to resolve it against. The browser-facing URL is baked into the
|
||||
# published image and is always /api.
|
||||
API_INTERNAL_URL: http://freeframe-api:8000
|
||||
UPLOAD_CONCURRENCY: ${UPLOAD_CONCURRENCY:-5}
|
||||
depends_on:
|
||||
freeframe-api:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:3000/login"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
freeframe-postgres-data:
|
||||
freeframe-redis-data:
|
||||
freeframe-silo-data:
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,94 @@
|
||||
# FreeFrame
|
||||
|
||||
FreeFrame is a self-hostable, open-source alternative to Frame.io: collaborative
|
||||
review, annotation and approval for images, audio and video, with frame-accurate
|
||||
comments, version comparison, share links and an approval workflow.
|
||||
|
||||
## Two domains
|
||||
|
||||
- The **main domain** serves the web app (service `freeframe`, port 3000). Its
|
||||
`/api` path is routed to the API (service `freeframe-api`, port 8000) with the
|
||||
prefix stripped, so both live on one hostname — that is what the web image
|
||||
expects, and why the API domain entry has `path = "/api"` and
|
||||
`stripPath = true`.
|
||||
- The **second domain** exposes the bundled object store (service
|
||||
`freeframe-silo`, port 9000). FreeFrame has no local-filesystem storage: media
|
||||
is uploaded straight to the bucket with presigned URLs that the *browser*
|
||||
fetches, so this endpoint has to be publicly reachable. It is not an internal
|
||||
detail.
|
||||
|
||||
## First start
|
||||
|
||||
`freeframe-api` waits for Postgres, Redis and the object store to report healthy,
|
||||
then runs `alembic upgrade head` and starts. It also creates the bucket and its
|
||||
CORS rules on startup from `FRONTEND_URL`, so there is nothing to set up by hand.
|
||||
Open the main domain and complete the setup wizard: **the first account becomes
|
||||
the super admin**, and it is created with a password, so no mail server is needed
|
||||
to sign in the first time.
|
||||
|
||||
Uploads take a moment before they are reviewable: a background worker transcodes
|
||||
each video into the review streams. There is no GPU in this template, so encoding
|
||||
is software x264/x265 on the CPU.
|
||||
|
||||
## HTTP by default, HTTPS later
|
||||
|
||||
`FRONTEND_URL` and `S3_PUBLIC_ENDPOINT` default to **`http://`**, so the stack works
|
||||
immediately on a host without a valid certificate (including the `*.traefik.me` names
|
||||
Dokploy generates when no domain is configured — those can never get a trusted
|
||||
certificate, so HTTPS there means every upload fails with
|
||||
`ERR_CERT_AUTHORITY_INVALID` before a single byte is sent).
|
||||
|
||||
To serve FreeFrame over HTTPS, with a real domain pointing at the server:
|
||||
|
||||
1. Enable HTTPS (Let's Encrypt) on **all three** domains in Dokploy. Do not enable it on
|
||||
one and leave another on HTTP: an HTTPS page cannot upload to an HTTP storage endpoint
|
||||
(the browser blocks it as mixed content), and an HTTP page cannot use an HTTPS storage
|
||||
endpoint without a trusted certificate.
|
||||
2. Change both variables to `https://` and redeploy:
|
||||
```
|
||||
FRONTEND_URL=https://<main domain>
|
||||
S3_PUBLIC_ENDPOINT=https://<storage domain>
|
||||
```
|
||||
3. **Recreate the API** after changing them. The bucket's CORS rules are written from
|
||||
`FRONTEND_URL` when the API starts, so a mismatch shows up as uploads stuck at 0% while
|
||||
the UI itself still loads.
|
||||
|
||||
If uploads stop working after a scheme change, that pair of variables is the first thing
|
||||
to check, and the second is whether the storage domain redirects HTTP to HTTPS.
|
||||
|
||||
## Sizing
|
||||
|
||||
Video transcoding is the only heavy operation, and it runs on the CPU here. 2 cores
|
||||
/ 4 GB is the documented minimum; 4 cores / 8 GB is the recommended starting point.
|
||||
|
||||
`template.toml` sets the tuning settings to the values FreeFrame would use anyway,
|
||||
so they are visible and editable in Dokploy's **Environment** tab after import (TOML
|
||||
comments are not carried over — only key/value pairs are). `docker-compose.yml` also
|
||||
defaults each one, so deleting a variable falls back to the same value. They are:
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| `TRANSCODING_CONCURRENCY` | `2` | Videos encoded at once. Size it to the cores (1 on 2 cores, 2-3 on 4, 4-6 on 8+). |
|
||||
| `TRANSCODER_CPU_LIMIT` | *(unbounded)* | Cores one ffmpeg may use: `4` or `40%`. Unbounded takes about 80% of a 10-core host for one three-rung ladder, so set this when the UI must stay responsive while encoding. Machine ceiling is this times `TRANSCODING_CONCURRENCY`. |
|
||||
| `TRANSCODER_QUALITIES` | `1080p,720p,360p` | Dropping `360p` saves CPU and storage but leaves viewers on slow connections without a lower rendition. |
|
||||
| `TRANSCODER_SOURCE_COPY` | `false` | Remux a browser-safe H.264 8-bit master instead of re-encoding it, when the ladder resolves to one rendition at the source's own size. |
|
||||
| `API_WORKERS` | `4` | Gunicorn workers in the API container. |
|
||||
| `UPLOAD_CONCURRENCY` | `5` | Upload parts in flight per tab; 6 is the useful maximum over HTTP/1.1. |
|
||||
|
||||
`max(1, round(cores × share))` rounds a percentage up, and a value at or above the
|
||||
available core count is treated as unbounded and reported in the worker log.
|
||||
|
||||
## Backups
|
||||
|
||||
Three volumes hold state: `freeframe-postgres-data` (metadata), `freeframe-silo-data`
|
||||
(originals and every rendition) and `freeframe-redis-data` (queues). Back up the
|
||||
database and the bucket together. Housekeeping runs on a schedule and hard-deletes
|
||||
soft-deleted rows and their objects after `SOFT_DELETE_RETENTION_DAYS` (30 by
|
||||
default); set it to `0` to switch that off.
|
||||
|
||||
## Versioning
|
||||
|
||||
Images are pinned to the release this template ships (`1.22.0`). To move to another
|
||||
release, change the three `ghcr.io/techiebutler/freeframe-*` tags together — the API,
|
||||
the four Celery containers and the web app are published per release — and run
|
||||
`alembic upgrade head` again by redeploying.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "freeframe",
|
||||
"name": "FreeFrame",
|
||||
"version": "1.22.0",
|
||||
"description": "FreeFrame is an open-source, self-hostable alternative to Frame.io: frame-accurate video, image and audio review with annotations, version comparison, share links and approvals.",
|
||||
"logo": "freeframe.png",
|
||||
"links": {
|
||||
"github": "https://github.com/Techiebutler/freeframe",
|
||||
"website": "https://github.com/Techiebutler/freeframe",
|
||||
"docs": "https://github.com/Techiebutler/freeframe/blob/main/docs/deployment.md"
|
||||
},
|
||||
"tags": ["media", "video", "collaboration", "file-sharing", "self-hosted"]
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
storage_domain = "${domain}"
|
||||
postgres_password = "${password:32}"
|
||||
redis_password = "${password:32}"
|
||||
jwt_secret = "${hash:64}"
|
||||
s3_access_key = "freeframe"
|
||||
s3_secret_key = "${password:32}"
|
||||
frontend_url = "http://${main_domain}"
|
||||
database_url = "postgresql://freeframe:${postgres_password}@freeframe-postgres:5432/freeframe"
|
||||
redis_url = "redis://:${redis_password}@freeframe-redis:6379/0"
|
||||
s3_endpoint = "http://freeframe-silo:9000"
|
||||
s3_public_endpoint = "http://${storage_domain}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "freeframe"
|
||||
port = 3000
|
||||
host = "${main_domain}"
|
||||
|
||||
# The API's routes have no prefix, but the browser calls /api/*, so the path is
|
||||
# stripped before the request reaches the container.
|
||||
[[config.domains]]
|
||||
serviceName = "freeframe-api"
|
||||
port = 8000
|
||||
host = "${main_domain}"
|
||||
path = "/api"
|
||||
stripPath = true
|
||||
|
||||
# Presigned S3 URLs are handed to the browser, so the store needs its own
|
||||
# publicly reachable hostname.
|
||||
[[config.domains]]
|
||||
serviceName = "freeframe-silo"
|
||||
port = 9000
|
||||
host = "${storage_domain}"
|
||||
|
||||
[config.env]
|
||||
POSTGRES_USER = "freeframe"
|
||||
POSTGRES_PASSWORD = "${postgres_password}"
|
||||
POSTGRES_DB = "freeframe"
|
||||
REDIS_PASSWORD = "${redis_password}"
|
||||
DATABASE_URL = "${database_url}"
|
||||
REDIS_URL = "${redis_url}"
|
||||
JWT_SECRET = "${jwt_secret}"
|
||||
|
||||
# Exact origin people browse to. The API derives its CORS policy and the
|
||||
# bucket's CORS rules from it, so it must match the scheme and host actually
|
||||
# served: http by default (works without a certificate), https once HTTPS is
|
||||
# enabled on the domains — then change this AND S3_PUBLIC_ENDPOINT together and
|
||||
# recreate the API, see instructions.md.
|
||||
FRONTEND_URL = "${frontend_url}"
|
||||
|
||||
S3_STORAGE = "minio"
|
||||
S3_ENDPOINT = "${s3_endpoint}"
|
||||
S3_PUBLIC_ENDPOINT = "${s3_public_endpoint}"
|
||||
S3_ACCESS_KEY = "${s3_access_key}"
|
||||
S3_SECRET_KEY = "${s3_secret_key}"
|
||||
S3_BUCKET = "freeframe"
|
||||
S3_REGION = "us-east-1"
|
||||
|
||||
# ── Tuning ───────────────────────────────────────────────────────────────────
|
||||
# These are set to FreeFrame's own defaults, so a fresh deploy behaves exactly
|
||||
# like upstream. Change a value in Dokploy's Environment tab and redeploy the
|
||||
# container it affects. (TOML comments like this one are not carried into the
|
||||
# Environment tab — Dokploy keeps key/value pairs only — so there is nothing to
|
||||
# uncomment here: the lines below are already active.)
|
||||
#
|
||||
# Parallel video encodes (Celery workers on the transcoding queue). Size it to
|
||||
# the cores: 1 on 2 cores, 2-3 on 4, 4-6 on 8+. There is no GPU here, so
|
||||
# encoding is software x264/x265 and each job saturates the machine unless it
|
||||
# is capped below.
|
||||
# TRANSCODING_CONCURRENCY (below)
|
||||
#
|
||||
# Cores ONE ffmpeg process may use: a count ("4") or a share of what the
|
||||
# container may use ("40%"). Empty = unbounded, which on a 10-core host is
|
||||
# about 8 cores for a single three-rung ladder. The machine-wide ceiling is
|
||||
# this times TRANSCODING_CONCURRENCY; it does not cover decoding, so a heavy
|
||||
# master can run ~25% over.
|
||||
# TRANSCODER_CPU_LIMIT (below; empty by default = unbounded)
|
||||
#
|
||||
# Review stream ladder. Dropping 360p saves CPU and storage, at the cost of a
|
||||
# lower rendition for viewers on slow connections.
|
||||
# TRANSCODER_QUALITIES (below)
|
||||
#
|
||||
# Remux a browser-safe H.264 8-bit master instead of re-encoding it: minutes
|
||||
# become seconds. Only applies when the ladder resolves to ONE rendition at the
|
||||
# source's own size, e.g. TRANSCODER_QUALITIES = "1080p".
|
||||
# TRANSCODER_SOURCE_COPY (below)
|
||||
#
|
||||
# API gunicorn workers, and upload parts in flight per browser tab (6 is the
|
||||
# useful maximum over HTTP/1.1; more only helps with HTTP/2).
|
||||
# API_WORKERS, UPLOAD_CONCURRENCY (below)
|
||||
|
||||
TRANSCODING_CONCURRENCY = "2"
|
||||
TRANSCODER_CPU_LIMIT = ""
|
||||
TRANSCODER_QUALITIES = "1080p,720p,360p"
|
||||
TRANSCODER_SOURCE_COPY = "false"
|
||||
API_WORKERS = "4"
|
||||
UPLOAD_CONCURRENCY = "5"
|
||||
|
||||
# Optional. The first account is created in the setup wizard with a password,
|
||||
# so the app is usable without a mail server; magic-code login, invitations and
|
||||
# notification emails need one.
|
||||
MAIL_PROVIDER = "smtp"
|
||||
MAIL_FROM_ADDRESS = "noreply@${main_domain}"
|
||||
MAIL_FROM_NAME = "FreeFrame"
|
||||
SMTP_HOST = ""
|
||||
SMTP_PORT = "587"
|
||||
SMTP_USER = ""
|
||||
SMTP_PASSWORD = ""
|
||||
SMTP_USE_TLS = "true"
|
||||
@@ -0,0 +1,83 @@
|
||||
# Testing FreeFrame
|
||||
|
||||
Use a disposable Dokploy project and test accounts. Upload only media you own, and do not
|
||||
send invitation or notification mail to addresses you do not control.
|
||||
|
||||
## Repository checks
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
node build-scripts/generate-meta.js --check
|
||||
cd build-scripts
|
||||
pnpm exec tsx validate-docker-compose.ts --file ../blueprints/freeframe/docker-compose.yml --verbose
|
||||
pnpm exec tsx validate-template.ts --dir ../blueprints/freeframe --verbose
|
||||
cd ../app
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Confirm that `meta.json`'s `version` (1.22.0) matches the three `ghcr.io/techiebutler/freeframe-*`
|
||||
tags. With a temporary `.env` holding the 24 variables `template.toml` generates, run
|
||||
`docker compose config --quiet` in the blueprint directory: it must pass, and it must fail if
|
||||
`DATABASE_URL`, `REDIS_URL` or `JWT_SECRET` is removed (the API reads `Settings` at import, so
|
||||
those three are not optional).
|
||||
|
||||
## Dokploy integration
|
||||
|
||||
1. Open the PR preview, search for **FreeFrame**, and copy the Base64 import configuration.
|
||||
Import it into a new Compose service and deploy with the generated environment. No
|
||||
variable needs editing for a first run.
|
||||
2. **Domains.** Three must exist after import: the main domain on `freeframe:3000`, the same
|
||||
main domain with path `/api` and **Strip Path** on `freeframe-api:8000`, and a second
|
||||
domain on `freeframe-silo:9000`. Verify `https://<main-domain>/api/health` answers through
|
||||
the strip-path router, and that the web app itself loads at `https://<main-domain>/`.
|
||||
3. **First start.** Wait for `freeframe-postgres`, `freeframe-redis` and `freeframe-silo` to
|
||||
report healthy; the API then runs `alembic upgrade head` before gunicorn. Check the deploy
|
||||
log for a completed migration and no restart loop, then complete the setup wizard — the
|
||||
first account becomes the super admin, with a password, without any mail server.
|
||||
4. **Storage.** The API creates the bucket and its CORS rules on startup from `FRONTEND_URL`.
|
||||
Confirm `https://<storage-domain>` is reachable in a browser (a 404/`AccessDenied` XML is
|
||||
fine, a connection error or certificate warning is not). Never expose the console (9001).
|
||||
5. **A real upload.** Create a project, upload a short 1080p clip. The browser PUTs parts
|
||||
straight to the storage domain (presigned, so no proxy limit applies) and the
|
||||
`freeframe-worker` transcodes it on the CPU: the deploy log shows the ffmpeg backend as
|
||||
`cpu`, and the asset becomes playable at 1080p/720p/360p. Check that the poster, the HLS
|
||||
streams and the download rendition appear, and that a second version can be compared with
|
||||
the first frame by frame.
|
||||
6. **Collaboration.** Add a comment at a timecode and a drawing annotation, then create a
|
||||
share link and open it in a private window: the reviewer sees the player and comments but
|
||||
no admin UI. Verify an approval decision is recorded.
|
||||
7. **Restart and recreation.** Force a redeploy (recreate the containers). Confirm the super
|
||||
admin login, the project, the uploaded media and every rendition survive, and that the
|
||||
bucket still holds the objects — `freeframe-postgres-data`, `freeframe-redis-data` and
|
||||
`freeframe-silo-data` are the only state.
|
||||
8. **HTTPS.** With a real domain, enable it on all three domains and change
|
||||
`FRONTEND_URL` and `S3_PUBLIC_ENDPOINT` to `https://` in the same redeploy, then recreate
|
||||
the API so the bucket CORS is rewritten. Verify uploads and playback still work (a
|
||||
scheme mismatch shows up here and nowhere else). Leaving everything on `http://` is the
|
||||
supported default and needs nothing. A `*.traefik.me` host cannot serve a trusted
|
||||
certificate: over HTTPS there, every part `PUT` fails with
|
||||
`ERR_CERT_AUTHORITY_INVALID` and the upload sits at 0%.
|
||||
9. **Optional settings.** Set `SMTP_*` only in Dokploy's Environment, redeploy, and check
|
||||
magic-code login and an invitation email against a local capture server. Confirm the six
|
||||
tuning variables arrived in the Environment tab (TOML comments do not survive the import,
|
||||
so only key/value pairs do), set `TRANSCODER_CPU_LIMIT` to `40%`, redeploy the transcoding
|
||||
worker, and check that the log reports the budget while a second upload waits behind the
|
||||
first instead of both encoding at once.
|
||||
|
||||
## Scope
|
||||
|
||||
These checks validate the template and core workflows. Real SMTP providers, an external S3
|
||||
bucket (R2/B2/AWS), HEVC 10-bit and HDR sources, large 4K ladders, Garage or other S3
|
||||
implementations, and long-running retention sweeps each need their own setup and are not
|
||||
implied by a healthy container. Intel (VAAPI) transcoding is not part of this template: the
|
||||
published images ship NVENC only, and there is no GPU here.
|
||||
|
||||
## Verified configuration
|
||||
|
||||
Fill this in from your own run — do not copy it from a template author's report.
|
||||
|
||||
- Tested on: `<date>`, Dokploy `<version>`, Traefik `<version>`, `<host arch>`.
|
||||
- Images: `ghcr.io/techiebutler/freeframe-api:1.22.0`, `ghcr.io/techiebutler/freeframe-web:1.22.0`.
|
||||
- Result: `<what passed>`, `<what could not be exercised, and why>`.
|
||||
@@ -0,0 +1,58 @@
|
||||
services:
|
||||
homelable-backend:
|
||||
image: ghcr.io/pouzor/homelable-backend:3.6.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SECRET_KEY: ${SECRET_KEY}
|
||||
SQLITE_PATH: ${SQLITE_PATH}
|
||||
CORS_ORIGINS: ${CORS_ORIGINS}
|
||||
AUTH_MODE: ${AUTH_MODE}
|
||||
AUTH_USERNAME: ${AUTH_USERNAME}
|
||||
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD}
|
||||
SCANNER_RANGES: ${SCANNER_RANGES}
|
||||
STATUS_CHECKER_INTERVAL: ${STATUS_CHECKER_INTERVAL}
|
||||
STATUS_CHECKER_ENABLED: ${STATUS_CHECKER_ENABLED}
|
||||
# The upstream image accepts a bcrypt hash, while Dokploy can generate a
|
||||
# password but cannot generate a bcrypt hash. Hash the generated password
|
||||
# before starting uvicorn, then remove the plaintext from the app process.
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
export AUTH_PASSWORD_HASH="$$(python -c 'import bcrypt, os; print(bcrypt.hashpw(os.environ["INITIAL_ADMIN_PASSWORD"].encode(), bcrypt.gensalt()).decode())')"
|
||||
unset INITIAL_ADMIN_PASSWORD
|
||||
exec "$$@"
|
||||
- --
|
||||
command:
|
||||
- uvicorn
|
||||
- app.main:app
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- "8000"
|
||||
expose:
|
||||
- "8000"
|
||||
cap_add:
|
||||
- NET_RAW
|
||||
volumes:
|
||||
- homelable-data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
|
||||
homelable:
|
||||
image: ghcr.io/pouzor/homelable-frontend:3.6.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
homelable-backend:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
BACKEND_UPSTREAM: homelable-backend:8000
|
||||
expose:
|
||||
- "80"
|
||||
|
||||
volumes:
|
||||
homelable-data:
|
||||
@@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 82" width="64" height="82" fill="none">
|
||||
<defs>
|
||||
<radialGradient id="bg-glow" cx="50%" cy="35%" r="55%">
|
||||
<stop offset="0%" stop-color="#00d4ff" stop-opacity="0.08"/>
|
||||
<stop offset="100%" stop-color="#0d1117" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="node-glow">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<circle cx="32" cy="32" r="32" fill="#0d1117"/>
|
||||
<circle cx="32" cy="32" r="32" fill="url(#bg-glow)"/>
|
||||
<path d="M32 11 L53 30 L48 30 L48 53 L16 53 L16 30 L11 30 Z"
|
||||
fill="#161b22" stroke="#00d4ff" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<line x1="16" y1="30" x2="48" y2="30" stroke="#00d4ff" stroke-width="0.5" opacity="0.25"/>
|
||||
<rect x="27" y="40" width="10" height="13" rx="1.5"
|
||||
fill="#0d1117" stroke="#00d4ff" stroke-width="1" opacity="0.9"/>
|
||||
<line x1="32" y1="23" x2="32" y2="30" stroke="#a855f7" stroke-width="1.2" opacity="0.7" stroke-linecap="round"/>
|
||||
<line x1="21" y1="38" x2="29" y2="33" stroke="#39d353" stroke-width="1.2" opacity="0.7" stroke-linecap="round"/>
|
||||
<line x1="43" y1="38" x2="35" y2="33" stroke="#39d353" stroke-width="1.2" opacity="0.7" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="33" r="5" fill="#00d4ff" opacity="0.12"/>
|
||||
<circle cx="32" cy="33" r="3" fill="#00d4ff" filter="url(#node-glow)"/>
|
||||
<circle cx="21" cy="38" r="2.5" fill="#39d353" filter="url(#node-glow)"/>
|
||||
<circle cx="43" cy="38" r="2.5" fill="#39d353" filter="url(#node-glow)"/>
|
||||
<circle cx="32" cy="23" r="2" fill="#a855f7" filter="url(#node-glow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "homelable",
|
||||
"name": "Homelable",
|
||||
"version": "3.6.0",
|
||||
"description": "Self-hosted homelab infrastructure visualisation, documentation, network scanning and live monitoring.",
|
||||
"logo": "homelable.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/Pouzor/homelable",
|
||||
"website": "https://homelable.net",
|
||||
"docs": "https://github.com/Pouzor/homelable/blob/main/INSTALLATION.md"
|
||||
},
|
||||
"tags": [
|
||||
"homelab",
|
||||
"network",
|
||||
"monitoring",
|
||||
"documentation",
|
||||
"self-hosted"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
secret_key = "${base64:32}"
|
||||
admin_password = "${password:32}"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "homelable"
|
||||
port = 80
|
||||
host = "${main_domain}"
|
||||
path = "/"
|
||||
|
||||
[config.env]
|
||||
SECRET_KEY = "${secret_key}"
|
||||
SQLITE_PATH = "/app/data/homelab.db"
|
||||
CORS_ORIGINS = '["https://${main_domain}", "http://${main_domain}"]'
|
||||
AUTH_MODE = "local"
|
||||
AUTH_USERNAME = "admin"
|
||||
INITIAL_ADMIN_PASSWORD = "${admin_password}"
|
||||
SCANNER_RANGES = '["192.168.1.0/24"]'
|
||||
STATUS_CHECKER_INTERVAL = "60"
|
||||
STATUS_CHECKER_ENABLED = "true"
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
echo "bucket ready: $$S3_BUCKET"
|
||||
|
||||
migrate:
|
||||
image: ghcr.io/croffasia/itsaplan-api:1.9.0
|
||||
image: ghcr.io/croffasia/itsaplan-api:${VERSION:-1.10.0}
|
||||
restart: "no"
|
||||
command: ["bun", "run", "packages/db/src/migrate.ts"]
|
||||
depends_on:
|
||||
@@ -79,7 +79,7 @@ services:
|
||||
- db-backups:/backups
|
||||
|
||||
api:
|
||||
image: ghcr.io/croffasia/itsaplan-api:1.9.0
|
||||
image: ghcr.io/croffasia/itsaplan-api:${VERSION:-1.10.0}
|
||||
restart: unless-stopped
|
||||
command: ["bun", "run", "apps/api/src/index.ts"]
|
||||
depends_on:
|
||||
@@ -118,7 +118,7 @@ services:
|
||||
- "3000"
|
||||
|
||||
worker:
|
||||
image: ghcr.io/croffasia/itsaplan-worker:1.9.0
|
||||
image: ghcr.io/croffasia/itsaplan-worker:${VERSION:-1.10.0}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -141,7 +141,7 @@ services:
|
||||
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-true}
|
||||
|
||||
bot:
|
||||
image: ghcr.io/croffasia/itsaplan-bot:1.9.0
|
||||
image: ghcr.io/croffasia/itsaplan-bot:${VERSION:-1.10.0}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -154,7 +154,7 @@ services:
|
||||
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in Dokploy}
|
||||
|
||||
web:
|
||||
image: ghcr.io/croffasia/itsaplan-web:1.9.0
|
||||
image: ghcr.io/croffasia/itsaplan-web:${VERSION:-1.10.0}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "itsaplan",
|
||||
"name": "It's a Plan",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"description": "It's a Plan is an open-source project management and issue tracking platform where people and AI agents work together.",
|
||||
"logo": "itsaplan.svg",
|
||||
"links": {
|
||||
|
||||
@@ -10,6 +10,7 @@ s3_secret_key = "${password:32}"
|
||||
[config]
|
||||
env = [
|
||||
"POSTGRES_USER=itsaplan",
|
||||
"VERSION=1.10.0",
|
||||
"POSTGRES_PASSWORD=${postgres_password}",
|
||||
"POSTGRES_DB=itsaplan",
|
||||
"BETTER_AUTH_SECRET=${auth_secret}",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
kividb:
|
||||
image: kividb/kividb:v1.2.0
|
||||
restart: unless-stopped
|
||||
command: ["--requirepass", "${KIVIDB_PASSWORD}", "--aof"]
|
||||
expose:
|
||||
- 6380
|
||||
volumes:
|
||||
- kividb-data:/home/kividb
|
||||
|
||||
volumes:
|
||||
kividb-data: {}
|
||||
@@ -0,0 +1,6 @@
|
||||
1. KiviDB speaks the Redis protocol (RESP2/RESP3) on port `6380`, not 6379. It has no web interface, so this template has no domain.
|
||||
2. Inside this compose project, connect to `kividb:6380`, e.g. `redis://:<password>@kividb:6380`. Use any Redis client or `redis-cli -h kividb -p 6380`.
|
||||
3. The password is the generated `KIVIDB_PASSWORD` in the Dokploy environment settings.
|
||||
4. Data is kept in the `kividb-data` volume (`/home/kividb`), with append-only persistence turned on (`--aof`).
|
||||
5. The image has no shell and no `redis-cli`, so there is no container healthcheck and `docker exec` into it will not work; check it from a client instead.
|
||||
6. Keep port `6380` private unless remote clients need direct access.
|
||||
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "kividb",
|
||||
"name": "KiviDB",
|
||||
"version": "1.2.0",
|
||||
"description": "KiviDB is a Redis-compatible in-memory database written in Rust, with full-text and vector search (FT.*) built in.",
|
||||
"logo": "kividb.png",
|
||||
"links": {
|
||||
"github": "https://github.com/kividbio/kividb",
|
||||
"website": "https://kividb.io/",
|
||||
"docs": "https://kividb.io/docs"
|
||||
},
|
||||
"tags": [
|
||||
"database",
|
||||
"cache",
|
||||
"redis",
|
||||
"in-memory",
|
||||
"vector-database"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[variables]
|
||||
kividb_password = "${password:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"KIVIDB_PASSWORD=${kividb_password}"
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
libredb-studio:
|
||||
image: ghcr.io/libredb/libredb-studio:1.2.0
|
||||
image: ghcr.io/libredb/libredb-studio:1.3.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Since 0.18.0 ADMIN_PASSWORD and USER_PASSWORD seed the account table only
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "libredb-studio",
|
||||
"name": "LibreDB Studio",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "The open-source SQL IDE you host yourself. Query PostgreSQL, MySQL, Oracle, SQL Server, MongoDB, Redis, ClickHouse, Cassandra and more from your browser, forty-two drivers reaching sixty-nine named engines.",
|
||||
"logo": "libredb-studio.svg",
|
||||
"links": {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/tj-coding/life-console:0.1.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgres://lifeconsole:${DB_PASSWORD}@db:5432/lifeconsole
|
||||
APP_URL: ${APP_URL}
|
||||
TRUST_PROXY: "1"
|
||||
SIGNUPS: open
|
||||
AUTH_PASSWORD: "on"
|
||||
UPDATE_CHECK: "off"
|
||||
BACKUP_DIR: /backups
|
||||
expose:
|
||||
- 8080
|
||||
volumes:
|
||||
- backups:/backups
|
||||
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: lifeconsole
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: lifeconsole
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
db:
|
||||
backups:
|
||||
|
After Width: | Height: | Size: 219 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "life-console",
|
||||
"name": "Life Console",
|
||||
"version": "0.1.0",
|
||||
"description": "Life Console is a keyboard-first planner that shows what to do right now: time-block your week from a backlog, count down deadlines and link each task to the goal it serves. Multi-user, works offline and installs on phones.",
|
||||
"logo": "life-console.png",
|
||||
"links": {
|
||||
"github": "https://github.com/TJ-coding/life-console",
|
||||
"website": "https://life-console.dev",
|
||||
"docs": "https://life-console.dev/docs/self-hosting"
|
||||
},
|
||||
"tags": [
|
||||
"productivity",
|
||||
"planner",
|
||||
"time-blocking",
|
||||
"tasks"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
db_password = "${password:32}"
|
||||
|
||||
[config]
|
||||
# APP_URL uses http to match Dokploy's generated domain; change to https:// once HTTPS is on for the domain
|
||||
env = [
|
||||
"DB_PASSWORD=${db_password}",
|
||||
"APP_URL=http://${main_domain}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "app"
|
||||
port = 8080
|
||||
host = "${main_domain}"
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
migrate:
|
||||
image: ghcr.io/mcpspan/core-api:0.13.0
|
||||
image: ghcr.io/mcpspan/core-api:0.14.0
|
||||
restart: "no"
|
||||
depends_on:
|
||||
db:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
command: ["./node_modules/.bin/node-pg-migrate", "up"]
|
||||
|
||||
api:
|
||||
image: ghcr.io/mcpspan/core-api:0.13.0
|
||||
image: ghcr.io/mcpspan/core-api:0.14.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
migrate:
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
start_period: 10s
|
||||
|
||||
dashboard:
|
||||
image: ghcr.io/mcpspan/core-dashboard:0.13.0
|
||||
image: ghcr.io/mcpspan/core-dashboard:0.14.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
api:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mcpspan",
|
||||
"name": "mcpspan",
|
||||
"version": "0.13.0",
|
||||
"version": "0.14.0",
|
||||
"description": "mcpspan is open-source analytics for MCP servers: which tools agents call, how often they fail and why, which clients call them, and how fast they answer. Add one line with an SDK in any of eight languages, or none at all behind its proxy, and see it in the dashboard. Parameter values never leave your server.",
|
||||
"logo": "mcpspan.svg",
|
||||
"links": {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
nakama:
|
||||
image: ghcr.io/ahmadrosid/nakama:0.4.71
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 4310
|
||||
environment:
|
||||
NAKAMA_CONFIG_DIR: /nakama/data
|
||||
# Daily encrypted export, newest 7 kept. Must stay outside the data volume.
|
||||
NAKAMA_BACKUP_DIR: /nakama/backups
|
||||
# Where the Update button leaves requests for the updater service.
|
||||
NAKAMA_UPDATER_DIR: /nakama/updates
|
||||
NAKAMA_SECRETS_KEY: ${NAKAMA_SECRETS_KEY}
|
||||
# Public URL for OAuth callbacks and shared links (Dokploy reverse proxy).
|
||||
NAKAMA_WEB_PUBLIC_URL: https://${NAKAMA_HOST}
|
||||
# Required behind Dokploy's Traefik proxy for correct login rate limiting.
|
||||
NAKAMA_TRUST_PROXY: "true"
|
||||
volumes:
|
||||
- nakama-data:/nakama/data
|
||||
- nakama-backups:/nakama/backups
|
||||
- nakama-updates:/nakama/updates
|
||||
|
||||
# Optional: the Update button in Settings. Turn it on with
|
||||
# COMPOSE_PROFILES=updater in .env. It holds the Docker socket, which is root
|
||||
# on this host, so it opens no port and only upgrades the nakama service.
|
||||
updater:
|
||||
image: docker:29-cli
|
||||
profiles: ["updater"]
|
||||
environment:
|
||||
# The docker:cli image defaults to tcp://docker:2375.
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
- NAKAMA_IMAGE=ghcr.io/ahmadrosid/nakama:${NAKAMA_VERSION:-latest}
|
||||
# The script ships inside the Nakama image, so it updates with Nakama.
|
||||
entrypoint:
|
||||
- sh
|
||||
- -c
|
||||
- docker run --rm --entrypoint cat "$$NAKAMA_IMAGE" /app/scripts/nakama-updater.sh > /tmp/updater.sh && exec sh /tmp/updater.sh
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- .:/compose
|
||||
- nakama-updates:/updates
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
nakama-data:
|
||||
nakama-backups:
|
||||
nakama-updates:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "nakama",
|
||||
"name": "Nakama",
|
||||
"version": "0.4.71",
|
||||
"description": "AI agent platform for teams: a shared workspace where agents get their own tools, skills, memory and channels (Telegram, WhatsApp, Discord, Slack) with orgs, invites and roles built in.",
|
||||
"logo": "nakama.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/ahmadrosid/nakama",
|
||||
"website": "https://getnakama.cloud",
|
||||
"docs": "https://ahmadrosid.github.io/nakama/"
|
||||
},
|
||||
"tags": [
|
||||
"ai",
|
||||
"agents",
|
||||
"automation"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg fill="none" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M24 10.58L8.5 37.42L39.5 37.42Z"
|
||||
stroke="#0A0A0A"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="4.2"
|
||||
/>
|
||||
<circle cx="24" cy="10.58" fill="#0A0A0A" r="7.5" />
|
||||
<circle cx="8.5" cy="37.42" fill="#0A0A0A" r="7.5" />
|
||||
<circle cx="39.5" cy="37.42" fill="#0A0A0A" r="7.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
@@ -0,0 +1,15 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
nakama_secrets_key = "${base64:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"NAKAMA_HOST=${main_domain}",
|
||||
"NAKAMA_SECRETS_KEY=${nakama_secrets_key}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "nakama"
|
||||
port = 4310
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
openbao:
|
||||
image: openbao/openbao:2.7.2
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
BAO_ADDR: "http://127.0.0.1:8200"
|
||||
expose:
|
||||
- "8200"
|
||||
volumes:
|
||||
- ../files/config/openbao.hcl:/openbao/config/openbao.hcl:ro
|
||||
- openbao-data:/openbao/file
|
||||
healthcheck:
|
||||
# exit code 1 = server unreachable; 2 = sealed (still healthy, so the UI stays reachable for init/unseal)
|
||||
test: ["CMD-SHELL", "bao status >/dev/null 2>&1; [ $$? -ne 1 ]"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
|
||||
volumes:
|
||||
openbao-data:
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "openbao",
|
||||
"name": "OpenBao",
|
||||
"version": "2.7.2",
|
||||
"description": "OpenBao is an open-source, community-driven fork of HashiCorp Vault under the Linux Foundation for managing secrets, encryption keys and certificates. This template runs a single node with persistent Raft storage and the web UI. After deploy, open the UI to initialize OpenBao, store the unseal keys and root token safely, then unseal it (unseal again after every restart).",
|
||||
"logo": "openbao.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/openbao/openbao",
|
||||
"website": "https://openbao.org/",
|
||||
"docs": "https://openbao.org/docs/"
|
||||
},
|
||||
"tags": [
|
||||
"security",
|
||||
"secrets",
|
||||
"devops",
|
||||
"infrastructure"
|
||||
],
|
||||
"seoTitle": "OpenBao Docker Compose Setup",
|
||||
"seoDescription": "Open-source secrets manager forked from Vault. Deploy OpenBao with Docker Compose on Dokploy – persistent Raft storage, web UI, manual init and unseal.",
|
||||
"newConfig": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,31 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
|
||||
[config]
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "openbao"
|
||||
port = 8200
|
||||
host = "${main_domain}"
|
||||
|
||||
[[config.mounts]]
|
||||
filePath = "./config/openbao.hcl"
|
||||
content = """
|
||||
ui = true
|
||||
disable_mlock = true
|
||||
|
||||
api_addr = "https://${main_domain}"
|
||||
cluster_addr = "https://openbao:8201"
|
||||
|
||||
storage "raft" {
|
||||
path = "/openbao/file"
|
||||
node_id = "openbao-1"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
cluster_address = "0.0.0.0:8201"
|
||||
# TLS is terminated by Traefik (Dokploy)
|
||||
tls_disable = true
|
||||
}
|
||||
"""
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
openfault:
|
||||
image: ghcr.io/openfault/openfault:0.1.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- OPENFAULT_BASE_URL=https://${OPENFAULT_HOST}
|
||||
- OPENFAULT_TRUST_PROXY=1
|
||||
- OPENFAULT_SETUP_TOKEN=${OPENFAULT_SETUP_TOKEN}
|
||||
volumes:
|
||||
- openfault-data:/data
|
||||
|
||||
volumes:
|
||||
openfault-data: {}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "openfault",
|
||||
"name": "OpenFault",
|
||||
"version": "0.1.0",
|
||||
"description": "Self-hosted, Sentry-compatible error tracker. One container, SQLite inside, built for coding agents.",
|
||||
"logo": "openfault.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/OpenFault/openfault",
|
||||
"website": "https://openfault.dev",
|
||||
"docs": "https://openfault.dev/docs/install"
|
||||
},
|
||||
"tags": ["monitoring"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="#000">
|
||||
<path d="M36.95 6H20A14 14 0 0 0 6 20V44A14 14 0 0 0 20 58H22.91Z"/>
|
||||
<path d="M41.09 6H44A14 14 0 0 1 58 20V44A14 14 0 0 1 44 58H27.05L33.53 34H42.5L44.12 28H35.15L37.04 21H48.04L49.66 15H38.66Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
@@ -0,0 +1,15 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
setup_token = "${password:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"OPENFAULT_HOST=${main_domain}",
|
||||
"OPENFAULT_SETUP_TOKEN=${setup_token}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "openfault"
|
||||
port = 8080
|
||||
host = "${main_domain}"
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
satgas:
|
||||
image: ghcr.io/zeative/satgas:0.1.13
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SATGAS_PUBLIC_URL: ${SATGAS_PUBLIC_URL}
|
||||
SATGAS_TOKEN: ${SATGAS_TOKEN}
|
||||
volumes:
|
||||
- satgas-data:/data
|
||||
- satgas-home:/home/node
|
||||
stop_grace_period: 15s
|
||||
|
||||
volumes:
|
||||
satgas-data: {}
|
||||
satgas-home: {}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "satgas",
|
||||
"name": "Satgas",
|
||||
"version": "0.1.13",
|
||||
"description": "Your own AI team, self-hosted. Describe what you want, and a lead hires the right agents, runs them in parallel on the model you choose, and puts the results on a shared canvas you can watch and edit.",
|
||||
"logo": "satgas.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/zeative/satgas",
|
||||
"website": "https://github.com/zeative/satgas",
|
||||
"docs": "https://github.com/zeative/satgas#self-hosting"
|
||||
},
|
||||
"tags": ["ai", "agents", "developer-tools", "self-hosted"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="14" fill="#141414"/>
|
||||
<g transform="translate(8 8) scale(2)">
|
||||
<path d="M8.5 12c3.5 0 3.5-6 7-6M8.5 12c3.5 0 3.5 6 7 6" fill="none" stroke="#f5f5f5" stroke-width="1.75" stroke-linecap="round"/>
|
||||
<circle cx="6" cy="12" r="2.5" fill="#f5f5f5"/>
|
||||
<circle cx="18" cy="6" r="2.5" fill="#ffb90e"/>
|
||||
<circle cx="18" cy="18" r="2.5" fill="#f5f5f5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 465 B |
@@ -0,0 +1,16 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
sign_in_token = "${password:32}"
|
||||
|
||||
[config]
|
||||
# Must match how the domain is served: Dokploy's generated domain is http, so switch this to https with your own domain.
|
||||
env = [
|
||||
"SATGAS_PUBLIC_URL=http://${main_domain}",
|
||||
"SATGAS_TOKEN=${sign_in_token}",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "satgas"
|
||||
port = 4317
|
||||
host = "${main_domain}"
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
retries: 5
|
||||
|
||||
senddock:
|
||||
image: ghcr.io/arkhe-systems/senddock:0.8.3
|
||||
image: ghcr.io/senddock/senddock:0.8.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Open-source, self-hosted email marketing platform with BYO SMTP. Manage subscribers, templates, broadcasts, and scheduled campaigns from one place.",
|
||||
"logo": "senddock.svg",
|
||||
"links": {
|
||||
"github": "https://github.com/Arkhe-Systems/senddock",
|
||||
"github": "https://github.com/SendDock/senddock",
|
||||
"website": "https://senddock.dev",
|
||||
"docs": "https://docs.senddock.dev"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="112" fill="#000000"/>
|
||||
<g stroke="#ffffff" stroke-width="8" stroke-linejoin="round" stroke-linecap="round" fill="#000000">
|
||||
<rect width="512" height="512" rx="112" fill="#111113"/>
|
||||
<g stroke="#00af7b" stroke-width="14" stroke-linejoin="round" stroke-linecap="round" fill="#111113">
|
||||
<polyline points="106,236 206,176 306,236" fill="none"/>
|
||||
<polygon points="106,236 66,216 166,156 206,176"/>
|
||||
<polygon points="206,176 246,156 346,216 306,236"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 856 B |
@@ -0,0 +1,60 @@
|
||||
services:
|
||||
terarouter:
|
||||
image: ghcr.io/montara-project/tera-router:v1.23.0
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8080
|
||||
environment:
|
||||
ENV: production
|
||||
DEBUG: "false"
|
||||
MACHINE_ID: ${MACHINE_ID}
|
||||
APP_SECRET: ${APP_SECRET}
|
||||
APP_NAME: tera-router-server
|
||||
|
||||
# For Default Super User
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||
|
||||
# No published port: every request arrives via Dokploy's proxy on the
|
||||
# internal network, so private ranges are safe to trust for X-Forwarded-For, this forward Real IP using DNS Cloudflare
|
||||
TRUSTED_PROXIES: ${TRUSTED_PROXIES}
|
||||
CORS_ALLOWED_ORIGINS: https://${TERA_HOST}
|
||||
API_URL: https://${TERA_HOST}
|
||||
|
||||
# For Database
|
||||
DATABASE_PATH: /app/data/terarouter.db
|
||||
MIGRATE_ON_BOOT: "true"
|
||||
SEED_ON_BOOT: "true"
|
||||
|
||||
# Optional for token reduction proxy
|
||||
HEADROOM_URL: http://headroom:8787
|
||||
HEADROOM_PROXY_TOKEN: ${HEADROOM_PROXY_TOKEN}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
volumes:
|
||||
- terarouter-data:/app/data
|
||||
depends_on:
|
||||
- headroom
|
||||
|
||||
# Token-reduction proxy the gateway calls through HEADROOM_URL; switch it on
|
||||
# under Settings > Token Saving. HEADROOM_PROXY_TOKEN is required because the
|
||||
# sidecar binds beyond loopback on the internal network.
|
||||
headroom:
|
||||
image: ghcr.io/headroomlabs-ai/headroom:0.41.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
HEADROOM_HOST: "0.0.0.0"
|
||||
HEADROOM_PORT: "8787"
|
||||
HEADROOM_COMPRESS_ALLOW_REMOTE: "1"
|
||||
HEADROOM_PROXY_TOKEN: ${HEADROOM_PROXY_TOKEN}
|
||||
# Kompress (ModernBERT on CPU) outruns the gateway's 5s Headroom timeout
|
||||
# on large tool results; the structural compressors stay on.
|
||||
HEADROOM_DISABLE_KOMPRESS: "1"
|
||||
HEADROOM_DISABLE_KOMPRESS_FALLBACK: "1"
|
||||
|
||||
volumes:
|
||||
terarouter-data:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "terarouter",
|
||||
"name": "Tera Router",
|
||||
"version": "v1.23.0",
|
||||
"description": "Self-hosted AI router: a single OpenAI/Anthropic-compatible gateway in front of all your providers, with a dashboard for accounts, routing, model catalogs, and cost analytics.",
|
||||
"logo": "tera.png",
|
||||
"links": {
|
||||
"github": "https://github.com/montara-project/tera-router",
|
||||
"website": "https://terarouter.xyz",
|
||||
"docs": "https://docs.terarouter.xyz"
|
||||
},
|
||||
"tags": [
|
||||
"ai",
|
||||
"proxy",
|
||||
"developer-tools"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
machine_id = "${randomPort}"
|
||||
app_secret = "${base64:48}"
|
||||
admin_email = "${email}"
|
||||
admin_password = "${password:32}"
|
||||
headroom_proxy_token = "${base64:32}"
|
||||
|
||||
[config]
|
||||
env = [
|
||||
"TERA_HOST=${main_domain}",
|
||||
"MACHINE_ID=${machine_id}",
|
||||
"APP_SECRET=${app_secret}",
|
||||
"ADMIN_EMAIL=${admin_email}",
|
||||
"ADMIN_PASSWORD=${admin_password}",
|
||||
"TRUSTED_PROXIES=10.0.1.0/24,173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22,2400:cb00::/32,2606:4700::/32,2803:f800::/32,2405:b500::/32,2405:8100::/32,2a06:98c0::/29,2c0f:f248::/32",
|
||||
"SENTRY_DSN=",
|
||||
"HEADROOM_PROXY_TOKEN=${headroom_proxy_token}",
|
||||
"OIDC_ISSUER=",
|
||||
"OIDC_CLIENT_ID=",
|
||||
"OIDC_CLIENT_SECRET=",
|
||||
"OIDC_PROVIDER_NAME=",
|
||||
"OIDC_REDIRECT_URL=",
|
||||
]
|
||||
mounts = []
|
||||
|
||||
[[config.domains]]
|
||||
serviceName = "terarouter"
|
||||
port = 8080
|
||||
host = "${main_domain}"
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
trmnl-larapaper:
|
||||
image: ghcr.io/usetrmnl/larapaper:0.45.0
|
||||
image: ghcr.io/usetrmnl/larapaper:0.46.0
|
||||
environment:
|
||||
- APP_URL=${APP_URL}
|
||||
- PHP_OPCACHE_ENABLE=${PHP_OPCACHE_ENABLE}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "trmnl-larapaper",
|
||||
"name": "TRMNL LaraPaper",
|
||||
"version": "0.45.0",
|
||||
"version": "0.46.0",
|
||||
"description": "LaraPaper is a self-hosted application (BYOS) to manage TRMNL e-ink devices.",
|
||||
"logo": "larapaper.svg",
|
||||
"links": {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
services:
|
||||
zeqqat:
|
||||
image: ghcr.io/zeqqat/zeqqat:${ZEQQAT_VERSION:-0.3.40}
|
||||
image: ghcr.io/zeqqat/zeqqat:${ZEQQAT_VERSION:-latest}
|
||||
# Pull on every deploy, so redeploying picks up a new "latest" instead of
|
||||
# reusing an older image Docker already has.
|
||||
pull_policy: always
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "zeqqat",
|
||||
"name": "Zeqqat",
|
||||
"version": "0.3.40",
|
||||
"version": "latest",
|
||||
"description": "Give AI agents access to your Dokploy without handing them your API key. Per-tool permissions, team roles and a full log of every call.",
|
||||
"logo": "zeqqat.svg",
|
||||
"links": {
|
||||
|
||||