Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de3152ee57 | |||
| de6c0fb781 | |||
| 9f1d05e886 | |||
| 25f326a659 | |||
| 418f3e0bb2 |
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
name: trek
|
||||
version: 3.0.15
|
||||
version: 3.0.17
|
||||
description: Minimal Helm chart for TREK app
|
||||
appVersion: "3.0.15"
|
||||
appVersion: "3.0.17"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trek-client",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trek-client",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"dependencies": {
|
||||
"@react-pdf/renderer": "^4.3.2",
|
||||
"axios": "^1.6.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trek-client",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trek-server",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trek-server",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||
"archiver": "^6.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trek-server",
|
||||
"version": "3.0.15",
|
||||
"version": "3.0.17",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node --import tsx src/index.ts",
|
||||
|
||||
@@ -50,11 +50,11 @@ import { getCollabFeatures } from './services/adminService';
|
||||
import { isAddonEnabled } from './services/adminService';
|
||||
import { ADDON_IDS } from './addons';
|
||||
import { ALL_SCOPES } from './mcp/scopes';
|
||||
import { getAppUrl } from './services/oidcService';
|
||||
import { mcpAuthMetadataRouter } from '@modelcontextprotocol/sdk/server/auth/router';
|
||||
import { authorizationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize';
|
||||
import { clientRegistrationHandler } from '@modelcontextprotocol/sdk/server/auth/handlers/register';
|
||||
import type { OAuthMetadata } from '@modelcontextprotocol/sdk/shared/auth';
|
||||
import { getMcpSafeUrl } from './services/notifications';
|
||||
|
||||
export function createApp(): express.Application {
|
||||
const app = express();
|
||||
@@ -388,7 +388,7 @@ export function createApp(): express.Application {
|
||||
|
||||
function getOAuthMetadata(): OAuthMetadata {
|
||||
if (_oauthMetadata) return _oauthMetadata;
|
||||
const base = (getAppUrl() || 'http://localhost:3001').replace(/\/+$/, '');
|
||||
const base = getMcpSafeUrl().replace(/\/+$/, '');
|
||||
_oauthMetadata = {
|
||||
issuer: base,
|
||||
authorization_endpoint: `${base}/oauth/authorize`,
|
||||
@@ -416,14 +416,11 @@ export function createApp(): express.Application {
|
||||
return _sdkMetaRouter;
|
||||
}
|
||||
|
||||
// Path-aware gate: only /.well-known/* returns 404 when disabled; other paths pass through
|
||||
// so static files and SPA routes are unaffected when MCP is off.
|
||||
// Only invoke the SDK metadata router for /.well-known/* paths.
|
||||
// Calling getMetaRouter() on every request triggers lazy init (new URL(...)) which
|
||||
// throws "Invalid URL" when APP_URL lacks a protocol — breaking all page loads.
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
const isMetadataPath =
|
||||
req.path === '/.well-known/oauth-authorization-server' ||
|
||||
req.path === '/.well-known/openid-configuration' ||
|
||||
req.path.startsWith('/.well-known/oauth-protected-resource');
|
||||
if (isMetadataPath && !isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
|
||||
if (req.path.startsWith('/.well-known/') && !isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();
|
||||
getMetaRouter()(req, res, next);
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const tmpDir = path.join(__dirname, '../data/tmp');
|
||||
const app = createApp();
|
||||
|
||||
import * as scheduler from './scheduler';
|
||||
import { getAppUrl, getMcpSafeUrl } from './services/notifications';
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3001;
|
||||
const HOST = process.env.HOST;
|
||||
@@ -29,22 +30,42 @@ const onListen = () => {
|
||||
const LOG_LVL = (process.env.LOG_LEVEL || 'info').toLowerCase();
|
||||
const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
const origins = process.env.ALLOWED_ORIGINS || '(same-origin)';
|
||||
const appUrl = getAppUrl();
|
||||
const resolvedAppUrl = getMcpSafeUrl();
|
||||
const banner = [
|
||||
'──────────────────────────────────────',
|
||||
' TREK API started',
|
||||
` Version ${APP_VERSION}`,
|
||||
...(HOST ? [` Host: ${HOST}`] : []),
|
||||
` Port: ${PORT}`,
|
||||
` Environment: ${process.env.NODE_ENV?.toLowerCase() || 'development'}`,
|
||||
` Timezone: ${tz}`,
|
||||
` Origins: ${origins}`,
|
||||
` Log level: ${LOG_LVL}`,
|
||||
` Log file: /app/data/logs/trek.log`,
|
||||
` PID: ${process.pid}`,
|
||||
` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`,
|
||||
` Version ${APP_VERSION}`,
|
||||
...(HOST ? [` Host: ${HOST}`] : []),
|
||||
` Container Port: ${PORT}`,
|
||||
` App URL: ${appUrl}`,
|
||||
` Environment: ${process.env.NODE_ENV?.toLowerCase() || 'development'}`,
|
||||
` Timezone: ${tz}`,
|
||||
` Origins: ${origins}`,
|
||||
` Log level: ${LOG_LVL}`,
|
||||
` Log file: /app/data/logs/trek.log`,
|
||||
` PID: ${process.pid}`,
|
||||
` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`,
|
||||
'──────────────────────────────────────',
|
||||
];
|
||||
banner.forEach(l => console.log(l));
|
||||
if (process.env.APP_URL) {
|
||||
let parsedAppUrl: URL | null = null;
|
||||
try { parsedAppUrl = new URL(process.env.APP_URL); } catch { /* invalid */ }
|
||||
|
||||
if (!parsedAppUrl) {
|
||||
sLogWarn(`APP_URL: "${process.env.APP_URL}" is not a valid URL — it will be ignored.`);
|
||||
}
|
||||
|
||||
const mcpSafe = parsedAppUrl !== null && (
|
||||
parsedAppUrl.protocol === 'https:' ||
|
||||
parsedAppUrl.hostname === 'localhost' ||
|
||||
parsedAppUrl.hostname === '127.0.0.1'
|
||||
);
|
||||
if (!mcpSafe) {
|
||||
sLogWarn(`APP_URL: not MCP-safe (requires https:// or http://localhost) — MCP will use ${resolvedAppUrl}.`);
|
||||
}
|
||||
}
|
||||
if (process.env.DEMO_MODE?.toLowerCase() === 'true') sLogInfo('Demo mode: ENABLED');
|
||||
if (process.env.DEMO_MODE?.toLowerCase() === 'true' && process.env.NODE_ENV?.toLowerCase() === 'production') {
|
||||
sLogWarn('SECURITY WARNING: DEMO_MODE is enabled in production!');
|
||||
|
||||
@@ -11,7 +11,7 @@ import { registerResources } from './resources';
|
||||
import { registerTools } from './tools';
|
||||
import { McpSession, sessions, revokeUserSessions, revokeUserSessionsForClient } from './sessionManager';
|
||||
import { writeAudit, getClientIp } from '../services/auditLog';
|
||||
import { getAppUrl } from '../services/oidcService';
|
||||
import { getMcpSafeUrl } from '../services/notifications';
|
||||
|
||||
export { revokeUserSessions, revokeUserSessionsForClient };
|
||||
|
||||
@@ -153,7 +153,7 @@ const sessionSweepInterval = setInterval(() => {
|
||||
sessionSweepInterval.unref();
|
||||
|
||||
function setAuthChallenge(res: Response, error = 'invalid_token'): void {
|
||||
const base = (getAppUrl() || '').replace(/\/+$/, '');
|
||||
const base = (getMcpSafeUrl() || '').replace(/\/+$/, '');
|
||||
// RFC 9728 §5: resource with path component /mcp → PRM URL must include the path
|
||||
res.set('WWW-Authenticate',
|
||||
`Bearer realm="TREK MCP", resource_metadata="${base}/.well-known/oauth-protected-resource/mcp", error="${error}"`);
|
||||
@@ -183,7 +183,7 @@ function verifyToken(authHeader: string | undefined): VerifyTokenResult | null {
|
||||
if (!result) return null;
|
||||
// RFC 8707: audience must always match this resource endpoint.
|
||||
// Pre-audit tokens with audience=null are revoked by the SEC-H6 migration.
|
||||
const expected = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||
const expected = `${(getMcpSafeUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||
if (result.audience !== expected) return null;
|
||||
return { user: result.user, scopes: result.scopes, clientId: result.clientId, isStaticToken: false };
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getUserByAccessToken,
|
||||
} from '../services/oauthService';
|
||||
import { ALL_SCOPES } from './scopes';
|
||||
import { getAppUrl } from '../services/oidcService';
|
||||
import { getMcpSafeUrl } from '../services/notifications';
|
||||
import { writeAudit } from '../services/auditLog';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -125,7 +125,7 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
||||
|
||||
// Redirects browser to the SPA consent page with OAuth params forwarded.
|
||||
async authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void> {
|
||||
const mcpResource = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||
const mcpResource = `${getMcpSafeUrl().replace(/\/+$/, '')}/mcp`;
|
||||
const resource = params.resource ? params.resource.href.replace(/\/+$/, '') : mcpResource;
|
||||
|
||||
if (resource !== mcpResource) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
touchLastLogin,
|
||||
generateToken,
|
||||
frontendUrl,
|
||||
getAppUrl,
|
||||
} from '../services/oidcService';
|
||||
import { getAppUrl } from '../services/notifications';
|
||||
import { resolveAuthToggles } from '../services/authService';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -46,13 +46,42 @@ function getSmtpConfig(): SmtpConfig | null {
|
||||
|
||||
// Exported for use by notificationService
|
||||
export function getAppUrl(): string {
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
if (process.env.APP_URL) {
|
||||
try {
|
||||
const _ = new URL(process.env.APP_URL);
|
||||
return process.env.APP_URL.replace(/\/+$/, '');
|
||||
} catch (_ignored) {
|
||||
}
|
||||
}
|
||||
const origins = process.env.ALLOWED_ORIGINS;
|
||||
if (origins) {
|
||||
const first = origins.split(',')[0]?.trim();
|
||||
if (first) return first.replace(/\/+$/, '');
|
||||
if (first) {
|
||||
try {
|
||||
const _ = new URL(first);
|
||||
return first.replace(/\/+$/, '');
|
||||
} catch (_ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const port = process.env.PORT || '3000';
|
||||
const port = Number(process.env.PORT) || 3001;
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
/** Returns a URL guaranteed to satisfy the MCP SDK's issuer requirements (HTTPS or localhost).
|
||||
* Falls back to http://localhost:{PORT} when APP_URL/ALLOWED_ORIGINS use a non-HTTPS, non-localhost scheme
|
||||
* that would cause checkIssuerUrl to throw "Issuer URL must be HTTPS". */
|
||||
export function getMcpSafeUrl(): string {
|
||||
const candidate = getAppUrl();
|
||||
try {
|
||||
const u = new URL(candidate);
|
||||
if (u.protocol === 'https:' || u.hostname === 'localhost' || u.hostname === '127.0.0.1') {
|
||||
return candidate;
|
||||
}
|
||||
} catch {
|
||||
// candidate was somehow invalid — fall through to localhost
|
||||
}
|
||||
const port = Number(process.env.PORT) || 3001;
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ADDON_IDS } from '../addons';
|
||||
import { User } from '../types';
|
||||
import { writeAudit, logWarn } from './auditLog';
|
||||
import { revokeUserSessionsForClient } from '../mcp/sessionManager';
|
||||
import { getAppUrl } from './oidcService';
|
||||
import { getMcpSafeUrl } from './notifications';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
@@ -587,7 +587,7 @@ export function validateAuthorizeRequest(
|
||||
// bind the token to the MCP endpoint by default — previously this
|
||||
// left `audience = null`, and the audience-bind check on MCP requests
|
||||
// then treated a null audience as "valid for any resource".
|
||||
const mcpResource = `${(getAppUrl() || '').replace(/\/+$/, '')}/mcp`;
|
||||
const mcpResource = `${getMcpSafeUrl().replace(/\/+$/, '')}/mcp`;
|
||||
const resource = params.resource
|
||||
? params.resource.replace(/\/+$/, '')
|
||||
: mcpResource;
|
||||
|
||||
@@ -194,14 +194,6 @@ export function generateToken(user: { id: number }): string {
|
||||
return jwt.sign({ id: user.id }, JWT_SECRET, { expiresIn: '24h', algorithm: 'HS256' });
|
||||
}
|
||||
|
||||
export function getAppUrl(): string | null {
|
||||
return (
|
||||
process.env.APP_URL ||
|
||||
(db.prepare("SELECT value FROM app_settings WHERE key = 'app_url'").get() as { value: string } | undefined)?.value ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Token exchange with OIDC provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -48,7 +48,10 @@ vi.mock('../../src/services/adminService', async (importOriginal) => {
|
||||
return { ...actual, isAddonEnabled: isAddonEnabledMock };
|
||||
});
|
||||
|
||||
vi.mock('../../src/services/oidcService', () => ({ getAppUrl: () => 'https://trek.example.com' }));
|
||||
vi.mock('../../src/services/notifications', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/services/notifications')>();
|
||||
return { ...actual, getMcpSafeUrl: () => 'https://trek.example.com' };
|
||||
});
|
||||
|
||||
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
|
||||
vi.mock('../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn(), sessions: new Map() }));
|
||||
|
||||
@@ -4,63 +4,7 @@ Production-ready setup using Docker Compose with security hardening enabled.
|
||||
|
||||
## Compose File
|
||||
|
||||
Create a `docker-compose.yml` with the following content (taken directly from the repository):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: mauriceboe/trek:latest
|
||||
container_name: trek
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- SETUID
|
||||
- SETGID
|
||||
tmpfs:
|
||||
- /tmp:noexec,nosuid,size=64m
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-} # Recommended. Generate with: openssl rand -hex 32. If unset, falls back to data/.jwt_secret (existing installs) or auto-generates a key (fresh installs).
|
||||
- TZ=${TZ:-UTC} # Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin)
|
||||
- LOG_LEVEL=${LOG_LEVEL:-info} # info = concise user actions; debug = verbose admin-level details
|
||||
# - DEFAULT_LANGUAGE=en # Default language on the login page for users with no saved preference. Browser/OS language is auto-detected first; this is the fallback. Supported: de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar
|
||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-} # Comma-separated origins for CORS and email notification links
|
||||
# - FORCE_HTTPS=true # Optional. Enables HTTPS redirect, HSTS, CSP upgrade-insecure-requests, and secure cookies behind a TLS proxy
|
||||
# - COOKIE_SECURE=false # Escape hatch: force session cookies over plain HTTP even in production. Not recommended.
|
||||
# - TRUST_PROXY=1 # Trusted proxy count for X-Forwarded-For / X-Forwarded-Proto. Required for FORCE_HTTPS to work.
|
||||
# - ALLOW_INTERNAL_NETWORK=false # Set to true if Immich or other services are hosted on your local network (RFC-1918 IPs). Loopback and link-local addresses remain blocked regardless.
|
||||
# - APP_URL=https://trek.example.com # Public base URL — required when OIDC is enabled (must match the redirect URI registered with your IdP); also used as base URL for links in email notifications
|
||||
# - OIDC_ISSUER=https://auth.example.com # OpenID Connect provider URL
|
||||
# - OIDC_CLIENT_ID=trek # OpenID Connect client ID
|
||||
# - OIDC_CLIENT_SECRET=supersecret # OpenID Connect client secret
|
||||
# - OIDC_DISPLAY_NAME=SSO # Label shown on the SSO login button
|
||||
# - OIDC_ONLY=false # Set true to force SSO-only mode: disables password login and registration, overrides Admin > Settings toggles, cannot be changed at runtime
|
||||
# - OIDC_ADMIN_CLAIM=groups # OIDC claim used to identify admin users
|
||||
# - OIDC_ADMIN_VALUE=app-trek-admins # Value of the OIDC claim that grants admin role
|
||||
# - OIDC_SCOPE=openid email profile # Fully overrides the default. Add extra scopes as needed (e.g. add groups if using OIDC_ADMIN_CLAIM)
|
||||
# - OIDC_DISCOVERY_URL= # Override the OIDC discovery endpoint for providers with non-standard paths (e.g. Authentik)
|
||||
# - ADMIN_EMAIL=admin@trek.local # Initial admin e-mail — only used on first boot when no users exist
|
||||
# - ADMIN_PASSWORD=changeme # Initial admin password — only used on first boot when no users exist
|
||||
# - MCP_RATE_LIMIT=300 # Max MCP API requests per user per minute (default: 300)
|
||||
# - MCP_MAX_SESSION_PER_USER=20 # Max concurrent MCP sessions per user (default: 20)
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./uploads:/app/uploads
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
```
|
||||
See https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml
|
||||
|
||||
## Security Hardening Explained
|
||||
|
||||
@@ -81,6 +25,25 @@ The compose file ships with several hardening options enabled by default:
|
||||
| `./data` | `/app/data` | SQLite database, logs, `.jwt_secret`, `.encryption_key` |
|
||||
| `./uploads` | `/app/uploads` | Uploaded files (photos, documents, covers, avatars) |
|
||||
|
||||
### Named Volumes
|
||||
|
||||
The compose file above uses bind mounts (`./data`, `./uploads`). You can switch to Docker named volumes, which are fully managed by Docker and not tied to a specific host path. See the [Docker Compose volumes reference](https://docs.docker.com/reference/compose-file/volumes/) for all options.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
# ... (rest of service config unchanged)
|
||||
volumes:
|
||||
- trek_data:/app/data
|
||||
- trek_uploads:/app/uploads
|
||||
|
||||
volumes:
|
||||
trek_data:
|
||||
trek_uploads:
|
||||
```
|
||||
|
||||
Docker creates the volumes automatically on first `docker compose up`. Use `docker volume ls` and `docker volume inspect` to manage them.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The compose file reads variables from a `.env` file placed alongside `docker-compose.yml`. At minimum, set:
|
||||
@@ -95,6 +58,23 @@ APP_URL=https://trek.example.com
|
||||
|
||||
Uncomment and fill in the OIDC, initial setup, or MCP variables as needed. For a full description of every variable, see [Environment-Variables](Environment-Variables).
|
||||
|
||||
## Image Tags
|
||||
|
||||
Three tag strategies are available:
|
||||
|
||||
| Tag | Example | Behavior |
|
||||
|---|---|---|
|
||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
||||
|
||||
The compose file above uses `latest`. To pin, change the `image:` line:
|
||||
|
||||
```yaml
|
||||
image: mauriceboe/trek:3 # track major version 3
|
||||
image: mauriceboe/trek:3.0.15 # pin to exact release
|
||||
```
|
||||
|
||||
## Start TREK
|
||||
|
||||
```bash
|
||||
|
||||
@@ -34,6 +34,16 @@ Pass additional `-e` flags for timezone and CORS/email link support:
|
||||
|
||||
See [Environment-Variables](Environment-Variables) for the full list.
|
||||
|
||||
## Image Tags
|
||||
|
||||
| Tag | Example | Behavior |
|
||||
|---|---|---|
|
||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
||||
|
||||
Replace `mauriceboe/trek:latest` in the run command with your chosen tag to pin to a major version or exact release.
|
||||
|
||||
## Volume Reference
|
||||
|
||||
| Volume | Container path | What lives there |
|
||||
@@ -43,6 +53,23 @@ See [Environment-Variables](Environment-Variables) for the full list.
|
||||
|
||||
Both volumes must survive container replacement — they are your persistent state. Never remove them before pulling a new image.
|
||||
|
||||
### Named Volumes
|
||||
|
||||
The run command above uses bind mounts (`./data`, `./uploads`). You can use Docker named volumes instead, which are fully managed by Docker and not tied to a host path:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name trek \
|
||||
-p 3000:3000 \
|
||||
-v trek_data:/app/data \
|
||||
-v trek_uploads:/app/uploads \
|
||||
-e ENCRYPTION_KEY=<your-32-byte-hex-key> \
|
||||
--restart unless-stopped \
|
||||
mauriceboe/trek:latest
|
||||
```
|
||||
|
||||
Docker creates `trek_data` and `trek_uploads` automatically on first run. Named volumes are easier to manage with `docker volume` commands and work better in some NAS or container-management environments.
|
||||
|
||||
## Health Check
|
||||
|
||||
The container exposes a health endpoint at:
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Install: Portainer
|
||||
|
||||
Install TREK on Portainer using a Stack (Docker Compose).
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Portainer must be installed and connected to your Docker environment. Use **Stacks** — it supports Docker Compose and gives you the full compose syntax including environment variables, volumes, and restart policies.
|
||||
|
||||
## Create a Stack
|
||||
|
||||

|
||||
|
||||
1. In Portainer, go to **Stacks → Add stack**.
|
||||
2. Give the stack a name (e.g. `trek`).
|
||||
3. Select **Web editor** and paste the compose file from [docker-compose.yml](https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml).
|
||||
|
||||

|
||||
|
||||
4. Fill in the environment variables at the bottom of the page.
|
||||
|
||||

|
||||
|
||||
5. Click **Deploy the stack**.
|
||||
|
||||

|
||||
|
||||
## Compose Content
|
||||
|
||||
See https://github.com/mauriceboe/TREK/blob/main/docker-compose.yml
|
||||
|
||||
Set at minimum `ENCRYPTION_KEY`, `TZ`, and `APP_URL` in the **Environment variables** section of the stack editor. Generate an encryption key with:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
## Image Tags
|
||||
|
||||
Three tag strategies are available:
|
||||
|
||||
| Tag | Example | Behavior |
|
||||
|---|---|---|
|
||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
||||
|
||||
Use `latest` or a major-version tag (e.g. `3`) if you want automatic updates on redeploy. Use a full version tag (e.g. `3.0.15`) if you want explicit control over which release runs.
|
||||
|
||||
## Updating
|
||||
|
||||
How you update depends on the tag you chose:
|
||||
|
||||
**`latest` or major-version tag** — In Portainer, open the stack, click **Redeploy**, enable the **Re-pull image and redeploy** switch, then confirm. Portainer will pull the newest matching image and recreate the container.
|
||||
|
||||

|
||||
|
||||
**Pinned full-version tag** — Edit the stack, change the tag in the `image:` line (e.g. `3.0.15` → `3.0.16`), then click **Update the stack**. No need to toggle the re-pull switch — a tag change forces a fresh pull.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
> Back up your data before any update. Go to **Admin Panel → Backups** or copy your `./data` and `./uploads` directories. See [Backups](Backups).
|
||||
|
||||
## Volumes
|
||||
|
||||
| Stack-relative path | Container path | Contents |
|
||||
|---|---|---|
|
||||
| `./data` | `/app/data` | SQLite database, logs, encryption key |
|
||||
| `./uploads` | `/app/uploads` | Uploaded files (photos, documents, covers, avatars) |
|
||||
|
||||
Portainer resolves `./` relative to the stack's working directory. Confirm the paths under **Stack details** after deploying.
|
||||
|
||||
### Named Volumes
|
||||
|
||||
You can use Docker named volumes instead of bind mounts. Named volumes are fully managed by Docker and not tied to a host path — a good fit for Portainer where the working directory can vary. See the [Docker Compose volumes reference](https://docs.docker.com/reference/compose-file/volumes/) for all options.
|
||||
|
||||
Replace the `volumes:` block in the service and add a top-level declaration:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
# ... (rest of service config unchanged)
|
||||
volumes:
|
||||
- trek_data:/app/data
|
||||
- trek_uploads:/app/uploads
|
||||
|
||||
volumes:
|
||||
trek_data:
|
||||
trek_uploads:
|
||||
```
|
||||
|
||||
Portainer lists named volumes under **Volumes** in the sidebar, where you can inspect or back them up.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Environment-Variables](Environment-Variables) — full variable reference
|
||||
- [Reverse-Proxy](Reverse-Proxy) — HTTPS configuration
|
||||
- [Updating](Updating) — update strategies across all install methods
|
||||
@@ -6,13 +6,33 @@ How to update TREK to a newer version without losing data.
|
||||
|
||||
Back up your data first. Go to Admin Panel → Backups and create a manual backup, or copy your `./data` and `./uploads` directories to a safe location. See [Backups](Backups) for details.
|
||||
|
||||
## Image Tags
|
||||
|
||||
| Tag | Example | Behavior |
|
||||
|---|---|---|
|
||||
| `latest` | `mauriceboe/trek:latest` | Always the newest release across all major versions |
|
||||
| Major version | `mauriceboe/trek:3` | Latest release pinned to that major version |
|
||||
| Full version | `mauriceboe/trek:3.0.15` | Exact release; never changes |
|
||||
|
||||
Use `latest` or a major-version tag if you want updates on each redeploy. Use a full version tag for explicit control — update by changing the tag, not by re-pulling.
|
||||
|
||||
## Docker Compose (Recommended)
|
||||
|
||||
**`latest` or major-version tag:**
|
||||
|
||||
```bash
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
This pulls the latest image and recreates the container with your existing volumes. Your data is untouched.
|
||||
This pulls the newest matching image and recreates the container with your existing volumes. Your data is untouched.
|
||||
|
||||
**Pinned full-version tag:**
|
||||
|
||||
Edit `docker-compose.yml`, update the tag in the `image:` line (e.g. `3.0.15` → `3.0.16`), then redeploy:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Docker Run
|
||||
|
||||
@@ -63,6 +83,22 @@ To verify the update completed and check for errors:
|
||||
journalctl -u trek -n 50
|
||||
```
|
||||
|
||||
## Portainer
|
||||
|
||||
Open the **Stacks** list, click the TREK stack, then click **Redeploy**.
|
||||
|
||||
**`latest` or major-version tag** — enable the **Re-pull image and redeploy** switch before confirming. Portainer pulls the newest matching image and recreates the container.
|
||||
|
||||

|
||||
|
||||
**Pinned full-version tag** (e.g. `3.0.15`) — edit the stack, update the tag in the `image:` line, then click **Update the stack**. No re-pull switch needed; the tag change forces a fresh pull.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
See [Install-Portainer](Install-Portainer) for the full installation walkthrough.
|
||||
|
||||
## Unraid
|
||||
|
||||
In the Unraid Docker tab, click the TREK container and select **Update**. Unraid will pull the latest image and restart with the same volumes.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
- [[Install: Helm|Install-Helm]]
|
||||
- [[Install: Proxmox VE (LXC)|Install-Proxmox]]
|
||||
- [[Install: Unraid|Install-Unraid]]
|
||||
- [[Install: Portainer|Install-Portainer]]
|
||||
- [[Reverse Proxy|Reverse-Proxy]]
|
||||
- [[Environment Variables|Environment-Variables]]
|
||||
- [[Updating]]
|
||||
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 86 KiB |