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
24 changes: 24 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Preflight — verify ~/.env.app on VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
set -e
if [ ! -f "$HOME/.env.app" ]; then
echo "::error::~/.env.app not found on VPS. Create it before deploying — see docs/VPS_DEPLOY.md section 5 (Set Up Production Environment)."
exit 1
fi
if [ ! -r "$HOME/.env.app" ]; then
echo "::error::~/.env.app exists but is not readable by $USER. Fix with: chmod 600 ~/.env.app"
exit 1
fi
PERMS="$(stat -c '%a' "$HOME/.env.app" 2>/dev/null || stat -f '%Lp' "$HOME/.env.app")"
# Warn (don't fail) if world-readable — users may have deliberate setups.
# $PERMS is a digit string like "600" or "644"; force octal interpretation.
if [ "$((8#$PERMS & 4))" -ne 0 ]; then
echo "::warning::~/.env.app is world-readable (mode $PERMS). Recommend: chmod 600 ~/.env.app"
fi
echo "Preflight OK (mode $PERMS)"

- name: Copy deploy script to VPS
uses: appleboy/scp-action@v1
with:
Expand Down
39 changes: 38 additions & 1 deletion docs/VPS_DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,19 @@ Go to your repo → **Settings** → **Secrets and variables** → **Actions**

## 5. Set Up Production Environment

On your VPS, create the environment file:
On your VPS, create the environment file at `~/.env.app` — **this file must exist before the first deploy**. The CD workflow runs a preflight check and will fail with a clear message if it is missing.

```bash
# On your VPS
cat > ~/.env.app << 'EOF'
PORT=3000
# Add your production environment variables
EOF
chmod 600 ~/.env.app
```

The preflight also verifies `~/.env.app` is not world-readable. `chmod 600` is recommended; wider permissions trigger a warning but do not fail the deploy.

## 6. Deploy

**Option A: Manual trigger**
Expand Down Expand Up @@ -161,6 +164,40 @@ docker compose up -d --wait

> **Tip:** GHCR keeps the last 10 versions by default (configured in `cd.yml`). Make sure the version you need hasn't been pruned.

## SSH Key Rotation

Rotate `VPS_SSH_KEY` on a quarterly cadence, or immediately if you suspect compromise (leaked laptop, ex-contributor access, etc.). The rotation is zero-downtime as long as you keep both keys valid during the switch.

1. **Generate a new ed25519 keypair** on your local machine (passphrase recommended for the at-rest copy):

```bash
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key_new -C "deploy@$(date +%Y-%m)"
```

See [OpenSSH docs](https://www.openssh.com/manual.html) for the underlying options.

2. **Append the new public key to the VPS** — do *not* remove the old one yet:

```bash
ssh-copy-id -i ~/.ssh/deploy_key_new.pub deploy@YOUR_VPS_IP
```

3. **Update the GitHub Secret** `VPS_SSH_KEY` with the contents of the new *private* key (`~/.ssh/deploy_key_new`). Repo → Settings → Secrets and variables → Actions → `VPS_SSH_KEY` → Update.

4. **Verify CD still works.** Push a trivial commit (or re-run the last Deploy workflow) and confirm it reaches the VPS. If it fails, the old key is still authorized — roll back the secret and investigate.

5. **Remove the old public key** from `~/.ssh/authorized_keys` on the VPS once the new key is confirmed working:

```bash
ssh deploy@YOUR_VPS_IP
# Edit authorized_keys and delete the old line
nano ~/.ssh/authorized_keys
```

6. **Delete the old private key locally** (`rm ~/.ssh/deploy_key`) and document the rotation date.

> **Locked out?** Use your VPS provider's web console (DigitalOcean, Hetzner, etc. all offer one) to log in, re-add a working public key, and restart. Never store the only copy of a recovery key in GitHub alone.

## Advanced: Multi-Container Setup

The default deployment creates a single-service `docker-compose.yml` on VPS at `~/app/`.
Expand Down
Loading