Compare commits

...

6 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
9e0a0b5a89 v1.14.10
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 59s
CI / Build All Platforms (push) Successful in 2m1s
Release / build-and-release (push) Successful in 2m42s
2026-03-16 08:40:48 +00:00
3a227bd838 fix(services): stop auto-update monitoring during shutdown 2026-03-16 08:40:48 +00:00
f5a7fccfc2 v1.14.9
Some checks failed
CI / Type Check & Lint (push) Failing after 25s
Publish to npm / npm-publish (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 57s
CI / Build All Platforms (push) Successful in 2m20s
Release / build-and-release (push) Successful in 3m50s
2026-03-16 08:25:32 +00:00
a30d2029a5 fix(repo): no changes to commit 2026-03-16 08:25:32 +00:00
13 changed files with 286 additions and 161 deletions

View File

@@ -1,5 +1,22 @@
# 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)
stop auto-update monitoring during shutdown
- Track the auto-update polling interval in the services manager
- Clear the auto-update interval when Onebox shuts down to prevent background checks after shutdown
## 2026-03-16 - 1.14.9 - fix(repo)
no changes to commit
## 2026-03-16 - 1.14.8 - fix(repo)
no changes to commit

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.14.8",
"version": "1.15.0",
"exports": "./mod.ts",
"tasks": {
"test": "deno test --allow-all test/",
@@ -15,7 +15,6 @@
"@std/assert": "jsr:@std/assert@^1.0.15",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@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/cloudflare": "npm:@apiclient.xyz/cloudflare@6.4.3",
"@push.rocks/smartacme": "npm:@push.rocks/smartacme@^8.0.0",

View File

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

View File

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

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/onebox',
version: '1.14.8',
version: '1.15.0',
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
*/
import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import { projectInfo } from '../info.ts';
import { getErrorMessage } from '../utils/error.ts';
import type { Onebox } from './onebox.ts';
@@ -18,7 +16,6 @@ const FALLBACK_PID_FILE = `${FALLBACK_PID_DIR}/onebox.pid`;
export class OneboxDaemon {
private oneboxRef: Onebox;
private smartdaemon: plugins.smartdaemon.SmartDaemon | null = null;
private running = false;
private monitoringInterval: 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)
*/
@@ -482,36 +417,7 @@ export class OneboxDaemon {
static async ensureNoDaemon(): Promise<void> {
const running = await OneboxDaemon.isDaemonRunning();
if (running) {
throw new Error('Daemon is already running. Please stop it first with: onebox daemon 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';
throw new Error('Daemon is already running. Please stop it first with: onebox systemd stop');
}
}
}

View File

@@ -14,6 +14,7 @@ import { OneboxReverseProxy } from './reverseproxy.ts';
import { OneboxDnsManager } from './dns.ts';
import { OneboxSslManager } from './ssl.ts';
import { OneboxDaemon } from './daemon.ts';
import { OneboxSystemd } from './systemd.ts';
import { OneboxHttpServer } from './httpserver.ts';
import { CloudflareDomainSync } from './cloudflare-sync.ts';
import { CertRequirementManager } from './cert-requirement-manager.ts';
@@ -33,6 +34,7 @@ export class Onebox {
public dns: OneboxDnsManager;
public ssl: OneboxSslManager;
public daemon: OneboxDaemon;
public systemd: OneboxSystemd;
public httpServer: OneboxHttpServer;
public cloudflareDomainSync: CloudflareDomainSync;
public certRequirementManager: CertRequirementManager;
@@ -57,6 +59,7 @@ export class Onebox {
this.dns = new OneboxDnsManager(this);
this.ssl = new OneboxSslManager(this);
this.daemon = new OneboxDaemon(this);
this.systemd = new OneboxSystemd();
this.httpServer = new OneboxHttpServer(this);
this.registry = new RegistryManager({
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)
*/
@@ -411,6 +400,9 @@ export class Onebox {
try {
logger.info('Shutting down Onebox...');
// Stop auto-update monitoring
this.services.stopAutoUpdateMonitoring();
// Stop backup scheduler
await this.backupScheduler.stop();

View File

@@ -15,6 +15,7 @@ export class OneboxServicesManager {
private oneboxRef: any; // Will be Onebox instance
private database: OneboxDatabase;
private docker: OneboxDockerManager;
private autoUpdateIntervalId: number | null = null;
constructor(oneboxRef: any) {
this.oneboxRef = oneboxRef;
@@ -681,7 +682,7 @@ export class OneboxServicesManager {
*/
startAutoUpdateMonitoring(): void {
// Check every 30 seconds
setInterval(async () => {
this.autoUpdateIntervalId = setInterval(async () => {
try {
await this.checkForRegistryUpdates();
} catch (error) {
@@ -692,6 +693,17 @@ export class OneboxServicesManager {
logger.info('Auto-update monitoring started (30s interval)');
}
/**
* Stop auto-update monitoring
*/
stopAutoUpdateMonitoring(): void {
if (this.autoUpdateIntervalId !== null) {
clearInterval(this.autoUpdateIntervalId);
this.autoUpdateIntervalId = null;
logger.debug('Auto-update monitoring stopped');
}
}
/**
* Check all services using onebox registry for updates
*/

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 { Onebox } from './classes/onebox.ts';
import { OneboxDaemon } from './classes/daemon.ts';
import { OneboxSystemd } from './classes/systemd.ts';
export async function runCli(): Promise<void> {
const args = Deno.args;
@@ -25,6 +26,19 @@ export async function runCli(): Promise<void> {
const subcommand = args[1];
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)
if (command === 'server') {
const onebox = new Onebox();
@@ -60,10 +74,6 @@ export async function runCli(): Promise<void> {
await handleNginxCommand(onebox, subcommand, args.slice(2));
break;
case 'daemon':
await handleDaemonCommand(onebox, subcommand, args.slice(2));
break;
case 'config':
await handleConfigCommand(onebox, subcommand, args.slice(2));
break;
@@ -72,10 +82,6 @@ export async function runCli(): Promise<void> {
await handleStatusCommand(onebox);
break;
case 'upgrade':
await handleUpgradeCommand();
break;
default:
logger.error(`Unknown command: ${command}`);
printHelp();
@@ -282,7 +288,7 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
await OneboxDaemon.ensureNoDaemon();
} catch (error) {
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');
Deno.exit(1);
}
@@ -326,39 +332,49 @@ async function handleServerCommand(onebox: Onebox, args: string[]) {
}
}
// Daemon commands
async function handleDaemonCommand(onebox: Onebox, subcommand: string, _args: string[]) {
// Systemd service commands (lightweight — no Onebox init)
async function handleSystemdCommand(subcommand: string, _args: string[]) {
const systemd = new OneboxSystemd();
switch (subcommand) {
case 'install':
await onebox.daemon.installService();
case 'enable':
await systemd.enable();
break;
case 'disable':
await systemd.disable();
break;
case 'start':
await onebox.startDaemon();
await systemd.start();
break;
case 'stop':
await onebox.stopDaemon();
await systemd.stop();
break;
case 'logs': {
const command = new Deno.Command('journalctl', {
args: ['-u', 'smartdaemon_onebox', '-f'],
stdout: 'inherit',
stderr: 'inherit',
});
await command.output();
case 'status': {
const status = await systemd.getStatus();
logger.info(`Service status: ${status}`);
break;
}
case 'status': {
const status = await onebox.daemon.getServiceStatus();
logger.info(`Daemon status: ${status}`);
case 'logs':
await systemd.showLogs();
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;
}
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 status
daemon install
daemon start
daemon stop
daemon logs
daemon status
systemd enable Install and enable systemd service
systemd disable Stop, disable, and remove systemd service
systemd start Start onebox via systemctl
systemd stop Stop onebox via systemctl
systemd status Show systemd service status
systemd logs Follow service logs (journalctl)
config show
config set <key> <value>
@@ -530,15 +547,15 @@ Development Workflow:
onebox service add ... # In another terminal
Production Workflow:
onebox daemon install # Install systemd service
onebox daemon start # Start daemon
onebox service add ... # CLI uses daemon
onebox systemd enable # Install and enable systemd service
onebox systemd start # Start via systemctl
onebox service add ... # CLI manages services
Examples:
onebox server --ephemeral # Start dev server
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 daemon install
onebox daemon start
onebox systemd enable
onebox systemd start
`);
}

View File

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

View File

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

View File

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