mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 17:36:42 +00:00
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:
+2
-2
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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
@@ -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,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" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 '{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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 })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user