Fix deployment guide issues found during a live walkthrough

Three real, reproducible bugs in DEPLOYMENT.md's app-server setup steps,
each found by actually following the guide on a real box rather than
static review:

- useradd --system --create-home is unreliable on Debian in both
  directions: sometimes it silently skips creating the home directory,
  sometimes it creates it AND populates it from /etc/skel
  (.bashrc/.profile/.bash_logout). Either way it broke a later step --
  the skel files make git clone refuse to clone into a non-empty
  directory. Fixed by dropping --create-home and creating the directory
  ourselves, deferring anything else that goes in it (uploads/) until
  after the clone.
- Documented a personal/deployment-user access token as a first-class
  alternative to the SSH deploy key for cloning, alongside its plaintext-
  in-.git/config tradeoff.
- The create-user command nested a user-chosen password inside two layers
  of shell quoting (an outer bash -c '...' plus inner double quotes) --
  fragile for any password with a space or a literal '. Replaced with
  dropping into an authenticated interactive shell first, so there's only
  one layer of quoting to get right at an actual prompt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 23:09:27 -06:00
co-authored by Claude Sonnet 5
parent e20916bca9
commit 630b07d48f
+60 -9
View File
@@ -105,12 +105,26 @@ sudo apt install -y python3 python3-venv nodejs npm git
### 3a. The `ds-chat` system user and directory ### 3a. The `ds-chat` system user and directory
```bash ```bash
sudo useradd --system --shell /usr/sbin/nologin --home-dir /srv/ds-chat --create-home ds-chat sudo useradd --system --shell /usr/sbin/nologin --home-dir /srv/ds-chat ds-chat
# Deliberately no --create-home: on Debian, --system + --create-home is
# unreliable in *both* directions -- sometimes it silently skips creating
# the directory at all, sometimes it creates it and populates it from
# /etc/skel (.bashrc, .profile, .bash_logout), which then makes the git
# clone in the next step fail since it refuses to clone into a non-empty
# directory. Creating the directory ourselves, with no skel copying,
# sidesteps both failure modes.
sudo mkdir -p /srv/ds-chat
sudo chown ds-chat:ds-chat /srv/ds-chat sudo chown ds-chat:ds-chat /srv/ds-chat
sudo -u ds-chat mkdir -p /srv/ds-chat/uploads
``` ```
### 3b. Clone the repo (deploy key, not a personal token) Deliberately not creating `/srv/ds-chat/uploads` here -- `git clone` in the
next step refuses to clone into a non-empty directory, so anything created
inside `/srv/ds-chat` has to wait until after the clone. (The app would
also create this directory itself on first upload if it were missing --
see `app/storage.py` -- so this step is for clarity/permissions, not a
strict requirement.)
### 3b. Clone the repo (deploy key or access token)
```bash ```bash
sudo -u ds-chat mkdir -p /srv/ds-chat/.ssh sudo -u ds-chat mkdir -p /srv/ds-chat/.ssh
@@ -129,6 +143,27 @@ sudo -u ds-chat git clone git@git.darksingularity.org:DarkSingularity/ds-chat.gi
(If your Gitea's SSH is on a non-default port, adjust the clone URL and (If your Gitea's SSH is on a non-default port, adjust the clone URL and
`ssh-keyscan -p <port>` accordingly.) `ssh-keyscan -p <port>` accordingly.)
**Alternative: a personal/deployment-user access token instead of a deploy
key** — skip the `.ssh`/`ssh-keygen`/`ssh-keyscan` commands above entirely
and clone over HTTPS with the token embedded in the URL:
```bash
sudo -u ds-chat git clone https://<TOKEN>@git.darksingularity.org/DarkSingularity/ds-chat.git /srv/ds-chat
```
The token then lives in plaintext in `/srv/ds-chat/.git/config` (`git
remote -v` shows it) — readable by root and the `ds-chat` user, not by
anyone else under normal file permissions. `deploy/upgrade.sh`'s later
`git pull`s reuse this same authenticated URL automatically, no extra
setup needed. Fine as long as the token is scoped to read-only access on
just this repo.
Either way, now that the repo is cloned:
```bash
sudo -u ds-chat mkdir -p /srv/ds-chat/uploads
```
### 3c. Backend: venv, env file, migrations, first admin ### 3c. Backend: venv, env file, migrations, first admin
```bash ```bash
@@ -150,15 +185,31 @@ Generate `SESSION_SECRET`:
python3 -c "import secrets; print(secrets.token_urlsafe(32))" python3 -c "import secrets; print(secrets.token_urlsafe(32))"
``` ```
Run migrations and create the first admin account (as `ds-chat`, with the Run migrations (as `ds-chat`, with the env file sourced so `DATABASE_URL`
env file sourced so `DATABASE_URL` is set): is set) — safe as a single non-interactive command since nothing here is a
user-chosen value that could contain quotes/spaces/special characters:
```bash ```bash
sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; \ sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; cd /srv/ds-chat/backend && .venv/bin/alembic upgrade head'
cd /srv/ds-chat/backend && .venv/bin/alembic upgrade head' ```
sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; \ Creating the first admin account is different — the password is arbitrary
cd /srv/ds-chat/backend && .venv/bin/python -m app.cli create-user <ADMIN_USERNAME> <ADMIN_EMAIL> "<ADMIN_PASSWORD>" --admin' user input, and wrapping it in a second layer of quoting inside the `bash
-c '...'` above is fragile (a space drops the rest of the password as
"unrecognized arguments"; a literal `'` in the password breaks the outer
quoting entirely). Drop into an authenticated shell first instead, so
there's only one layer of quoting to get right, at an actual prompt:
```bash
sudo -u ds-chat bash -c 'set -a; source /etc/ds-chat/env; set +a; exec bash'
```
Then, from inside that shell:
```bash
cd /srv/ds-chat/backend
.venv/bin/python -m app.cli create-user <ADMIN_USERNAME> <ADMIN_EMAIL> "<ADMIN_PASSWORD>" --admin
exit
``` ```
Optional: push notifications. Skipped silently if `VAPID_PUBLIC_KEY`/ Optional: push notifications. Skipped silently if `VAPID_PUBLIC_KEY`/