[fix] cubes: service:autostart reads its data, and its README describes it

Closes DOCS-AUDIT §6.1 and §5.1.

**The script could not run.** It read `APP` off `host.data` and then used
`SERVICE_NAME` and `AUTOSTART` as if they were in scope, so the very first
statement — `if AUTOSTART:` — raised `NameError`; `server` was used in the else
branch but never imported. Three lines: import `server` alongside `systemd`,
read the two names next to `APP`. The logic underneath was always right.
`python3 -m py_compile` passes.

**The README documented a different cube.** It was titled "TypeStack Install
Cube" and described cloning a git repository, `yarn install`, `yarn build`,
`docker compose up -d` and PM2 — none of which this cube does, and it listed
parameters (`USER`, `REPO`, `ENV`, `NODE_PATH`) the manifest does not have,
carrying someone's private repository URL and username as defaults.

Rewritten from the manifest and the now-working script: the three parameters
that exist, and the thing the old text obscured by describing a deploy
pipeline — this cube does not create the unit file, it enables and starts one
that is already installed. `SERVICE_NAME` is documented as what it is, a label
that never reaches systemd, so getting it wrong is cosmetic rather than a cube
managing the wrong unit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
This commit is contained in:
Benjamin Diedrichsen
2026-09-01 12:48:55 +02:00
co-authored by Claude Opus 5
parent 89450cb7bc
commit b5702e423a
2 changed files with 49 additions and 162 deletions
@@ -1,176 +1,61 @@
# TypeStack Install Cube # autostart
Deploys a Node.js/TypeScript application from a Git repository as a systemd service with Docker Compose and PM2 support. **Enable and start an existing systemd service**
## Features ## Purpose
- Clones Git repository Takes a systemd unit that is already installed on the host and decides whether
- Installs dependencies with Yarn it runs: `systemctl enable` plus `systemctl start`, or neither.
- Builds the application
- Starts Docker Compose services
- Creates a systemd service for automatic startup
- Configures PM2 for process management
- Automatic restart on failure
## Requirements It does **not** create the unit. Something else — a package, another cube, a
`files.template` — has to have put `<APP>.service` on the host first. This cube
- Git (for cloning repository) is the switch, not the wiring.
- Yarn (for dependency management)
- Docker and Docker Compose
- PM2 (for process management)
- Node.js/NVM installed
- SSH key access to the repository (if using private repos)
## Configuration Parameters
### Required
- **USER**: System user to run the application (default: `teclabmin`)
- **REPO**: Git repository URL (default: `git@github.com:bennidi/teclab-flintstone.git`)
- **APP**: Application name/directory name (default: `flintstone`)
### Optional
- **ENV**: Application environment (default: `production`)
- **AUTOSTART**: Enable and start service immediately (default: `True`)
- **NODE_PATH**: Path to Node.js binaries (default: `/home/teclabmin/.nvm/versions/node/v21.7.3/bin`)
## Example Usage
### Basic Configuration
```json
{
"USER": "myuser",
"REPO": "git@github.com:myorg/myapp.git",
"APP": "myapp"
}
```
### Advanced Configuration
```json
{
"USER": "appuser",
"REPO": "git@github.com:myorg/myapp.git",
"APP": "myapp",
"ENV": "staging",
"AUTOSTART": false,
"NODE_PATH": "/home/appuser/.nvm/versions/node/v20.0.0/bin"
}
```
## What This Cube Does ## What This Cube Does
1. **Clone Repository**: Clones the specified Git repository to `/home/<USER>/<APP>` With `AUTOSTART=True` (the default), two `systemd.service` operations against
2. **Install Dependencies**: Runs `yarn install` to install all dependencies `<APP>`: one setting `enabled=True` so the unit comes up on boot, one setting
3. **Build Application**: Runs `yarn build` to compile the application `running=True` so it comes up now. Both are idempotent — a unit already enabled
4. **Start Docker Services**: Runs `docker compose up -d` to start containerized services and running is left alone.
5. **Create Startup Script**: Creates `/home/<USER>/<APP>.service.sh` that:
- Starts Docker Compose services
- Starts PM2 with ecosystem.config.js
6. **Create Systemd Service**: Creates `/etc/systemd/system/<APP>.service` that:
- Runs after Docker service
- Uses the specified user
- Configures proper environment (HOME, PATH)
- Auto-restarts on failure
7. **Enable & Start Service**: Enables and starts the service (if AUTOSTART=True)
## Service Management With `AUTOSTART=False`, nothing is changed. The cube prints the two commands you
would run by hand and exits, which is the point of the flag: install now, decide
later.
### Check service status ## Configuration
| Variable | Default | What it does |
| --- | --- | --- |
| `APP` | *(required)* | the systemd unit name, without the `.service` suffix — `flintstone` for `/etc/systemd/system/flintstone.service`. This is what `systemctl` is actually pointed at. |
| `SERVICE_NAME` | `Application` | a display name, used only in the operation labels pyinfra prints and in the `AUTOSTART=False` message. Changing it changes what you read, not what happens. |
| `AUTOSTART` | `true` | whether to enable and start the unit at all. |
`APP` has no default, so `nopy -D` (`--use-defaults`) fails by name rather than
guessing. Supply it under `env` in `.nopyrc.json`, from a dependency, or at the
prompt.
## Dependencies
None declared, and none implied beyond the unit file itself. `systemd.service`
is a pyinfra built-in; there is nothing to install.
## Post-Installation
```bash ```bash
sudo systemctl status <APP> systemctl status <APP> # is it running?
systemctl is-enabled <APP> # will it come back after a reboot?
journalctl -u <APP> -f # follow its log
``` ```
### Start the service If the run fails with *Unit `<APP>.service` could not be found*, the unit was
never installed — see Purpose. `systemctl daemon-reload` is worth trying if the
```bash file was written after systemd last read the directory.
sudo systemctl start <APP>
```
### Stop the service
```bash
sudo systemctl stop <APP>
```
### Restart the service
```bash
sudo systemctl restart <APP>
```
### View service logs
```bash
sudo journalctl -u <APP> -f
```
### Disable autostart
```bash
sudo systemctl disable <APP>
```
## File Structure
After deployment:
```
/home/<USER>/
├── <APP>/ # Application directory
│ ├── ecosystem.config.js # PM2 configuration
│ ├── docker-compose.yml # Docker services
│ └── ... # Application files
├── <APP>.service.sh # Startup script
/etc/systemd/system/
└── <APP>.service # Systemd service file
```
## Troubleshooting
### Service fails to start
1. Check service logs:
```bash
sudo journalctl -u <APP> -n 50
```
2. Verify Docker is running:
```bash
sudo systemctl status docker
```
3. Check if Node.js path is correct:
```bash
which node
which pm2
```
### Repository clone fails
- Ensure SSH keys are properly configured for the user
- Test SSH access: `ssh -T git@github.com`
- Check repository URL is correct
### Docker Compose fails
- Verify Docker is installed and running
- Check docker-compose.yml exists in the application directory
- Ensure user has Docker permissions: `sudo usermod -aG docker <USER>`
### PM2 not starting
- Verify PM2 is installed: `pm2 --version`
- Check ecosystem.config.js exists
- Verify NODE_PATH includes PM2 binary location
## Notes ## Notes
- The service type is set to `forking` to support PM2's daemon mode - Enabling and starting are separate systemd concepts and this cube always does
- Service will auto-restart on failure with a 5-second delay both or neither. If you need one without the other, call `systemd.service`
- Maximum 5 restart attempts in the burst period from your own deploy script.
- The service waits for Docker to be ready before starting - `SERVICE_NAME` is deliberately not passed to systemd. The unit is identified
- Environment variables can be configured in the ecosystem.config.js file by `APP` alone, so a wrong `SERVICE_NAME` is a cosmetic mistake rather than a
cube that manages the wrong service.
@@ -1,8 +1,10 @@
from pyinfra.operations import systemd from pyinfra.operations import server, systemd
from pyinfra import host from pyinfra import host
APP = host.data.APP APP = host.data.APP
SERVICE_NAME = host.data.SERVICE_NAME
AUTOSTART = host.data.AUTOSTART
# Enable and start the service based on AUTOSTART flag # Enable and start the service based on AUTOSTART flag
if AUTOSTART: if AUTOSTART:
@@ -24,4 +26,4 @@ else:
name=f'Service {SERVICE_NAME} created but not enabled (AUTOSTART=False)', name=f'Service {SERVICE_NAME} created but not enabled (AUTOSTART=False)',
commands=[f'echo "Service {SERVICE_NAME} is ready but not started. Enable with: sudo systemctl enable {APP} && sudo systemctl start {APP}"'], commands=[f'echo "Service {SERVICE_NAME} is ready but not started. Enable with: sudo systemctl enable {APP} && sudo systemctl start {APP}"'],
_sudo=False _sudo=False
) )