|
| 1 | +# Persistence and Data Storage |
| 2 | + |
| 3 | +This document is the authoritative reference for how opencode-cloud persists |
| 4 | +data across container restarts, updates, and recreations. It covers the volume |
| 5 | +architecture, host paths, backup strategies, and design rationale. |
| 6 | + |
| 7 | +## Two Persistence Modes |
| 8 | + |
| 9 | +opencode-cloud supports two ways to persist container data: |
| 10 | + |
| 11 | +| Mode | When it's used | How data is stored | |
| 12 | +|------|---------------|-------------------| |
| 13 | +| **Named Docker volumes** | Docker Compose (`docker-compose.yml`), `docker run -v` | Docker manages storage on the host; paths are opaque | |
| 14 | +| **Bind mounts** | `occ start` (the CLI) | Host directories are mounted directly into the container | |
| 15 | + |
| 16 | +With **named volumes** (Docker Compose / `docker run`), Docker manages the |
| 17 | +storage location. You interact with volumes by name (e.g., `opencode-data`) |
| 18 | +rather than by host path. |
| 19 | + |
| 20 | +With **bind mounts** (CLI mode), the CLI creates directories on the host and |
| 21 | +mounts them into the container. You can see and back up these directories |
| 22 | +directly. |
| 23 | + |
| 24 | +Both modes mount to the same container paths, so the application behaves |
| 25 | +identically regardless of which mode you use. |
| 26 | + |
| 27 | +## Volume Reference |
| 28 | + |
| 29 | +The container uses 7 persistent storage locations: |
| 30 | + |
| 31 | +| Volume Name | Container Path | CLI Bind Mount (Host Path) | Contents | Backup Priority | |
| 32 | +|-------------|---------------|---------------------------|----------|----------------| |
| 33 | +| `opencode-data` | `/home/opencoder/.local/share/opencode` | `~/.local/share/opencode` | Session data, project metadata, SSH key metadata | Critical | |
| 34 | +| `opencode-workspace` | `/home/opencoder/workspace` | `~/opencode` | Cloned repositories, project files | Critical | |
| 35 | +| `opencode-ssh` | `/home/opencoder/.ssh` | `~/.local/share/opencode-cloud/ssh` | SSH private/public keys, SSH config | High | |
| 36 | +| `opencode-users` | `/var/lib/opencode-users` | *(named volume only)* | User account records (password hashes, lock status) | High | |
| 37 | +| `opencode-config` | `/home/opencoder/.config/opencode` | `~/.config/opencode-cloud/opencode` | opencode app configuration (auth settings, workspace root) | Medium | |
| 38 | +| `opencode-state` | `/home/opencoder/.local/state/opencode` | `~/.local/state/opencode` | Application state, update command files | Medium | |
| 39 | +| `opencode-cache` | `/home/opencoder/.cache/opencode` | `~/.cache/opencode` | Cache data (regenerable) | Low | |
| 40 | + |
| 41 | +**Note:** `opencode-users` has no default bind mount in CLI mode. It always |
| 42 | +uses a named Docker volume because `/var/lib/opencode-users` is a system path |
| 43 | +managed internally by the CLI. Users don't need direct filesystem access to |
| 44 | +this data. |
| 45 | + |
| 46 | +## What Survives What |
| 47 | + |
| 48 | +| Scenario | Named Volumes | Anonymous Volumes | |
| 49 | +|----------|--------------|-------------------| |
| 50 | +| `docker restart` | Preserved | Preserved | |
| 51 | +| `docker stop` + `docker start` | Preserved | Preserved | |
| 52 | +| `docker rm` + `docker run` | Preserved | **Lost** | |
| 53 | +| Image update (`docker pull` + recreate) | Preserved | **Lost** | |
| 54 | +| `docker compose down` | Preserved | Preserved | |
| 55 | +| `docker compose down -v` | **Lost** | **Lost** | |
| 56 | +| `occ update container` | Preserved | N/A (CLI uses bind mounts) | |
| 57 | +| `occ reset container` | Preserved | N/A | |
| 58 | +| `occ reset container --volumes` | **Lost** | N/A | |
| 59 | + |
| 60 | +**Recommendation:** Always use named volumes (Docker Compose or `-v` flags) or |
| 61 | +the CLI with bind mounts for data you want to keep. The Docker image declares |
| 62 | +`VOLUME` directives for all 7 paths as an anonymous volume fallback, but |
| 63 | +anonymous volumes do not survive container removal. |
| 64 | + |
| 65 | +## Non-Persistent Paths |
| 66 | + |
| 67 | +Any container path **not listed above** is ephemeral. Data written to |
| 68 | +unlisted paths is lost when the container is removed or recreated. |
| 69 | + |
| 70 | +The entrypoint detects non-persistent paths at startup and logs a warning if |
| 71 | +any critical path lacks persistent storage. Look for |
| 72 | +`WARNING: Persistence is not configured` in container logs. |
| 73 | + |
| 74 | +## Backing Up Your Data |
| 75 | + |
| 76 | +### Bind Mount Mode (CLI) |
| 77 | + |
| 78 | +When using the CLI (`occ start`), data lives in regular host directories. Back |
| 79 | +them up with any file-level backup tool: |
| 80 | + |
| 81 | +```bash |
| 82 | +# Back up all critical data |
| 83 | +tar czf opencode-backup-$(date +%Y%m%d).tar.gz \ |
| 84 | + ~/.local/share/opencode \ |
| 85 | + ~/opencode \ |
| 86 | + ~/.local/share/opencode-cloud/ssh \ |
| 87 | + ~/.config/opencode-cloud/opencode \ |
| 88 | + ~/.local/state/opencode |
| 89 | +``` |
| 90 | + |
| 91 | +The `opencode-users` volume is managed by Docker even in CLI mode. To back it |
| 92 | +up, use the named volume method below. |
| 93 | + |
| 94 | +### Named Volume Mode (Docker Compose / `docker run`) |
| 95 | + |
| 96 | +Docker named volumes are not directly accessible as host directories. Use a |
| 97 | +temporary container to extract the data: |
| 98 | + |
| 99 | +```bash |
| 100 | +# Back up a single volume |
| 101 | +docker run --rm \ |
| 102 | + -v opencode-data:/data:ro \ |
| 103 | + -v "$(pwd)":/backup \ |
| 104 | + alpine tar czf /backup/opencode-data.tar.gz -C /data . |
| 105 | + |
| 106 | +# Back up all 7 volumes |
| 107 | +for vol in opencode-data opencode-workspace opencode-ssh \ |
| 108 | + opencode-users opencode-config opencode-state \ |
| 109 | + opencode-cache; do |
| 110 | + docker run --rm \ |
| 111 | + -v "${vol}":/data:ro \ |
| 112 | + -v "$(pwd)":/backup \ |
| 113 | + alpine tar czf "/backup/${vol}.tar.gz" -C /data . |
| 114 | +done |
| 115 | +``` |
| 116 | + |
| 117 | +### Restoring from Backup |
| 118 | + |
| 119 | +**Bind mount mode:** Extract the archive back to the original host paths: |
| 120 | + |
| 121 | +```bash |
| 122 | +tar xzf opencode-backup-20250101.tar.gz -C / |
| 123 | +``` |
| 124 | + |
| 125 | +**Named volume mode:** Stop the container first, then restore: |
| 126 | + |
| 127 | +```bash |
| 128 | +docker compose down # or: docker stop opencode-cloud-sandbox |
| 129 | + |
| 130 | +for vol in opencode-data opencode-workspace opencode-ssh \ |
| 131 | + opencode-users opencode-config opencode-state \ |
| 132 | + opencode-cache; do |
| 133 | + docker run --rm \ |
| 134 | + -v "${vol}":/data \ |
| 135 | + -v "$(pwd)":/backup \ |
| 136 | + alpine sh -c "rm -rf /data/* && tar xzf /backup/${vol}.tar.gz -C /data" |
| 137 | +done |
| 138 | + |
| 139 | +docker compose up -d # or: docker start opencode-cloud-sandbox |
| 140 | +``` |
| 141 | + |
| 142 | +## Design Rationale |
| 143 | + |
| 144 | +### Why `~/opencode` for the workspace host path |
| 145 | + |
| 146 | +Upstream opencode's default `workspace.root` is `~/opencode`. By using the |
| 147 | +same host path for the CLI bind mount, users who previously ran vanilla |
| 148 | +upstream opencode locally will see their existing repos immediately when |
| 149 | +switching to opencode-cloud. The host `~/opencode/` maps to the container's |
| 150 | +`/home/opencoder/workspace/`, and the container's `workspace.root` config |
| 151 | +points there, so clones land on the persistent mount. |
| 152 | + |
| 153 | +### Why SSH keys are isolated from the host's `~/.ssh` |
| 154 | + |
| 155 | +The default CLI bind mount for SSH is `~/.local/share/opencode-cloud/ssh` |
| 156 | +rather than the host's `~/.ssh` directory. This is intentional: |
| 157 | + |
| 158 | +1. **Security.** The container runs AI-generated code. If that code (or a |
| 159 | + malicious package) reads files in the mounted SSH directory, an isolated |
| 160 | + path limits exposure to only keys generated inside the sandbox rather than |
| 161 | + the user's entire SSH keyring (which may include keys for production |
| 162 | + servers, infrastructure, etc.). |
| 163 | + |
| 164 | +2. **Write safety.** The opencode app writes to `~/.ssh/config` and creates |
| 165 | + keys under `~/.ssh/opencode/`. If the container modified the host's real |
| 166 | + `~/.ssh/config`, it could break the user's existing SSH setup. SSH config |
| 167 | + parsing is order-sensitive; an opencode-added `Host *` block could shadow |
| 168 | + existing entries. |
| 169 | + |
| 170 | +3. **Permission compatibility.** SSH enforces strict permissions (`~/.ssh` |
| 171 | + must be 700, keys must be 600, owned by the correct user). The container |
| 172 | + runs as `opencoder` (UID 1000), but the host user may have a different |
| 173 | + UID. A bind mount preserves host UID ownership, so the container process |
| 174 | + can't read/write the files unless UIDs happen to match. This would |
| 175 | + silently fail on many setups. |
| 176 | + |
| 177 | +4. **macOS Docker Desktop.** Docker Desktop's Linux VM emulates file |
| 178 | + permissions for bind mounts inconsistently across versions, making host |
| 179 | + `~/.ssh` mounts unreliable for SSH's strict permission checks. |
| 180 | + |
| 181 | +**Power-user escape hatch:** If you want the container to use your host SSH |
| 182 | +keys directly, you can override the mount: |
| 183 | + |
| 184 | +```bash |
| 185 | +occ mount add ~/.ssh:/home/opencoder/.ssh |
| 186 | +occ restart |
| 187 | +``` |
| 188 | + |
| 189 | +This replaces the default isolated mount with a direct bind to your host's |
| 190 | +`~/.ssh`. Use this if you understand the security implications and want |
| 191 | +seamless access to your existing keys. |
| 192 | + |
| 193 | +### Why the config host path is `~/.config/opencode-cloud/opencode` |
| 194 | + |
| 195 | +The config bind mount uses `~/.config/opencode-cloud/opencode` rather than |
| 196 | +`~/.config/opencode` (the upstream opencode config directory). This prevents |
| 197 | +the container's entrypoint from modifying an upstream opencode installation's |
| 198 | +config if the user runs both locally. The entrypoint patches config files |
| 199 | +(enabling auth, setting `workspace.root`), and writing those changes to |
| 200 | +`~/.config/opencode` would alter the user's standalone opencode setup. The |
| 201 | +`opencode-cloud` subdirectory keeps our fork's config isolated, consistent |
| 202 | +with other fork-owned host paths (`~/.config/opencode-cloud/config.json` for |
| 203 | +CLI config, `~/.local/share/opencode-cloud/ssh` for SSH keys). |
| 204 | + |
| 205 | +### Why `opencode-users` has no default bind mount |
| 206 | + |
| 207 | +The `/var/lib/opencode-users` path is a system-level directory managed |
| 208 | +internally by the CLI for user account records (JSON files with password |
| 209 | +hashes and lock status). It uses a named Docker volume rather than a bind |
| 210 | +mount because there is no meaningful host-side equivalent, and the CLI manages |
| 211 | +these records programmatically. |
| 212 | + |
| 213 | +## Platform-Specific Notes |
| 214 | + |
| 215 | +### Railway |
| 216 | + |
| 217 | +Railway currently supports one volume per service. Mount it at |
| 218 | +`/home/opencoder/.local/share/opencode` to cover the most critical data. |
| 219 | +Other paths reset on each redeploy. See [Railway deployment guide](deploy/railway.md) |
| 220 | +for details. |
| 221 | + |
| 222 | +### Docker Desktop (GUI) |
| 223 | + |
| 224 | +Clicking "Run" in Docker Desktop without configuring named volumes creates |
| 225 | +anonymous volumes. These survive container restarts but are lost if the |
| 226 | +container is removed. For durable persistence, use Docker Compose or the CLI |
| 227 | +method with named `-v` flags. See [Docker Desktop guide](deploy/docker-desktop.md). |
| 228 | + |
| 229 | +### Isolated Sandbox Instances |
| 230 | + |
| 231 | +When using `--sandbox-instance` (worktree isolation), volume names are |
| 232 | +suffixed with the instance ID (e.g., `opencode-data-mytree`, |
| 233 | +`opencode-ssh-mytree`). Each instance has its own independent set of 7 |
| 234 | +volumes. |
0 commit comments