Skip to content

Services API

Monitor and control the systemd services that make up the Portlama stack, with live log streaming via WebSocket.

In Plain English

Portlama runs several background services on the droplet: nginx (the reverse proxy), Chisel (the tunnel server), Authelia (the login system for tunneled apps), and the panel server itself. The services API lets you check whether each service is running, start or stop them, restart them, and watch their log output in real time.

Think of it as a simplified version of the systemctl and journalctl commands, accessible from your browser.

Authentication

All service endpoints require a valid mTLS client certificate and a completed onboarding. See the API Overview for details.

If onboarding is not complete, all endpoints return 503 Service Unavailable.

In addition to admin certificates, agent certificates with the appropriate capabilities can access these endpoints:

  • services:read — grants access to GET /api/services (list statuses)
  • services:write — grants access to POST /api/services/:name/:action (control services)

Service Whitelist

Only these services can be managed through the API:

Service NameDescription
nginxReverse proxy, TLS termination, mTLS enforcement
chiselWebSocket tunnel server
autheliaTOTP two-factor authentication
portlama-panelThe panel server itself (Fastify)

Requests for any other service name are rejected with a 400 error. This whitelist prevents arbitrary systemd service manipulation.

Safety Rules

  • Cannot stop the panel from the UI. Calling POST /api/services/portlama-panel/stop returns a 400 error because stopping the panel would terminate the API session — you would lose the ability to restart it without SSH access.
  • All actions use sudo. The panel runs as the portlama user. Service control commands use sudo systemctl via sudoers rules that restrict which services and actions are permitted.

Endpoints

GET /api/services

Returns the status and uptime of all managed services. Statuses are queried in parallel from systemd.

Request:

No request body.

bash
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/services | jq

Response (200):

json
{
  "services": [
    {
      "name": "nginx",
      "status": "active",
      "uptime": "5d 3h 20m"
    },
    {
      "name": "chisel",
      "status": "active",
      "uptime": "5d 3h 19m"
    },
    {
      "name": "authelia",
      "status": "active",
      "uptime": "5d 3h 18m"
    },
    {
      "name": "portlama-panel",
      "status": "active",
      "uptime": "2h 45m"
    }
  ]
}
FieldTypeDescription
namestringsystemd service name
statusstringOne of: active, inactive, failed, unknown
uptimestring | nullHuman-readable uptime (e.g., "2d 5h 30m"), or null if not active

The uptime is calculated from the ActiveEnterTimestamp reported by systemd and formatted as a human-readable duration. If the timestamp cannot be read, uptime is null even for active services.


POST /api/services/:name/:action

Executes a systemctl action on a managed service. The command runs with a 30-second timeout.

URL parameters:

ParameterTypeValidationDescription
:namestringMust be in the service whitelistService to control
:actionstringOne of: start, stop, restart, reloadAction to execute
bash
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/services/nginx/restart | jq

Response (200):

json
{
  "ok": true,
  "name": "nginx",
  "action": "restart"
}

Errors:

StatusBodyWhen
400{"error":"Unknown service"}Service name not in whitelist
400{"error":"Invalid action"}Action not one of start/stop/restart/reload
400{"error":"Cannot stop the panel service from the UI — it would terminate this session"}Attempting to stop portlama-panel
400{"error":"Validation failed","details":{"issues":[...]}}Zod validation of params failed
500{"error":"Failed to restart nginx","details":"..."}systemctl command failed

Available Actions

Actionsystemctl CommandNotes
startsudo systemctl start <name>Start a stopped service
stopsudo systemctl stop <name>Stop a running service (blocked for portlama-panel)
restartsudo systemctl restart <name>Stop then start a service
reloadsudo systemctl reload <name>Reload configuration without full restart (supported by nginx and authelia)

WS /api/services/:name/logs

WebSocket endpoint that streams live logs from journalctl for a specific service. The connection tails the last 100 log lines and then follows new output in real time.

URL parameters:

ParameterTypeValidationDescription
:namestringMust be in the service whitelistService to tail logs for

If the service name is not in the whitelist, the WebSocket is closed immediately with code 1008 ("Policy Violation") and the reason "Unknown service".

Connection:

javascript
const ws = new WebSocket('wss://203.0.113.42:9292/api/services/nginx/logs');

ws.onmessage = (event) => {
  const entry = JSON.parse(event.data);
  console.log(`[${entry.timestamp}] ${entry.message}`);
};

ws.onclose = (event) => {
  console.log(`Log stream closed: ${event.reason}`);
};

Message format:

Each message is a JSON object with a timestamp and message:

json
{
  "timestamp": "2026-03-13T14:30:45+0000",
  "message": "hostname nginx[1234]: 203.0.113.42 - - [13/Mar/2026:14:30:45 +0000] \"GET /api/health HTTP/1.1\" 200 32"
}
FieldTypeDescription
timestampstringISO timestamp from journalctl --output=short-iso format
messagestringThe rest of the log line (hostname, process, PID, and message)

If the timestamp cannot be parsed from the log line, timestamp is an empty string and message contains the entire raw line.

Special messages:

json
{
  "timestamp": "2026-03-13T14:30:45.000Z",
  "message": "[Error: Failed to start log stream for nginx]"
}
json
{
  "timestamp": "2026-03-13T14:30:45.000Z",
  "message": "[Log stream ended]"
}

Close codes:

CodeReasonWhen
1000Log stream endedjournalctl process exited normally
1008Unknown serviceService name not in whitelist
1011Failed to spawn journalctlCould not start the journalctl process

Implementation details:

  • Uses journalctl -u <name> -f -n 100 --output=short-iso
  • The -n 100 flag sends the last 100 log lines on connect (backfill)
  • The -f flag follows new output in real time
  • The journalctl process is killed (SIGTERM) when the WebSocket closes
  • All active journalctl processes are cleaned up on server shutdown

Quick Reference

MethodPathDescription
GET/api/servicesList all service statuses and uptimes
POST/api/services/:name/:actionExecute start/stop/restart/reload
WS/api/services/:name/logsStream live logs via WebSocket

Service Status Object Shape

json
{
  "name": "nginx",
  "status": "active",
  "uptime": "5d 3h 20m"
}

Allowed Combinations

Servicestartstoprestartreload
nginxYesYesYesYes
chiselYesYesYesYes
autheliaYesYesYesYes
portlama-panelYesNoYesYes

curl Cheat Sheet

bash
# List service statuses
curl -s --cert client.p12:password \
  https://203.0.113.42:9292/api/services | jq

# Restart nginx
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/services/nginx/restart | jq

# Reload Authelia (after manual config changes)
curl -s --cert client.p12:password \
  -X POST \
  https://203.0.113.42:9292/api/services/authelia/reload | jq

# Stream nginx logs (requires wscat or similar)
wscat -c wss://203.0.113.42:9292/api/services/nginx/logs \
  --cert client.pem --key client-key.pem

Released under the PolyForm Noncommercial License 1.0.0