1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
---
name: ship-binary
description: Upload and deploy a pre-built binary to a ship VPS. Handles port allocation, systemd service, env vars, Caddy config, and SQLite backup. Use when deploying a Go binary or other compiled executable.
argument-hint: "<path-to-binary> <app-name> [host-nickname]"
---
# ship-binary
Upload a pre-built binary and deploy it as a systemd service with Caddy reverse proxy.
## Read Config
```bash
python3 -c "
import json, os
cfg = json.load(open(os.path.expanduser('~/.config/ship/config.json')))
nick = '<nickname-or-default>'
h = cfg['hosts'].get(nick, cfg['hosts'][cfg['default']])
print(h['host'])
print(h['domain'])
"
```
## Inputs
- **Binary path** — local path to the compiled binary
- **App name** — short lowercase name, becomes the service name and subdomain (e.g. `foodtracker`)
- **Domain** — defaults to `<app-name>.<base-domain>` from config, ask if different
- **Env vars** — ask if there are any env vars to set (beyond PORT/SHIP_NAME/SHIP_URL)
- **Host** — use default unless specified
## Steps
### 1. Check if app already exists
```bash
ssh <host> "test -f /etc/ship/ports/<app-name> && echo exists || echo new"
```
This determines whether to allocate a new port or reuse the existing one.
### 2. Backup SQLite databases (if app exists)
Before touching anything, check for SQLite files in the app's data directory:
```bash
ssh <host> "find /var/lib/<app-name>/ -name '*.db' 2>/dev/null"
```
If any `.db` files are found, back them up:
```bash
ssh <host> "sudo mkdir -p /var/lib/<app-name>/backups && sudo cp /var/lib/<app-name>/data/<name>.db /var/lib/<app-name>/backups/<name>-\$(date +%Y%m%d-%H%M%S).db"
```
Tell the user what was backed up before proceeding.
### 3. Allocate or retrieve port
**New app** — find the highest port in use and add 1:
```bash
ssh <host> "sudo bash -c 'max=9000; for f in /etc/ship/ports/*; do p=\$(cat \$f 2>/dev/null); [ \"\$p\" -gt \"\$max\" ] && max=\$p; done; port=\$((max+1)); echo \$port | tee /etc/ship/ports/<app-name>; echo \$port'"
```
**Existing app** — reuse the existing port:
```bash
ssh <host> "cat /etc/ship/ports/<app-name>"
```
### 4. Upload binary
```bash
scp <binary-path> <host>:/tmp/<app-name>
ssh <host> "sudo mv /tmp/<app-name> /usr/local/bin/<app-name> && sudo chmod +x /usr/local/bin/<app-name>"
```
### 5. Create work directory and service user
```bash
ssh <host> "sudo mkdir -p /var/lib/<app-name>/data && sudo useradd -r -s /bin/false <app-name> 2>/dev/null || true && sudo chown -R <app-name>:<app-name> /var/lib/<app-name>"
```
### 6. Write env file
Build the env file content merging ship-managed vars with any user-provided vars.
If the file already exists, read it first and merge (new values win, old values survive):
```bash
ssh <host> "sudo cat /etc/ship/env/<app-name>.env 2>/dev/null"
```
Then write merged result:
```bash
ssh <host> "sudo tee /etc/ship/env/<app-name>.env > /dev/null << 'EOF'
PORT=<port>
SHIP_NAME=<app-name>
SHIP_URL=https://<domain>
<user-env-vars>
EOF
sudo chmod 600 /etc/ship/env/<app-name>.env"
```
### 7. Write systemd unit
```bash
ssh <host> "sudo tee /etc/systemd/system/<app-name>.service > /dev/null << 'EOF'
[Unit]
Description=<app-name>
After=network.target
[Service]
Type=simple
User=<app-name>
WorkingDirectory=/var/lib/<app-name>
EnvironmentFile=/etc/ship/env/<app-name>.env
ExecStart=/usr/local/bin/<app-name>
Restart=always
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF"
```
### 8. Start or restart service
```bash
ssh <host> "sudo systemctl daemon-reload"
```
**New app:**
```bash
ssh <host> "sudo systemctl enable --now <app-name>"
```
**Existing app:**
```bash
ssh <host> "sudo systemctl restart <app-name>"
```
### 9. Write Caddy config
```bash
ssh <host> "sudo tee /etc/caddy/sites-enabled/<app-name>.caddy > /dev/null << 'EOF'
<domain> {
reverse_proxy 127.0.0.1:<port>
}
EOF
sudo systemctl reload caddy"
```
### 10. Confirm
Tell the user:
- App name, URL, and port
- Whether it was a new deploy or update
- Any SQLite backups made
- Any env vars set
## Notes
- Always back up SQLite before swapping the binary
- Always merge env vars — never replace the whole file
- Use `systemctl restart` for existing apps, `enable --now` for new ones
- The data directory `/var/lib/<app-name>/data/` persists across deploys
- If the user doesn't specify env vars, ask if they need any before deploying
|