development
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 1 | --- |
| 2 | name: smbcloud-deploy-rust |
| 3 | description: Use when deploying or debugging Rust services on smbCloud with `kind = "rust"`, which cross-compiles a Linux binary locally, uploads it with rsync, and restarts it over SSH (nohup by default, or a git-owned `systemctl --user` service when `process_manager = "systemd"`), with a health check and automatic rollback to the previous binary. |
| 4 | --- |
| 5 | |
| 6 | # smbCloud Deploy Rust |
| 7 | |
| 8 | Use this skill when work touches `kind = "rust"` deploy targets. |
| 9 | |
| 10 | ## What `smb deploy` does |
| 11 | |
| 12 | 1. Resolves `process_manager` and `health_path` (a bad value fails before the build). |
| 13 | 2. Builds `--release --bin <binary_name>` for `rust_target` (cargo, `cargo-zigbuild` or `cross`). |
| 14 | 3. rsyncs the binary to `<path>/<binary>.new`. |
| 15 | 4. Runs a remote script that copies the live binary to `<binary>.previous`, moves `.new` into place, restarts the service, and checks it. |
| 16 | 5. If the check fails and `.previous` exists, restores it, restarts, prints the last 20 log lines, and exits non-zero. The deployment is marked `Failed`. |
| 17 | |
| 18 | ## Config |
| 19 | |
| 20 | ```toml |
| 21 | [project] |
| 22 | kind = "rust" |
| 23 | path = "apps/rest-api/my-service" # remote app directory |
| 24 | binary_name = "my-service" # optional; defaults to the Cargo package name |
| 25 | rust_target = "x86_64-unknown-linux-gnu" # optional |
| 26 | process_manager = "systemd" # optional; "nohup" (default) or "systemd" |
| 27 | port = 8080 # optional; used by the health check |
| 28 | health_path = "/health" # optional; must start with "/" |
| 29 | ``` |
| 30 | |
| 31 | - `process_manager`: unset, `""` and `"nohup"` keep the kill-and-nohup start. `"systemd"` restarts `<binary_name>.service`. Any other value is an error, so a typo cannot start a second process next to a supervised one. |
| 32 | - `health_path`: with `port` set, the script polls `http://127.0.0.1:<port><health_path>` with `curl -fsS` once a second for up to 30 seconds. Without both, the check is `systemctl --user is-active` after a short sleep (systemd) or `pidof` (nohup). Only letters, digits and `/ - _ . ~ %` are accepted. |
| 33 | |
| 34 | ## Remote layout |
| 35 | |
| 36 | ``` |
| 37 | <path>/<binary> live binary |
| 38 | <path>/<binary>.new upload staging, moved into place by the deploy |
| 39 | <path>/<binary>.previous copy of the binary that was live before this deploy |
| 40 | <path>/<binary>.log nohup mode only |
| 41 | ``` |
| 42 | |
| 43 | ## Process manager: systemd --user |
| 44 | |
| 45 | The deploy SSHes as the app user and runs `systemctl --user restart <binary_name>.service` with `XDG_RUNTIME_DIR=/run/user/$(id -u)`. No sudo. The unit is created out of band; a missing unit fails the deploy before the binary is swapped. |
| 46 | |
| 47 | The graceful stop time is the unit's `TimeoutStopSec`. Set it above the longest stream the service holds. |
| 48 | |
| 49 | Prerequisites, once per app: |
| 50 | |
| 51 | 1. Enable linger for the app user so the unit runs without a login and starts at boot: `sudo loginctl enable-linger <user>`. |
| 52 | 2. Create the env file and the unit as that user: |
| 53 | |
| 54 | ```sh |
| 55 | export XDG_RUNTIME_DIR="/run/user/$(id -u)" |
| 56 | APP=my-service # == binary_name == the unit name |
| 57 | APP_PATH=~/apps/rest-api/my-service # == `path` in .smb/config.toml |
| 58 | mkdir -p ~/.config/systemd/user |
| 59 | |
| 60 | install -m 600 /dev/null ~/.config/systemd/user/$APP.env |
| 61 | # add the service's environment variables, one KEY=value per line |
| 62 | |
| 63 | cat > ~/.config/systemd/user/$APP.service <<EOF2 |
| 64 | [Unit] |
| 65 | Description=$APP |
| 66 | After=network-online.target |
| 67 | |
| 68 | [Service] |
| 69 | Type=simple |
| 70 | WorkingDirectory=$APP_PATH |
| 71 | EnvironmentFile=%h/.config/systemd/user/$APP.env |
| 72 | ExecStart=$APP_PATH/$APP |
| 73 | Restart=on-failure |
| 74 | RestartSec=2 |
| 75 | TimeoutStopSec=60 |
| 76 | |
| 77 | [Install] |
| 78 | WantedBy=default.target |
| 79 | EOF2 |
| 80 | |
| 81 | systemctl --user daemon-reload |
| 82 | ``` |
| 83 | |
| 84 | 3. Set `process_manager = "systemd"` in `.smb/config.toml`. |
| 85 | |
| 86 | ## Cutting over from nohup or a system unit |
| 87 | |
| 88 | From a nohup process: stop it (`kill $(pidof <binary>)`), then `systemctl --user enable --now <binary>.service` and confirm `systemctl --user is-active`. Never run both; they fight over the port. |
| 89 | |
| 90 | From a system unit (`/etc/systemd/system`): this mode does not use sudo, so move the service to a user unit first: stop and disable the system unit, create the user unit as above, enable it, then switch `process_manager`. Until then, keep deploying that service manually. |
| 91 | |
| 92 | ## Troubleshooting |
| 93 | |
| 94 | - `unit ... not found`: the user unit does not exist or `daemon-reload` was not run. |
| 95 | - `Failed to connect to bus`: linger is not enabled for the app user. |
| 96 | - Health check times out: check `journalctl --user -u <binary>.service -n 50`, and that `port` matches what the service binds. |
| 97 | - Rolled back: the previous binary is serving again; the failed build is gone from the server, fix and redeploy. |
| 98 | - No `.previous` on the first deploy, so a failed first deploy cannot roll back. |