Compare commits

...

2 Commits

Author SHA1 Message Date
69e23f667e v1.15.0
Some checks failed
CI / Build All Platforms (push) Failing after 7s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 26s
CI / Build Test (Current Platform) (push) Successful in 58s
Release / build-and-release (push) Successful in 2m58s
2026-03-16 10:02:59 +00:00
a2bf4df7c2 feat(systemd): replace smartdaemon-based service management with native systemd commands 2026-03-16 10:02:59 +00:00
12 changed files with 260 additions and 160 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-03-16 - 1.15.0 - feat(systemd)
replace smartdaemon-based service management with native systemd commands
- adds a dedicated OneboxSystemd manager for enabling, disabling, starting, stopping, checking status, and following logs
- introduces a new `onebox systemd` CLI command set and updates install and help output to use it
- removes the smartdaemon dependency and related service management code
## 2026-03-16 - 1.14.10 - fix(services) ## 2026-03-16 - 1.14.10 - fix(services)
stop auto-update monitoring during shutdown stop auto-update monitoring during shutdown

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.14.10", "version": "1.15.0",
"exports": "./mod.ts", "exports": "./mod.ts",
"tasks": { "tasks": {
"test": "deno test --allow-all test/", "test": "deno test --allow-all test/",
@@ -15,7 +15,6 @@
"@std/assert": "jsr:@std/assert@^1.0.15", "@std/assert": "jsr:@std/assert@^1.0.15",
"@std/encoding": "jsr:@std/encoding@^1.0.10", "@std/encoding": "jsr:@std/encoding@^1.0.10",
"@db/sqlite": "jsr:@db/sqlite@0.12.0", "@db/sqlite": "jsr:@db/sqlite@0.12.0",
"@push.rocks/smartdaemon": "npm:@push.rocks/smartdaemon@^2.1.0",
"@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.1", "@apiclient.xyz/docker": "npm:@apiclient.xyz/docker@^5.1.1",
"@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3", "@apiclient.xyz/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0", "@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",

View File

@@ -23,7 +23,7 @@ SPECIFIED_VERSION=""
INSTALL_DIR="/opt/onebox" INSTALL_DIR="/opt/onebox"
GITEA_BASE_URL="https://code.foss.global" GITEA_BASE_URL="https://code.foss.global"
GITEA_REPO="serve.zone/onebox" GITEA_REPO="serve.zone/onebox"
SERVICE_NAME="smartdaemon_onebox" SERVICE_NAME="onebox"
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -276,7 +276,7 @@ if [ -f "/var/lib/onebox/onebox.db" ]; then
if [ $SERVICE_WAS_RUNNING -eq 1 ]; then if [ $SERVICE_WAS_RUNNING -eq 1 ]; then
echo "The service has been restarted with your current settings." echo "The service has been restarted with your current settings."
else else
echo "Start the service with: onebox daemon start" echo "Start the service with: onebox systemd start"
fi fi
else else
echo "Get started:" echo "Get started:"
@@ -293,11 +293,11 @@ else
echo " 2. Configure ACME email:" echo " 2. Configure ACME email:"
echo " onebox config set acmeEmail <your@email.com>" echo " onebox config set acmeEmail <your@email.com>"
echo "" echo ""
echo " 3. Install daemon:" echo " 3. Enable systemd service:"
echo " onebox daemon install" echo " onebox systemd enable"
echo "" echo ""
echo " 4. Start daemon:" echo " 4. Start service:"
echo " onebox daemon start" echo " onebox systemd start"
echo "" echo ""
echo " 5. Deploy your first service:" echo " 5. Deploy your first service:"
echo " onebox service add myapp --image nginx:latest --domain app.example.com" echo " onebox service add myapp --image nginx:latest --domain app.example.com"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.14.10", "version": "1.15.0",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers", "description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts", "main": "mod.ts",
"type": "module", "type": "module",

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/onebox', name: '@serve.zone/onebox',
version: '1.14.10', version: '1.15.0',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
} }

View File

@@ -4,9 +4,7 @@
* Handles background monitoring, metrics collection, and automatic tasks * Handles background monitoring, metrics collection, and automatic tasks
*/ */
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts'; import { logger } from '../logging.ts';
import { projectInfo } from '../info.ts';
import { getErrorMessage } from '../utils/error.ts'; import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts'; import type { Onebox } from './onebox.ts';
@@ -18,7 +16,6 @@ const FALLBACK_PID_FILE = `${FALLBACK_PID_DIR}/onebox.pid`;
export class OneboxDaemon { export class OneboxDaemon {
private oneboxRef: Onebox; private oneboxRef: Onebox;
private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null;
private running = false; private running = false;
private monitoringInterval: number | null = null; private monitoringInterval: number | null = null;
private statsInterval: number | null = null; private statsInterval: number | null = null;
@@ -46,68 +43,6 @@ export class OneboxDaemon {
} }
} }
/**
* Install systemd service
*/
async installService(): Promise<void> {
try {
logger.info('Installing Onebox daemon service...');
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
// Get installation directory
const execPath = Deno.execPath();
const service = await this.smartdaemon.addService({
name: 'onebox',
version: projectInfo.version,
command: `${execPath} run --allow-all ${Deno.cwd()}/mod.ts daemon start`,
description: 'Onebox - Self-hosted container platform',
workingDir: Deno.cwd(),
});
await service.save();
await service.enable();
logger.success('Onebox daemon service installed');
logger.info('Start with: sudo systemctl start smartdaemon_onebox');
} catch (error) {
logger.error(`Failed to install daemon service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Uninstall systemd service
*/
async uninstallService(): Promise<void> {
try {
logger.info('Uninstalling Onebox daemon service...');
// Initialize smartdaemon if needed
if (!this.smartdaemon) {
this.smartdaemon = new plugins.smartdaemon.SmartDaemon();
}
const services = await this.smartdaemon.systemdManager.getServices();
const service = services.find(s => s.name === 'onebox');
if (service) {
await service.stop();
await service.disable();
await service.delete();
}
logger.success('Onebox daemon service uninstalled');
} catch (error) {
logger.error(`Failed to uninstall daemon service: ${getErrorMessage(error)}`);
throw error;
}
}
/** /**
* Start daemon mode (background monitoring) * Start daemon mode (background monitoring)
*/ */
@@ -482,36 +417,7 @@ export class OneboxDaemon {
static async ensureNoDaemon(): Promise<void> { static async ensureNoDaemon(): Promise<void> {
const running = await OneboxDaemon.isDaemonRunning(); const running = await OneboxDaemon.isDaemonRunning();
if (running) { if (running) {
throw new Error('Daemon is already running. Please stop it first with: onebox daemon stop'); throw new Error('Daemon is already running. Please stop it first with: onebox systemd stop');
}
}
/**
* Get service status from systemd
*/
async getServiceStatus(): Promise<string> {
try {
// Don't need smartdaemon to check status, just use systemctl directly
const command = new Deno.Command('systemctl', {
args: ['status', 'smartdaemon_onebox'],
stdout: 'piped',
stderr: 'piped',
});
const { code, stdout } = await command.output();
const output = new TextDecoder().decode(stdout);
if (code === 0 || output.includes('active (running)')) {
return 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
return 'stopped';
} else if (output.includes('failed')) {
return 'failed';
} else {
return 'unknown';
}
} catch (error) {
return 'not-installed';
} }
} }
} }

View File

@@ -14,6 +14,7 @@ import { OneboxReverseProxy } from './reverseproxy.ts';
import { OneboxDnsManager } from './dns.ts'; import { OneboxDnsManager } from './dns.ts';
import { OneboxSslManager } from './ssl.ts'; import { OneboxSslManager } from './ssl.ts';
import { OneboxDaemon } from './daemon.ts'; import { OneboxDaemon } from './daemon.ts';
import { OneboxSystemd } from './systemd.ts';
import { OneboxHttpServer } from './httpserver.ts'; import { OneboxHttpServer } from './httpserver.ts';
import { CloudflareDomainSync } from './cloudflare-sync.ts'; import { CloudflareDomainSync } from './cloudflare-sync.ts';
import { CertRequirementManager } from './cert-requirement-manager.ts'; import { CertRequirementManager } from './cert-requirement-manager.ts';
@@ -33,6 +34,7 @@ export class Onebox {
public dns: OneboxDnsManager; public dns: OneboxDnsManager;
public ssl: OneboxSslManager; public ssl: OneboxSslManager;
public daemon: OneboxDaemon; public daemon: OneboxDaemon;
public systemd: OneboxSystemd;
public httpServer: OneboxHttpServer; public httpServer: OneboxHttpServer;
public cloudflareDomainSync: CloudflareDomainSync; public cloudflareDomainSync: CloudflareDomainSync;
public certRequirementManager: CertRequirementManager; public certRequirementManager: CertRequirementManager;
@@ -57,6 +59,7 @@ export class Onebox {
this.dns = new OneboxDnsManager(this); this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this); this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this); this.daemon = new OneboxDaemon(this);
this.systemd = new OneboxSystemd();
this.httpServer = new OneboxHttpServer(this); this.httpServer = new OneboxHttpServer(this);
this.registry = new RegistryManager({ this.registry = new RegistryManager({
dataDir: './.nogit/registry-data', dataDir: './.nogit/registry-data',
@@ -376,20 +379,6 @@ export class Onebox {
} }
} }
/**
* Start daemon mode
*/
async startDaemon(): Promise<void> {
await this.daemon.start();
}
/**
* Stop daemon mode
*/
async stopDaemon(): Promise<void> {
await this.daemon.stop();
}
/** /**
* Start OpsServer (TypedRequest-based, serves new UI) * Start OpsServer (TypedRequest-based, serves new UI)
*/ */

185
ts/classes/systemd.ts Normal file
View File

@@ -0,0 +1,185 @@
/**
* Systemd Service Manager for Onebox
*
* Handles systemd unit file installation, enabling, starting, stopping,
* and status checking. Modeled on nupst's direct systemctl approach —
* no external library dependencies.
*/
import { logger } from '../logging.ts';
import { getErrorMessage } from '../utils/error.ts';
const SERVICE_NAME = 'onebox';
const SERVICE_FILE_PATH = '/etc/systemd/system/onebox.service';
const SERVICE_UNIT_TEMPLATE = `[Unit]
Description=Onebox - Self-hosted container platform
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/local/bin/onebox systemd start-daemon
Restart=always
RestartSec=10
WorkingDirectory=/var/lib/onebox
Environment=PATH=/usr/bin:/usr/local/bin
[Install]
WantedBy=multi-user.target
`;
export class OneboxSystemd {
/**
* Install and enable the systemd service
*/
async enable(): Promise<void> {
try {
// Write the unit file
logger.info('Writing systemd unit file...');
await Deno.writeTextFile(SERVICE_FILE_PATH, SERVICE_UNIT_TEMPLATE);
logger.info(`Unit file written to ${SERVICE_FILE_PATH}`);
// Reload systemd daemon
await this.runSystemctl(['daemon-reload']);
// Enable the service
const result = await this.runSystemctl(['enable', `${SERVICE_NAME}.service`]);
if (!result.success) {
throw new Error(`Failed to enable service: ${result.stderr}`);
}
logger.success('Onebox systemd service enabled');
logger.info('Start with: onebox systemd start');
} catch (error) {
logger.error(`Failed to enable service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Stop, disable, and remove the systemd service
*/
async disable(): Promise<void> {
try {
// Stop the service (ignore errors if not running)
await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]);
// Disable the service
await this.runSystemctl(['disable', `${SERVICE_NAME}.service`]);
// Remove the unit file
try {
await Deno.remove(SERVICE_FILE_PATH);
logger.info(`Removed ${SERVICE_FILE_PATH}`);
} catch {
// File might not exist
}
// Reload systemd daemon
await this.runSystemctl(['daemon-reload']);
logger.success('Onebox systemd service disabled and removed');
} catch (error) {
logger.error(`Failed to disable service: ${getErrorMessage(error)}`);
throw error;
}
}
/**
* Start the service via systemctl
*/
async start(): Promise<void> {
const result = await this.runSystemctl(['start', `${SERVICE_NAME}.service`]);
if (!result.success) {
logger.error(`Failed to start service: ${result.stderr}`);
throw new Error(`Failed to start onebox service`);
}
logger.success('Onebox service started');
}
/**
* Stop the service via systemctl
*/
async stop(): Promise<void> {
const result = await this.runSystemctl(['stop', `${SERVICE_NAME}.service`]);
if (!result.success) {
logger.error(`Failed to stop service: ${result.stderr}`);
throw new Error(`Failed to stop onebox service`);
}
logger.success('Onebox service stopped');
}
/**
* Get and display service status
*/
async getStatus(): Promise<string> {
const result = await this.runSystemctl(['status', `${SERVICE_NAME}.service`]);
const output = result.stdout;
let status: string;
if (output.includes('active (running)')) {
status = 'running';
} else if (output.includes('inactive') || output.includes('dead')) {
status = 'stopped';
} else if (output.includes('failed')) {
status = 'failed';
} else if (!result.success && result.stderr.includes('could not be found')) {
status = 'not-installed';
} else {
status = 'unknown';
}
// Print the raw systemctl output for full details
if (output.trim()) {
console.log(output);
}
return status;
}
/**
* Show service logs via journalctl
*/
async showLogs(): Promise<void> {
const cmd = new Deno.Command('journalctl', {
args: ['-u', `${SERVICE_NAME}.service`, '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await cmd.output();
}
/**
* Check if the service unit file is installed
*/
async isInstalled(): Promise<boolean> {
try {
await Deno.stat(SERVICE_FILE_PATH);
return true;
} catch {
return false;
}
}
/**
* Run a systemctl command and return results
*/
private async runSystemctl(
args: string[]
): Promise<{ success: boolean; stdout: string; stderr: string }> {
const cmd = new Deno.Command('systemctl', {
args,
stdout: 'piped',
stderr: 'piped',
});
const result = await cmd.output();
return {
success: result.success,
stdout: new TextDecoder().decode(result.stdout),
stderr: new TextDecoder().decode(result.stderr),
};
}
}

View File

@@ -7,6 +7,7 @@ import { projectInfo } from './info.ts';
import { getErrorMessage } from './utils/error.ts'; import { getErrorMessage } from './utils/error.ts';
import { Onebox } from './classes/onebox.ts'; import { Onebox } from './classes/onebox.ts';
import { OneboxDaemon } from './classes/daemon.ts'; import { OneboxDaemon } from './classes/daemon.ts';
import { OneboxSystemd } from './classes/systemd.ts';
export async function runCli(): Promise<void> { export async function runCli(): Promise<void> {
const args = Deno.args; const args = Deno.args;
@@ -25,6 +26,19 @@ export async function runCli(): Promise<void> {
const subcommand = args[1]; const subcommand = args[1];
try { try {
// === LIGHTWEIGHT COMMANDS (no init()) ===
if (command === 'systemd') {
await handleSystemdCommand(subcommand, args.slice(2));
return;
}
if (command === 'upgrade') {
await handleUpgradeCommand();
return;
}
// === HEAVY COMMANDS (require full init()) ===
// Server command has special handling (doesn't shut down) // Server command has special handling (doesn't shut down)
if (command === 'server') { if (command === 'server') {
const onebox = new Onebox(); const onebox = new Onebox();
@@ -60,10 +74,6 @@ export async function runCli(): Promise<void> {
await handleNginxCommand(onebox, subcommand, args.slice(2)); await handleNginxCommand(onebox, subcommand, args.slice(2));
break; break;
case 'daemon':
await handleDaemonCommand(onebox, subcommand, args.slice(2));
break;
case 'config': case 'config':
await handleConfigCommand(onebox, subcommand, args.slice(2)); await handleConfigCommand(onebox, subcommand, args.slice(2));
break; break;
@@ -72,10 +82,6 @@ export async function runCli(): Promise<void> {
await handleStatusCommand(onebox); await handleStatusCommand(onebox);
break; break;
case 'upgrade':
await handleUpgradeCommand();
break;
default: default:
logger.error(`Unknown command: ${command}`); logger.error(`Unknown command: ${command}`);
printHelp(); printHelp();
@@ -282,7 +288,7 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
await OneboxDaemon.ensureNoDaemon(); await OneboxDaemon.ensureNoDaemon();
} catch (error) { } catch (error) {
logger.error('Cannot start in ephemeral mode: Daemon is already running'); logger.error('Cannot start in ephemeral mode: Daemon is already running');
logger.info('Stop the daemon first: onebox daemon stop'); logger.info('Stop the daemon first: onebox systemd stop');
logger.info('Or run without --ephemeral to use the existing daemon'); logger.info('Or run without --ephemeral to use the existing daemon');
Deno.exit(1); Deno.exit(1);
} }
@@ -326,39 +332,49 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
} }
} }
// Daemon commands // Systemd service commands (lightweight — no Onebox init)
async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) { async function handleSystemdCommand(subcommand: string, _args: string[]) {
const systemd = new OneboxSystemd();
switch (subcommand) { switch (subcommand) {
case 'install': case 'enable':
await onebox.daemon.installService(); await systemd.enable();
break;
case 'disable':
await systemd.disable();
break; break;
case 'start': case 'start':
await onebox.startDaemon(); await systemd.start();
break; break;
case 'stop': case 'stop':
await onebox.stopDaemon(); await systemd.stop();
break; break;
case 'logs': { case 'status': {
const command = new Deno.Command('journalctl', { const status = await systemd.getStatus();
args: ['-u', 'smartdaemon_onebox', '-f'], logger.info(`Service status: ${status}`);
stdout: 'inherit',
stderr: 'inherit',
});
await command.output();
break; break;
} }
case 'status': { case 'logs':
const status = await onebox.daemon.getServiceStatus(); await systemd.showLogs();
logger.info(`Daemon status: ${status}`); break;
case 'start-daemon': {
// This is what systemd's ExecStart calls — full init + daemon loop
const onebox = new Onebox();
await onebox.init();
await onebox.daemon.start();
// start() blocks (keepAlive loop) until SIGTERM/SIGINT
break; break;
} }
default: default:
logger.error(`Unknown daemon subcommand: ${subcommand}`); logger.error(`Unknown systemd subcommand: ${subcommand}`);
logger.info('Available: enable, disable, start, stop, status, logs');
} }
} }
@@ -506,11 +522,12 @@ Commands:
nginx test nginx test
nginx status nginx status
daemon install systemd enable Install and enable systemd service
daemon start systemd disable Stop, disable, and remove systemd service
daemon stop systemd start Start onebox via systemctl
daemon logs systemd stop Stop onebox via systemctl
daemon status systemd status Show systemd service status
systemd logs Follow service logs (journalctl)
config show config show
config set <key> <value> config set <key> <value>
@@ -530,15 +547,15 @@ Development Workflow:
onebox service add ... # In another terminal onebox service add ... # In another terminal
Production Workflow: Production Workflow:
onebox daemon install # Install systemd service onebox systemd enable # Install and enable systemd service
onebox daemon start # Start daemon onebox systemd start # Start via systemctl
onebox service add ... # CLI uses daemon onebox service add ... # CLI manages services
Examples: Examples:
onebox server --ephemeral # Start dev server onebox server --ephemeral # Start dev server
onebox service add myapp --image nginx:latest --domain app.example.com --port 80 onebox service add myapp --image nginx:latest --domain app.example.com --port 80
onebox registry add --url registry.example.com --username user --password pass onebox registry add --url registry.example.com --username user --password pass
onebox daemon install onebox systemd enable
onebox daemon start onebox systemd start
`); `);
} }

View File

@@ -12,6 +12,7 @@ export { OneboxReverseProxy } from './classes/reverseproxy.ts';
export { OneboxDnsManager } from './classes/dns.ts'; export { OneboxDnsManager } from './classes/dns.ts';
export { OneboxSslManager } from './classes/ssl.ts'; export { OneboxSslManager } from './classes/ssl.ts';
export { OneboxDaemon } from './classes/daemon.ts'; export { OneboxDaemon } from './classes/daemon.ts';
export { OneboxSystemd } from './classes/systemd.ts';
export { OneboxHttpServer } from './classes/httpserver.ts'; export { OneboxHttpServer } from './classes/httpserver.ts';
export { OneboxApiClient } from './classes/apiclient.ts'; export { OneboxApiClient } from './classes/apiclient.ts';

View File

@@ -17,10 +17,6 @@ export { path, fs, http, encoding };
import { Database } from '@db/sqlite'; import { Database } from '@db/sqlite';
export const sqlite = { DB: Database }; export const sqlite = { DB: Database };
// Systemd Daemon Integration
import * as smartdaemon from '@push.rocks/smartdaemon';
export { smartdaemon };
// Docker API Client // Docker API Client
import { DockerHost } from '@apiclient.xyz/docker'; import { DockerHost } from '@apiclient.xyz/docker';
export const docker = { Docker: DockerHost }; export const docker = { Docker: DockerHost };

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/onebox', name: '@serve.zone/onebox',
version: '1.14.10', version: '1.15.0',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
} }