4.9 KiB
CI/CD Deploy Setup
Auto-deploys to your LXC server on every push to main via .gitea/workflows/deploy.yml.
1. Server preparation
On the LXC server, allow the deploy user to restart the service without a password:
# As root on the LXC
echo "budget ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart buildfor-life-budget, /usr/bin/systemctl status buildfor-life-budget" > /etc/sudoers.d/budget-deploy
chmod 440 /etc/sudoers.d/budget-deploy
Make sure the repo is cloned and the app works manually first (see docs/deployment.md).
2. Generate SSH keys
You need two SSH key pairs:
a) Deploy key (CI runner → LXC server)
This lets the CI runner SSH into your server:
ssh-keygen -t ed25519 -C "ci-to-server" -f ci_deploy_key -N ""
Copy the public key to the server:
ssh-copy-id -i ci_deploy_key.pub budget@your-lxc-ip
b) Repo deploy key (LXC server → private Gitea repo)
This lets the server git pull from the private repo:
ssh-keygen -t ed25519 -C "server-to-repo" -f repo_deploy_key -N ""
Add the public key in Gitea: repo → Settings → Deploy Keys → Add Deploy Key, paste repo_deploy_key.pub.
3. Add secrets in Gitea
Go to your repo on git.b4l.co.th → Settings → Actions → Secrets, and add:
| Secret | Value |
|---|---|
DEPLOY_HOST |
LXC server IP (e.g. 192.168.10.5) |
DEPLOY_USER |
SSH user (e.g. budget) |
DEPLOY_KEY |
Contents of ci_deploy_key (private key — CI runner → server) |
REPO_DEPLOY_KEY |
Contents of repo_deploy_key (private key — server → Gitea repo) |
DEPLOY_PORT |
SSH port (optional, defaults to 22) |
DEPLOY_PATH |
App directory (optional, defaults to /opt/buildfor-life-budget) |
First clone on the server
The workflow will clone the repo automatically on the first run if DEPLOY_PATH doesn't exist. If you prefer to clone manually:
# On the server as the budget user, set up the deploy key first
mkdir -p ~/.ssh
cp repo_deploy_key ~/.ssh/repo_deploy_key
chmod 600 ~/.ssh/repo_deploy_key
cat >> ~/.ssh/config <<EOF
Host git.b4l.co.th
HostName git.b4l.co.th
IdentityFile ~/.ssh/repo_deploy_key
StrictHostKeyChecking accept-new
EOF
sudo mkdir -p /opt/buildfor-life-budget
sudo chown budget:budget /opt/buildfor-life-budget
git clone git@git.b4l.co.th:B4L/buildfor_life_budget.git /opt/buildfor-life-budget
Remember to create /opt/buildfor-life-budget/.env (see docs/deployment.md) before the first deploy — the service won't start without it.
4. Enable Actions in Gitea
Make sure Gitea Actions is enabled on your instance:
# In app.ini (Gitea config)
[actions]
ENABLED = true
You also need a runner registered. If you don't have one, install the Gitea runner on the Gitea host or another machine:
# Download the runner
wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64
chmod +x act_runner-linux-amd64
# Register with your Gitea instance
./act_runner-linux-amd64 register --instance https://git.b4l.co.th --token <your-runner-token>
# Start
./act_runner-linux-amd64 daemon
5. Test
Push any change to main and check the Actions tab in Gitea for the deploy log.
What the workflow does
- SSHs into the LXC server
- Installs the repo deploy key for private repo access
git pullthe latest code (orgit cloneon first deploy)npm cito install exact lockfile depsnpm run buildto compile SvelteKit (adapter-node)npm run db:pushto apply any schema changes to PostgreSQLsudo systemctl restart buildfor-life-budgetto restart the service- Verifies the service started successfully via
systemctl is-active
Troubleshooting
| Symptom | Likely cause |
|---|---|
Permission denied (publickey) from CI |
DEPLOY_KEY mismatches what's in ~/.ssh/authorized_keys for the deploy user. Re-paste exactly (include -----BEGIN/END----- lines). |
sudo: a password is required |
Sudoers file not installed or has a typo. Check /etc/sudoers.d/budget-deploy with sudo visudo -cf /etc/sudoers.d/budget-deploy. |
git@git.b4l.co.th: Permission denied on the LXC |
REPO_DEPLOY_KEY not registered as a Deploy Key on the repo, or the LXC's ~/.ssh/repo_deploy_key has wrong permissions (must be 600). |
npm ci fails with "lock file version mismatch" |
Node version on the LXC doesn't match what produced package-lock.json. Use the same Node major version as local dev (check .nvmrc if present, else node --version on both sides). |
npm run db:push hangs on interactive prompt |
Destructive schema change (column/table drop). Either revert the change or run it manually with npx drizzle-kit push --force after confirming data loss is acceptable. |
| Service restarts but then exits | Missing or invalid .env — check journalctl -u buildfor-life-budget -n 50. ORIGIN, DATABASE_URL, and UPLOADS_DIR are mandatory. |