Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/install/ubuntu.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,18 @@ sudo apt update && sudo apt upgrade simpledeploy

Schema migrations run automatically on the next start. See [Upgrading](/install/upgrading/) for rollback notes.

## Troubleshooting

### `status=226/NAMESPACE` or `Failed to set up mount namespacing`

Old releases (< the systemd unit fix) listed a path in `ReadWritePaths` that the package never created, and let Caddy's ACME state fall under `/root/.local/share/caddy`, which the unit's `ProtectHome=true` blocks. Both are fixed in current releases. If you hit this on an existing host:

```bash
sudo apt update && sudo apt upgrade simpledeploy
sudo systemctl daemon-reload
sudo systemctl restart simpledeploy
```

ACME state now lives under `/var/lib/simpledeploy/caddy/`. If you previously had certs under `/root/.local/share/caddy/`, you can copy them over to skip a re-issue, or just let Let's Encrypt re-issue on next request.

Next: [First deploy](/first-deploy/prepare/).
4 changes: 2 additions & 2 deletions docs/reference/directory-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ Default: `/var/lib/simpledeploy/`.
├── backups/ # local backup target output
│ └── <app-slug>/
│ └── 2026-04-17T02-00-00Z.sql.gz
└── caddy/ # Caddy storage (only when tls.mode is "local")
└── caddy/ # Caddy storage (ACME state, certs, locks)
├── certificates/
└── locks/
```

For `tls.mode: auto`, Caddy stores ACME state and certificates under its default location (`$HOME/.local/share/caddy` for the user running `simpledeploy`, typically `/root/.local/share/caddy` under systemd). For `tls.mode: local`, state lives under `data_dir/caddy/`. For `tls.mode: custom`, you provide cert files and SimpleDeploy reads them from the path declared by each app.
Caddy storage lives under `data_dir/caddy/` for both `tls.mode: auto` (Let's Encrypt ACME state and issued certs) and `tls.mode: local` (self-signed CA). For `tls.mode: custom`, you provide cert files and SimpleDeploy reads them from the path declared by each app.

Permissions:

Expand Down
5 changes: 4 additions & 1 deletion internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ func (c *CaddyProxy) buildConfig() map[string]interface{} {
},
}

if (c.tlsMode == "local" || needsLocalTLS) && c.dataDir != "" {
// Pin Caddy storage under data_dir for all TLS modes. Without this,
// certmagic falls back to $HOME/.local/share/caddy, which is masked by
// systemd's ProtectHome=true in the shipped unit and breaks tls.mode=auto.
if c.dataDir != "" {
cfg["storage"] = map[string]interface{}{
"module": "file_system",
"root": filepath.Join(c.dataDir, "caddy"),
Expand Down
26 changes: 26 additions & 0 deletions internal/proxy/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,32 @@ func TestBuildConfigTLSLocalStorage(t *testing.T) {
}
}

func TestBuildConfigTLSAutoStorage(t *testing.T) {
// Storage must be pinned for auto mode too, otherwise certmagic falls
// back to $HOME/.local/share/caddy which ProtectHome=true blocks under
// the shipped systemd unit.
dataDir := "/tmp/testdata"
p := NewCaddyProxy(CaddyConfig{
ListenAddr: ":443",
TLSMode: "auto",
TLSEmail: "ops@example.com",
DataDir: dataDir,
})
cfg := parseConfig(t, p)

storage, ok := cfg["storage"].(map[string]interface{})
if !ok {
t.Fatal("storage not set for tls.mode=auto")
}
if storage["module"] != "file_system" {
t.Errorf("storage.module: got %v, want \"file_system\"", storage["module"])
}
wantRoot := dataDir + "/caddy"
if storage["root"] != wantRoot {
t.Errorf("storage.root: got %v, want %q", storage["root"], wantRoot)
}
}

func TestBuildConfigMixedLocalAndOff(t *testing.T) {
p := NewCaddyProxy(CaddyConfig{
ListenAddr: ":443",
Expand Down
8 changes: 7 additions & 1 deletion simpledeploy.service
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ ExecStart=/usr/local/bin/simpledeploy serve --config /etc/simpledeploy/config.ya
Restart=always
RestartSec=5

# Point HOME at a writable, allow-listed path. ProtectHome=true masks /root,
# so anything that consults $HOME (docker CLI config, certmagic ACME fallback,
# future deps) needs an alternate writable HOME. /var/lib/simpledeploy is
# already in ReadWritePaths below.
Environment=HOME=/var/lib/simpledeploy

# --- Hardening ---
# SimpleDeploy must run as root by default because it talks to the docker
# daemon socket and binds privileged ports (80/443) via Caddy. The
Expand All @@ -23,7 +29,7 @@ NoNewPrivileges=true

# Read-only system tree, with explicit writable paths for state.
ProtectSystem=strict
ReadWritePaths=/etc/simpledeploy /var/lib/simpledeploy /var/log/simpledeploy
ReadWritePaths=/etc/simpledeploy /var/lib/simpledeploy
ProtectHome=true
PrivateTmp=true

Expand Down
Loading