Appearance
Writing a Service
This guide walks through creating a minimal service package from scratch. We'll build a simple Redis service.
1. Create the package directory
services/redis/
service.yaml
scripts/
install.js
configure.js2. Define service.yaml
yaml
name: redis
nodes:
server:
count: 1
ports:
- port: 6379
from: private
actions:
install: scripts/install.js
configure: scripts/configure.jsThis declares a single-node service with one port open to your private networks (use from: public for a port that should serve the internet).
3. Write the install script
scripts/install.js runs once on first setup:
javascript
const managed = require("managed");
// Install Redis
managed.packages.install(["redis-server"], { sudo: true });
// Ensure it starts on boot
managed.systemd.enable("redis-server");
managed.log.info("Redis installed");4. Write the configure script
scripts/configure.js runs after install (and on subsequent applies when config may have changed):
javascript
const managed = require("managed");
const maxmemory = managed.params.maxmemory || "256mb";
const config = `
bind 0.0.0.0
protected-mode no
maxmemory ${maxmemory}
maxmemory-policy allkeys-lru
`;
managed.files.write("/etc/redis/redis.conf", config, { sudo: true });
managed.systemd.restart("redis-server");
managed.log.info("Redis configured with maxmemory=" + maxmemory);The maxmemory value comes from the user's managed.yaml:
yaml
services:
cache:
package: redis
config:
maxmemory: 1gbFields under config are available via managed.params.
5. Deploy
bash
managed applyThe platform creates a server, runs install.js, then configure.js, and sets up firewall rules for port 6379.
Config form widgets
The dashboard builds the "add a service" form straight from your params:. The widget is inferred from the param's type and name (a *_password field masks and offers a generate button, a boolean becomes a toggle, an enum becomes a dropdown, and so on), so most packages need no extra metadata. Optional fields, label, description, group, advanced, placeholder, unit, secret, generate, multiline, only shape how it looks; they never affect a deploy.
Pickers
A widget: override turns a free-text field into a selector that autocompletes against the project's live data, so users pick a real name instead of typing one:
yaml
params:
# Pick one service already configured in managed.yaml.
backends:
type: string
required: true
widget: service-picker
description: "Service to load balance"
# Pick several, the value is then a list of service names.
upstreams:
type: string
widget: service-picker
multiple: true
# Optional: only offer services of these packages.
picker_filter: [app, worker]
# Pick a server from the project's pool (single or multiple).
pinned_servers:
type: string
widget: server-picker
multiple: truewidget: service-picker, choose from the services inservices:. Narrow the suggestions withpicker_filter: [<package>, …](omit for all services).widget: server-picker, choose from the servers in the project's pool.multiple: true, multi-select; the saved value becomes a YAML list. Read it in a script as an array (managed.params.upstreams.forEach(…)). Without it the value stays a single string.multiplealso works on anenumparam, turning it into a multi-select of the allowed values.
Both pickers still accept a typed value that isn't in the list, so a name you'll add later isn't blocked.
Adding backups
To add scheduled backups, extend service.yaml:
yaml
nodes:
server:
count: 1
ports:
- port: 6379
from: private
actions:
install: scripts/install.js
configure: scripts/configure.js
schedule:
backup:
script: scripts/backup.js
every: 6hAnd write scripts/backup.js:
javascript
const managed = require("managed");
const params = managed.params;
// Trigger RDB save
managed.runCommand("redis-cli BGSAVE", { sudo: true });
managed.sleep(5000);
// Upload the dump. Key it under backups/<server>/ — the namespace the
// managed-backup retention library and the built-in packages' restore and
// backup-list scripts all share.
managed.storage.upload(
"backups/" + params._serverName + "/" + managed.sh.timestamp() + ".rdb",
"file:/var/lib/redis/dump.rdb"
);
managed.log.info("Redis backup uploaded");That's all a schedule needs — there is no extra mechanism to hook into. At deploy time managed ships the package (and its deps libraries) to the node's server and installs a cron entry that runs the schedule's script on the box itself, through the same managed binary the agent uses. A cron run and a manual managed backup execute the exact same script with the same params, so storage keys and retention can't drift between the two. Script output lands in /var/log/managed-<service>-<schedule>.log on the server.
The platform provisions object storage automatically when it sees a schedule with backup scripts.
Adding replicas
For a service with replicas, add a second node role:
yaml
nodes:
primary:
count: 1
ports:
- port: 6379
from: service
actions:
install: scripts/install_primary.js
configure: scripts/configure_primary.js
replica:
count_from: replicas
actions:
install: scripts/install_replica.js
configure: scripts/configure_replica.jsIn the replica's configure script, use managed.state.getServer() to discover the primary's IP and set up replication.
Lifecycle options
A few optional fields on nodes/actions control how a service is placed, updated, and removed. They all degrade gracefully, omit them and nothing changes.
Run on every server (scope: fleet)
For a host agent (metrics, log shipper) that should run on every box, give its node scope: fleet instead of a count. Managed installs it on each server, provisions none of its own, and never takes ownership of the boxes, new servers pick it up automatically on the next apply.
yaml
nodes:
agent:
scope: fleet
ports:
- { port: 9100, protocol: tcp, from: private }
actions:
install: scripts/install.js
configure: scripts/configure.js
verify: scripts/verify.js
uninstall: scripts/uninstall.js # so it can be removed cleanlyRemove cleanly (uninstall)
When a service is removed from a box it doesn't own (a fleet/co-located service taken out of managed.yaml, or managed service destroy), managed runs its uninstall action. Use it to stop and remove the daemon; make every step idempotent. The engine removes firewall rules and state itself. Daemon and fleet packages should ship an uninstall script, without one, a removed service leaves its process running.
js
const managed = require("managed");
if (managed.systemd.isActive("myd")) managed.systemd.stop("myd");
managed.systemd.disable("myd");
managed.files.delete("/etc/systemd/system/myd.service", { sudo: true });
managed.systemd.daemonReload();
managed.files.delete("/usr/local/bin/myd", { sudo: true });Rolling updates and config-change detection
The configure phase re-runs a node only when its config actually changed (a param, or a peer's internal IP). When several nodes of a role change at once, they update one at a time by default, with a readiness check between them, so a warm-up-sensitive role isn't all restarted together. Tune it with rolling_update: { max_unavailable, ready_timeout } on the node and depends_on: [roles] on the configure action.
See DESIGN_SERVICES.md (§F) for the full reference.