feat: change default listen port from 3000 to 1852 (#756)

Updates the backend listen port, Vite dev proxy target, Docker EXPOSE,
compose port mapping, .env.example default, GitHub Actions smoke-test
default, healthcheck URLs, and every doc/example reference. Test fixtures
that include example URLs were updated for consistency, though their
assertions are port-agnostic.

The rate-limit value of 3000 in middleware/rateLimiters.ts and the
3000 entry in WEB_UI_PORTS (which detects user containers like Grafana)
are intentionally untouched.
This commit is contained in:
Anso
2026-04-24 22:23:31 -04:00
committed by GitHub
parent d6b744e8e6
commit ed553f1f19
27 changed files with 72 additions and 72 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ JWT_SECRET=your-secure-jwt-secret-here
# Directory containing docker-compose files # Directory containing docker-compose files
COMPOSE_DIR=/path/to/your/compose/files COMPOSE_DIR=/path/to/your/compose/files
# HTTP server port (default: 3000) # HTTP server port (default: 1852)
PORT=3000 PORT=1852
# Database and state directory inside the container (default: /app/data) # Database and state directory inside the container (default: /app/data)
DATA_DIR=/app/data DATA_DIR=/app/data
+1 -1
View File
@@ -13,7 +13,7 @@ inputs:
default: '/tmp/compose' default: '/tmp/compose'
port: port:
required: false required: false
default: '3000' default: '1852'
skip-backend-build: skip-backend-build:
description: > description: >
When 'true', skip running `npm run build` in backend/ and assume the When 'true', skip running `npm run build` in backend/ and assume the
+3 -3
View File
@@ -126,12 +126,12 @@ jobs:
# enough for `docker logs` in the trap to surface the stack trace. # enough for `docker logs` in the trap to surface the stack trace.
# The trap force-removes it explicitly whether it is still running # The trap force-removes it explicitly whether it is still running
# or already exited. # or already exited.
docker run -d --name sencho-smoke -p 3000:3000 localhost/sencho:release-scan docker run -d --name sencho-smoke -p 1852:1852 localhost/sencho:release-scan
trap 'docker logs sencho-smoke 2>&1 || true; docker rm -f sencho-smoke >/dev/null 2>&1 || true' EXIT trap 'docker logs sencho-smoke 2>&1 || true; docker rm -f sencho-smoke >/dev/null 2>&1 || true' EXIT
for i in $(seq 1 30); do for i in $(seq 1 30); do
if curl -fsS http://localhost:3000/api/health >/dev/null 2>&1; then if curl -fsS http://localhost:1852/api/health >/dev/null 2>&1; then
echo "Container healthy after ${i}s" echo "Container healthy after ${i}s"
curl -s http://localhost:3000/api/health curl -s http://localhost:1852/api/health
exit 0 exit 0
fi fi
sleep 1 sleep 1
+1 -1
View File
@@ -14,7 +14,7 @@ Thank you for your interest in contributing to Sencho!
``` ```
5. Start the dev servers: 5. Start the dev servers:
```bash ```bash
cd backend && npm run dev # Express + nodemon on :3000 cd backend && npm run dev # Express + nodemon on :1852
cd frontend && npm run dev # Vite on :5173 cd frontend && npm run dev # Vite on :5173
``` ```
+2 -2
View File
@@ -184,11 +184,11 @@ RUN sed -i 's/\r//' /usr/local/bin/docker-entrypoint.sh \
&& chmod +x /usr/local/bin/docker-entrypoint.sh && chmod +x /usr/local/bin/docker-entrypoint.sh
# Expose port # Expose port
EXPOSE 3000 EXPOSE 1852
# Health check - polls the public /api/health endpoint every 30s # Health check - polls the public /api/health endpoint every 30s
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD node -e "const h=require('http');h.get('http://localhost:3000/api/health',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))" CMD node -e "const h=require('http');h.get('http://localhost:1852/api/health',r=>{process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"
# Entrypoint ensures /app/data is writable and execs the CMD as root by default, # Entrypoint ensures /app/data is writable and execs the CMD as root by default,
# or drops to $SENCHO_USER via su-exec when that env var is set (see comment above). # or drops to $SENCHO_USER via su-exec when that env var is set (see comment above).
+3 -3
View File
@@ -28,7 +28,7 @@ services:
container_name: sencho container_name: sencho
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "1852:1852"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data - ./data:/app/data
@@ -43,7 +43,7 @@ services:
docker compose up -d docker compose up -d
``` ```
Then open `http://your-server:3000` and create your admin account. Then open `http://your-server:1852` and create your admin account.
See the [full documentation](https://docs.sencho.io) for configuration details, multi-node setup, and more. See the [full documentation](https://docs.sencho.io) for configuration details, multi-node setup, and more.
@@ -57,7 +57,7 @@ cd backend && npm install && npm run dev
cd frontend && npm install && npm run dev cd frontend && npm install && npm run dev
``` ```
The frontend dev server proxies `/api` requests to the backend on port 3000. The frontend dev server proxies `/api` requests to the backend on port 1852.
## Contributing ## Contributing
+1 -1
View File
@@ -63,7 +63,7 @@ describe('POST /api/nodes - api_url SSRF validation (C2 fix)', () => {
.send({ .send({
name: 'lan-node', name: 'lan-node',
type: 'remote', type: 'remote',
api_url: 'http://192.168.1.50:3000', api_url: 'http://192.168.1.50:1852',
api_token: 'sometoken', api_token: 'sometoken',
}); });
// Should succeed (201 or 200) - not a validation error // Should succeed (201 or 200) - not a validation error
@@ -1185,7 +1185,7 @@ describe('SchedulerService - executeUpdateRemote', () => {
it('proxies update execution to remote node', async () => { it('proxies update execution to remote node', async () => {
mockGetNode.mockReturnValue({ id: 2, name: 'remote', type: 'remote', status: 'online' }); mockGetNode.mockReturnValue({ id: 2, name: 'remote', type: 'remote', status: 'online' });
mockGetProxyTarget.mockReturnValue({ mockGetProxyTarget.mockReturnValue({
apiUrl: 'http://remote:3000', apiUrl: 'http://remote:1852',
apiToken: 'test-token', apiToken: 'test-token',
}); });
@@ -1211,7 +1211,7 @@ describe('SchedulerService - executeUpdateRemote', () => {
await svc.triggerTask(88); await svc.triggerTask(88);
expect(mockFetch).toHaveBeenCalledWith( expect(mockFetch).toHaveBeenCalledWith(
'http://remote:3000/api/auto-update/execute', 'http://remote:1852/api/auto-update/execute',
expect.objectContaining({ expect.objectContaining({
method: 'POST', method: 'POST',
body: JSON.stringify({ target: 'web-app' }), body: JSON.stringify({ target: 'web-app' }),
@@ -1226,7 +1226,7 @@ describe('SchedulerService - executeUpdateRemote', () => {
it('records failure when remote node returns error', async () => { it('records failure when remote node returns error', async () => {
mockGetNode.mockReturnValue({ id: 2, name: 'remote', type: 'remote', status: 'online' }); mockGetNode.mockReturnValue({ id: 2, name: 'remote', type: 'remote', status: 'online' });
mockGetProxyTarget.mockReturnValue({ mockGetProxyTarget.mockReturnValue({
apiUrl: 'http://remote:3000', apiUrl: 'http://remote:1852',
apiToken: 'test-token', apiToken: 'test-token',
}); });
@@ -210,11 +210,11 @@ describe('TemplateService', () => {
it('handles values with special characters', () => { it('handles values with special characters', () => {
const result = service.generateEnvString({ const result = service.generateEnvString({
PASSWORD: 'p@ss=word!', PASSWORD: 'p@ss=word!',
URL: 'http://localhost:3000', URL: 'http://localhost:1852',
}); });
expect(result).toContain('PASSWORD=p@ss=word!'); expect(result).toContain('PASSWORD=p@ss=word!');
expect(result).toContain('URL=http://localhost:3000'); expect(result).toContain('URL=http://localhost:1852');
}); });
}); });
+1 -1
View File
@@ -568,7 +568,7 @@ describe('Orphaned role assignment cleanup', () => {
it('deleting a node removes its role assignments', async () => { it('deleting a node removes its role assignments', async () => {
const db = DatabaseService.getInstance(); const db = DatabaseService.getInstance();
// Create a test node // Create a test node
const nodeId = db.addNode({ name: 'test-cleanup-node', type: 'remote', api_url: 'http://test:3000', api_token: '', compose_dir: '/tmp', is_default: false }); const nodeId = db.addNode({ name: 'test-cleanup-node', type: 'remote', api_url: 'http://test:1852', api_token: '', compose_dir: '/tmp', is_default: false });
// Create a role assignment for this node // Create a role assignment for this node
const hash = await bcrypt.hash('password123', 1); const hash = await bcrypt.hash('password123', 1);
const userId = db.addUser({ username: 'nodeorphan', password_hash: hash, role: 'viewer' }); const userId = db.addUser({ username: 'nodeorphan', password_hash: hash, role: 'viewer' });
+8 -8
View File
@@ -63,7 +63,7 @@ describe('isValidStackName', () => {
describe('isValidRemoteUrl', () => { describe('isValidRemoteUrl', () => {
it('accepts valid http URLs', () => { it('accepts valid http URLs', () => {
const result = isValidRemoteUrl('http://192.168.1.10:3000'); const result = isValidRemoteUrl('http://192.168.1.10:1852');
expect(result.valid).toBe(true); expect(result.valid).toBe(true);
}); });
@@ -84,25 +84,25 @@ describe('isValidRemoteUrl', () => {
}); });
it('rejects localhost', () => { it('rejects localhost', () => {
expect(isValidRemoteUrl('http://localhost:3000').valid).toBe(false); expect(isValidRemoteUrl('http://localhost:1852').valid).toBe(false);
expect(isValidRemoteUrl('http://LOCALHOST:3000').valid).toBe(false); expect(isValidRemoteUrl('http://LOCALHOST:1852').valid).toBe(false);
}); });
it('rejects loopback IPs', () => { it('rejects loopback IPs', () => {
expect(isValidRemoteUrl('http://127.0.0.1:3000').valid).toBe(false); expect(isValidRemoteUrl('http://127.0.0.1:1852').valid).toBe(false);
expect(isValidRemoteUrl('http://127.1.2.3').valid).toBe(false); expect(isValidRemoteUrl('http://127.1.2.3').valid).toBe(false);
// Node.js URL.hostname preserves brackets: new URL('http://[::1]').hostname === '[::1]' // Node.js URL.hostname preserves brackets: new URL('http://[::1]').hostname === '[::1]'
expect(isValidRemoteUrl('http://[::1]:3000').valid).toBe(false); expect(isValidRemoteUrl('http://[::1]:1852').valid).toBe(false);
}); });
it('rejects 0.0.0.0', () => { it('rejects 0.0.0.0', () => {
expect(isValidRemoteUrl('http://0.0.0.0:3000').valid).toBe(false); expect(isValidRemoteUrl('http://0.0.0.0:1852').valid).toBe(false);
}); });
it('allows LAN/private IPs (users need these for local network nodes)', () => { it('allows LAN/private IPs (users need these for local network nodes)', () => {
// Users legitimately run Sencho nodes on their LAN // Users legitimately run Sencho nodes on their LAN
expect(isValidRemoteUrl('http://192.168.1.100:3000').valid).toBe(true); expect(isValidRemoteUrl('http://192.168.1.100:1852').valid).toBe(true);
expect(isValidRemoteUrl('http://10.0.0.5:3000').valid).toBe(true); expect(isValidRemoteUrl('http://10.0.0.5:1852').valid).toBe(true);
}); });
}); });
+1 -1
View File
@@ -3,7 +3,7 @@
// monolith. // monolith.
// Server // Server
export const PORT = 3000; export const PORT = 1852;
// Password policy // Password policy
export const MIN_PASSWORD_LENGTH = 8; export const MIN_PASSWORD_LENGTH = 8;
+1 -1
View File
@@ -37,7 +37,7 @@ function mintPilotEnrollment(nodeId: number, req: Request): { token: string; exp
const forwardedProto = req.headers['x-forwarded-proto']; const forwardedProto = req.headers['x-forwarded-proto'];
const protoHeader = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto; const protoHeader = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
const protocol = protoHeader || req.protocol || 'http'; const protocol = protoHeader || req.protocol || 'http';
const host = req.get('host') || 'localhost:3000'; const host = req.get('host') || 'localhost:1852';
const primaryUrl = `${protocol}://${host}`; const primaryUrl = `${protocol}://${host}`;
const dockerRun = const dockerRun =
+1 -1
View File
@@ -22,7 +22,7 @@ export function isValidRemoteUrl(
console.warn('[Validation] URL parse failure:', (e as Error).message, '— input:', raw); console.warn('[Validation] URL parse failure:', (e as Error).message, '— input:', raw);
return { return {
valid: false, valid: false,
reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:3000)', reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:1852)',
}; };
} }
if (!['http:', 'https:'].includes(url.protocol)) { if (!['http:', 'https:'].includes(url.protocol)) {
+1 -1
View File
@@ -5,7 +5,7 @@ services:
container_name: sencho container_name: sencho
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "1852:1852"
volumes: volumes:
# Required: Docker Socket for container orchestration # Required: Docker Socket for container orchestration
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
+8 -8
View File
@@ -8,7 +8,7 @@ Sencho exposes a REST API for automating stack deployments, managing webhooks, m
## Base URL ## Base URL
``` ```
https://your-sencho-instance:3000/api https://your-sencho-instance:1852/api
``` ```
Replace with your actual Sencho host and port. All API paths are prefixed with `/api`. Replace with your actual Sencho host and port. All API paths are prefixed with `/api`.
@@ -19,7 +19,7 @@ Authenticated endpoints require a **Bearer token** in the `Authorization` header
```bash ```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \ curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://your-sencho-instance:3000/api/stacks https://your-sencho-instance:1852/api/stacks
``` ```
<Note> <Note>
@@ -51,7 +51,7 @@ If omitted, the request targets the default node.
# Target node ID 2 # Target node ID 2
curl -H "Authorization: Bearer TOKEN" \ curl -H "Authorization: Bearer TOKEN" \
-H "x-node-id: 2" \ -H "x-node-id: 2" \
https://your-sencho-instance:3000/api/stacks https://your-sencho-instance:1852/api/stacks
``` ```
## Error format ## Error format
@@ -132,7 +132,7 @@ Sencho also provides real-time streaming via WebSocket connections. These are no
Stream live logs from a stack's containers. Stream live logs from a stack's containers.
**URL:** `wss://your-sencho-instance:3000/api/stacks/{stackName}/logs?nodeId={nodeId}` **URL:** `wss://your-sencho-instance:1852/api/stacks/{stackName}/logs?nodeId={nodeId}`
**Authentication:** Pass the token as a cookie (`sencho_token`) or Bearer token. For WebSocket connections, authentication is verified during the upgrade handshake. **Authentication:** Pass the token as a cookie (`sencho_token`) or Bearer token. For WebSocket connections, authentication is verified during the upgrade handshake.
@@ -142,7 +142,7 @@ Stream live logs from a stack's containers.
import WebSocket from "ws"; import WebSocket from "ws";
const ws = new WebSocket( const ws = new WebSocket(
"wss://your-sencho-instance:3000/api/stacks/my-app/logs", "wss://your-sencho-instance:1852/api/stacks/my-app/logs",
{ headers: { Cookie: "sencho_token=YOUR_JWT" } } { headers: { Cookie: "sencho_token=YOUR_JWT" } }
); );
@@ -156,7 +156,7 @@ curl -i -N \
-H "Connection: Upgrade" \ -H "Connection: Upgrade" \
-H "Upgrade: websocket" \ -H "Upgrade: websocket" \
-H "Cookie: sencho_token=YOUR_JWT" \ -H "Cookie: sencho_token=YOUR_JWT" \
https://your-sencho-instance:3000/api/stacks/my-app/logs https://your-sencho-instance:1852/api/stacks/my-app/logs
``` ```
</CodeGroup> </CodeGroup>
@@ -165,7 +165,7 @@ curl -i -N \
Open an interactive shell session inside a running container. Open an interactive shell session inside a running container.
**URL:** `wss://your-sencho-instance:3000/ws` **URL:** `wss://your-sencho-instance:1852/ws`
**Authentication:** Cookie-based JWT only. API tokens with `read-only` or `deploy-only` scope are blocked. **Authentication:** Cookie-based JWT only. API tokens with `read-only` or `deploy-only` scope are blocked.
@@ -174,7 +174,7 @@ Open an interactive shell session inside a running container.
```javascript Node.js ```javascript Node.js
import WebSocket from "ws"; import WebSocket from "ws";
const ws = new WebSocket("wss://your-sencho-instance:3000/ws", { const ws = new WebSocket("wss://your-sencho-instance:1852/ws", {
headers: { Cookie: "sencho_token=YOUR_JWT" }, headers: { Cookie: "sencho_token=YOUR_JWT" },
}); });
+9 -9
View File
@@ -21,7 +21,7 @@ Writes are admin-only and rejected on replica nodes (policies are managed on the
```bash ```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \ curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://your-sencho-instance:3000/api/security/policies https://your-sencho-instance:1852/api/security/policies
``` ```
**Response:** **Response:**
@@ -60,7 +60,7 @@ curl -H "Authorization: Bearer YOUR_API_TOKEN" \
| `node_id` | number or `null` | no | Scope the policy to one node. `null` applies the policy fleet-wide. | | `node_id` | number or `null` | no | Scope the policy to one node. `null` applies the policy fleet-wide. |
```bash ```bash
curl -X POST https://your-sencho-instance:3000/api/security/policies \ curl -X POST https://your-sencho-instance:1852/api/security/policies \
-H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -89,7 +89,7 @@ curl -X POST https://your-sencho-instance:3000/api/security/policies \
Any of the create fields can be updated individually. Omitted fields are left unchanged. Any of the create fields can be updated individually. Omitted fields are left unchanged.
```bash ```bash
curl -X PUT https://your-sencho-instance:3000/api/security/policies/1 \ curl -X PUT https://your-sencho-instance:1852/api/security/policies/1 \
-H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ "block_on_deploy": 0 }' -d '{ "block_on_deploy": 0 }'
@@ -106,7 +106,7 @@ curl -X PUT https://your-sencho-instance:3000/api/security/policies/1 \
**License:** Skipper or Admiral · **Role:** Admin **License:** Skipper or Admiral · **Role:** Admin
```bash ```bash
curl -X DELETE https://your-sencho-instance:3000/api/security/policies/1 \ curl -X DELETE https://your-sencho-instance:1852/api/security/policies/1 \
-H "Authorization: Bearer YOUR_API_TOKEN" -H "Authorization: Bearer YOUR_API_TOKEN"
``` ```
@@ -156,7 +156,7 @@ Response rows include an `active` boolean computed from the `expires_at` timesta
| `expires_at` | number or `null` | no | Unix timestamp in milliseconds. `null` for an indefinite suppression. | | `expires_at` | number or `null` | no | Unix timestamp in milliseconds. `null` for an indefinite suppression. |
```bash ```bash
curl -X POST https://your-sencho-instance:3000/api/security/suppressions \ curl -X POST https://your-sencho-instance:1852/api/security/suppressions \
-H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
@@ -179,7 +179,7 @@ curl -X POST https://your-sencho-instance:3000/api/security/suppressions \
**License:** Skipper or Admiral · **Role:** Admin **License:** Skipper or Admiral · **Role:** Admin
```bash ```bash
curl -X DELETE https://your-sencho-instance:3000/api/security/suppressions/3 \ curl -X DELETE https://your-sencho-instance:1852/api/security/suppressions/3 \
-H "Authorization: Bearer YOUR_API_TOKEN" -H "Authorization: Bearer YOUR_API_TOKEN"
``` ```
@@ -201,7 +201,7 @@ Accepts an image reference and starts an asynchronous scan. The response returns
| `scanners` | `["vuln"]` or `["vuln","secret"]` | no | Omit for vuln-only. `secret` requires Skipper or Admiral. | | `scanners` | `["vuln"]` or `["vuln","secret"]` | no | Omit for vuln-only. `secret` requires Skipper or Admiral. |
```bash ```bash
curl -X POST https://your-sencho-instance:3000/api/security/scan \ curl -X POST https://your-sencho-instance:1852/api/security/scan \
-H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ "imageRef": "nginx:1.27" }' -d '{ "imageRef": "nginx:1.27" }'
@@ -221,7 +221,7 @@ Supports filters: `imageRef`, `imageRefLike`, `status`, `limit`, `offset`. Respo
```bash ```bash
curl -H "Authorization: Bearer YOUR_API_TOKEN" \ curl -H "Authorization: Bearer YOUR_API_TOKEN" \
"https://your-sencho-instance:3000/api/security/scans?imageRefLike=nginx&limit=20" "https://your-sencho-instance:1852/api/security/scans?imageRefLike=nginx&limit=20"
``` ```
## Stack deploy with policy ## Stack deploy with policy
@@ -266,7 +266,7 @@ HTTP 409 Conflict. The response body is parseable JSON so CI pipelines can branc
Admins can bypass the gate on a per-deploy basis by passing `?ignorePolicy=true` on the deploy URL. The flag is only honored when the calling session resolves to an admin user; API tokens with read-only or deploy-only scope cannot bypass a policy. Admins can bypass the gate on a per-deploy basis by passing `?ignorePolicy=true` on the deploy URL. The flag is only honored when the calling session resolves to an admin user; API tokens with read-only or deploy-only scope cannot bypass a policy.
```bash ```bash
curl -X POST "https://your-sencho-instance:3000/api/stacks/myapp/deploy?ignorePolicy=true" \ curl -X POST "https://your-sencho-instance:1852/api/stacks/myapp/deploy?ignorePolicy=true" \
-H "Authorization: Bearer ADMIN_API_TOKEN" -H "Authorization: Bearer ADMIN_API_TOKEN"
``` ```
+6 -6
View File
@@ -39,7 +39,7 @@ On your **primary** Sencho instance, open **Settings → Nodes** and click **+ A
|-------|-------------| |-------|-------------|
| **Name** | A display name (e.g. `Production VPS`, `media-box`) | | **Name** | A display name (e.g. `Production VPS`, `media-box`) |
| **Type** | Select **Remote** (or **Local** for an additional local Docker socket) | | **Type** | Select **Remote** (or **Local** for an additional local Docker socket) |
| **Sencho API URL** | The full HTTP/HTTPS URL of the remote instance (e.g. `http://192.168.1.50:3000`) | | **Sencho API URL** | The full HTTP/HTTPS URL of the remote instance (e.g. `http://192.168.1.50:1852`) |
| **API Token** | The token you generated in Step 1 | | **API Token** | The token you generated in Step 1 |
| **Compose Directory** | The root directory where compose stack folders live on the remote node (defaults to `/app/compose`) | | **Compose Directory** | The root directory where compose stack folders live on the remote node (defaults to `/app/compose`) |
@@ -156,8 +156,8 @@ There are three recommended approaches depending on your deployment:
If all your Sencho instances are on the same local network, VPC, or subnet, HTTP is perfectly fine. The token never leaves the private network, so there is no interception risk. If all your Sencho instances are on the same local network, VPC, or subnet, HTTP is perfectly fine. The token never leaves the private network, so there is no interception risk.
``` ```
http://192.168.1.50:3000 ← safe on a private LAN http://192.168.1.50:1852 ← safe on a private LAN
http://10.0.1.20:3000 ← safe inside a VPC http://10.0.1.20:1852 ← safe inside a VPC
``` ```
#### VPN tunnel (WireGuard, Tailscale) #### VPN tunnel (WireGuard, Tailscale)
@@ -169,7 +169,7 @@ http://10.0.1.20:3000 ← safe inside a VPC
With a mesh VPN like [Tailscale](https://tailscale.com) or [WireGuard](https://www.wireguard.com/), each server gets a private IP on the VPN. Use those IPs as your Sencho API URLs: With a mesh VPN like [Tailscale](https://tailscale.com) or [WireGuard](https://www.wireguard.com/), each server gets a private IP on the VPN. Use those IPs as your Sencho API URLs:
``` ```
http://100.64.0.2:3000 ← Tailscale IP, encrypted by the VPN tunnel http://100.64.0.2:1852 ← Tailscale IP, encrypted by the VPN tunnel
``` ```
All traffic between nodes is encrypted by the VPN. Sencho does not need to do anything additional. All traffic between nodes is encrypted by the VPN. Sencho does not need to do anything additional.
@@ -181,7 +181,7 @@ If you prefer TLS termination at each node, place a reverse proxy in front of ea
<CodeGroup> <CodeGroup>
```text Caddyfile ```text Caddyfile
sencho.example.com { sencho.example.com {
reverse_proxy localhost:3000 reverse_proxy localhost:1852
} }
``` ```
@@ -194,7 +194,7 @@ server {
ssl_certificate_key /etc/letsencrypt/live/sencho.example.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/sencho.example.com/privkey.pem;
location / { location / {
proxy_pass http://localhost:3000; proxy_pass http://localhost:1852;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1; proxy_http_version 1.1;
+1 -1
View File
@@ -186,7 +186,7 @@ Only one policy is evaluated per deploy; use a single tight pattern rather than
Policy CRUD endpoints are documented in the [Security API reference](/api-reference/security). A typical create call from CI looks like this: Policy CRUD endpoints are documented in the [Security API reference](/api-reference/security). A typical create call from CI looks like this:
```bash ```bash
curl -X POST https://your-sencho-instance:3000/api/security/policies \ curl -X POST https://your-sencho-instance:1852/api/security/policies \
-H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
+7 -7
View File
@@ -23,7 +23,7 @@ When you point `COMPOSE_DIR` at a directory, Sencho expects each stack to live i
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `PORT` | `3000` | Port the Sencho HTTP server listens on. | | `PORT` | `1852` | Port the Sencho HTTP server listens on. |
| `DATA_DIR` | `/app/data` | Directory where Sencho stores its SQLite database, node registry, and cached metrics. | | `DATA_DIR` | `/app/data` | Directory where Sencho stores its SQLite database, node registry, and cached metrics. |
| `FRONTEND_URL` | *(empty)* | Frontend origin for CORS. Only needed if the UI is served from a different domain than the API. Leave empty for same-origin setups. | | `FRONTEND_URL` | *(empty)* | Frontend origin for CORS. Only needed if the UI is served from a different domain than the API. Leave empty for same-origin setups. |
| `NODE_ENV` | `production` | Set automatically in the Docker image. Only change this for local development. | | `NODE_ENV` | `production` | Set automatically in the Docker image. Only change this for local development. |
@@ -140,7 +140,7 @@ services:
image: saelix/sencho:latest image: saelix/sencho:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "1852:1852"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./sencho-data:/app/data - ./sencho-data:/app/data
@@ -180,7 +180,7 @@ server {
server_name sencho.yourdomain.com; server_name sencho.yourdomain.com;
location / { location / {
proxy_pass http://localhost:3000; proxy_pass http://localhost:1852;
proxy_http_version 1.1; proxy_http_version 1.1;
# WebSocket support # WebSocket support
@@ -211,7 +211,7 @@ server {
ssl_certificate_key /etc/letsencrypt/live/sencho.yourdomain.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/sencho.yourdomain.com/privkey.pem;
location / { location / {
proxy_pass http://localhost:3000; proxy_pass http://localhost:1852;
proxy_http_version 1.1; proxy_http_version 1.1;
# WebSocket support # WebSocket support
@@ -235,7 +235,7 @@ Use [Certbot](https://certbot.eff.org/) to obtain and auto-renew certificates: `
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)" - "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.services.sencho.loadbalancer.server.port=3000" - "traefik.http.services.sencho.loadbalancer.server.port=1852"
``` ```
### Traefik with SSL (Let's Encrypt) ### Traefik with SSL (Let's Encrypt)
@@ -246,7 +246,7 @@ labels:
- "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)" - "traefik.http.routers.sencho.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.routers.sencho.entrypoints=websecure" - "traefik.http.routers.sencho.entrypoints=websecure"
- "traefik.http.routers.sencho.tls.certresolver=letsencrypt" - "traefik.http.routers.sencho.tls.certresolver=letsencrypt"
- "traefik.http.services.sencho.loadbalancer.server.port=3000" - "traefik.http.services.sencho.loadbalancer.server.port=1852"
# HTTP to HTTPS redirect # HTTP to HTTPS redirect
- "traefik.http.routers.sencho-http.rule=Host(`sencho.yourdomain.com`)" - "traefik.http.routers.sencho-http.rule=Host(`sencho.yourdomain.com`)"
- "traefik.http.routers.sencho-http.entrypoints=web" - "traefik.http.routers.sencho-http.entrypoints=web"
@@ -262,7 +262,7 @@ labels:
``` ```
sencho.yourdomain.com { sencho.yourdomain.com {
reverse_proxy localhost:3000 reverse_proxy localhost:1852
} }
``` ```
+2 -2
View File
@@ -13,7 +13,7 @@ description: Get Sencho running in under five minutes.
```bash ```bash
docker run -d \ docker run -d \
--name sencho \ --name sencho \
-p 3000:3000 \ -p 1852:1852 \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v /opt/compose:/opt/compose \ -v /opt/compose:/opt/compose \
-v sencho_data:/app/data \ -v sencho_data:/app/data \
@@ -21,7 +21,7 @@ docker run -d \
saelix/sencho:latest saelix/sencho:latest
``` ```
Open `http://localhost:3000` in your browser. On first boot you'll be prompted to create an admin account. Open `http://localhost:1852` in your browser. On first boot you'll be prompted to create an admin account.
<Note> <Note>
Replace `/opt/compose` with the path to your Compose projects directory. Every subdirectory inside it becomes a stack in Sencho. A `JWT_SECRET` is generated automatically on first boot; you do not need to provide one. Replace `/opt/compose` with the path to your Compose projects directory. Every subdirectory inside it becomes a stack in Sencho. A `JWT_SECRET` is generated automatically on first boot; you do not need to provide one.
+1 -1
View File
@@ -162,7 +162,7 @@ services:
image: saelix/sencho:latest image: saelix/sencho:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" - "1852:1852"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./sencho-data:/app/data - ./sencho-data:/app/data
+2 -2
View File
@@ -44,7 +44,7 @@ servers:
host: host:
default: localhost default: localhost
port: port:
default: "3000" default: "1852"
security: security:
- bearerAuth: [] - bearerAuth: []
@@ -1679,7 +1679,7 @@ paths:
api_url: api_url:
type: string type: string
description: Remote Sencho instance URL (required for remote nodes). description: Remote Sencho instance URL (required for remote nodes).
example: https://sencho.example.com:3000 example: https://sencho.example.com:1852
api_token: api_token:
type: string type: string
description: API token for authenticating with the remote Sencho instance. description: API token for authenticating with the remote Sencho instance.
+1 -1
View File
@@ -70,7 +70,7 @@ Sencho itself is lightweight. The majority of resource usage on your host comes
## Networking ## Networking
- **Listen port:** 3000 (fixed). Map it to any host port using Docker's `-p` flag or `ports` in your compose file - **Listen port:** 1852 (fixed). Map it to any host port using Docker's `-p` flag or `ports` in your compose file
- **Inbound:** Only the listen port needs to be reachable (directly or through a reverse proxy) - **Inbound:** Only the listen port needs to be reachable (directly or through a reverse proxy)
- **Outbound:** No outbound connections are required for local-only setups. If you use multi-node management, Sencho needs HTTP/HTTPS access to remote Sencho instances on their configured API URLs - **Outbound:** No outbound connections are required for local-only setups. If you use multi-node management, Sencho needs HTTP/HTTPS access to remote Sencho instances on their configured API URLs
- **Health check:** `GET /api/health` returns `200` when the application is ready. The Docker image includes a built-in `HEALTHCHECK` that polls this endpoint every 30 seconds - **Health check:** `GET /api/health` returns `200` when the application is ready. The Docker image includes a built-in `HEALTHCHECK` that polls this endpoint every 30 seconds
+1 -1
View File
@@ -557,7 +557,7 @@ docker compose pull && docker compose up -d
Sencho exposes a health endpoint for monitoring and container health checks: Sencho exposes a health endpoint for monitoring and container health checks:
```bash ```bash
curl http://localhost:3000/api/health curl http://localhost:1852/api/health
# {"status":"ok","uptime":12345.67} # {"status":"ok","uptime":12345.67}
``` ```
+1 -1
View File
@@ -368,7 +368,7 @@ export function NodeManager() {
<Label htmlFor="node-api-url">Sencho API URL</Label> <Label htmlFor="node-api-url">Sencho API URL</Label>
<Input <Input
id="node-api-url" id="node-api-url"
placeholder="http://192.168.1.50:3000" placeholder="http://192.168.1.50:1852"
value={formData.api_url} value={formData.api_url}
onChange={(e) => setFormData({ ...formData, api_url: e.target.value })} onChange={(e) => setFormData({ ...formData, api_url: e.target.value })}
/> />
+2 -2
View File
@@ -27,12 +27,12 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:1852',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
}, },
'/ws': { '/ws': {
target: 'http://localhost:3000', target: 'http://localhost:1852',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
}, },