ci(project): add Gitea Actions deploy workflow and docs
Deploy to the production host on push to master via a self-hosted act_runner (host/shell executor): git fetch + reset --hard in /opt/tanabata, then docker compose up -d --build. Shell-only steps, so the host needs just git and docker — no node, no rsync. docs/DEPLOY.md covers the one-time setup: what a runner is, the runner user, cloning to /opt/tanabata with a read-only deploy key, registering act_runner with the host label, and the host .env. Notes the security reason to scope the runner to this repository. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
name: deploy
|
||||
|
||||
# Build the image and (re)start the compose stack on the production host
|
||||
# whenever master moves. Also runnable manually from the Gitea Actions tab.
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch: {}
|
||||
|
||||
# One deploy at a time; queue rather than cancel an in-flight run.
|
||||
concurrency:
|
||||
group: deploy-prod
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
# Self-hosted act_runner registered on the prod host with the "host" label
|
||||
# (shell executor), so the job uses the host's git + Docker daemon and the
|
||||
# existing clone in /opt/tanabata. See docs/DEPLOY.md for runner setup.
|
||||
#
|
||||
# Only shell steps here (no `uses:` actions), so the host needs git + docker
|
||||
# and nothing else — no node, no rsync.
|
||||
runs-on: host
|
||||
|
||||
env:
|
||||
DEPLOY_DIR: /opt/tanabata
|
||||
|
||||
steps:
|
||||
- name: Pull latest master
|
||||
# DEPLOY_DIR is a git clone set up once at deploy time. reset --hard
|
||||
# makes it match origin exactly; .env is untracked (.gitignore) so it
|
||||
# is never touched.
|
||||
run: |
|
||||
cd "$DEPLOY_DIR"
|
||||
git fetch --prune origin
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Build image and start the stack
|
||||
working-directory: /opt/tanabata
|
||||
# .env must already exist in DEPLOY_DIR on the host (secrets + DB mode).
|
||||
run: docker compose up -d --build --remove-orphans
|
||||
|
||||
- name: Prune dangling build layers
|
||||
run: docker image prune -f
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# Deployment (Gitea Actions → host)
|
||||
|
||||
Tanabata is deployed by a [Gitea Actions](https://docs.gitea.com/usage/actions/overview)
|
||||
workflow ([`.gitea/workflows/deploy.yml`](../.gitea/workflows/deploy.yml)) that
|
||||
runs on the **production host itself**. On every push to `master` it updates the
|
||||
git clone in `/opt/tanabata` and runs `docker compose up -d --build` there, so the
|
||||
image is built from the freshly-pushed code and the stack is restarted.
|
||||
|
||||
```
|
||||
push master ──> Gitea (container) ──> act_runner (host, "host" label)
|
||||
│ git fetch + reset --hard (in /opt/tanabata)
|
||||
└ docker compose up -d --build
|
||||
```
|
||||
|
||||
The Gitea server runs in a container, but the **runner runs directly on the host**
|
||||
(shell executor) so it can use the host's git, the host Docker daemon, and the
|
||||
clone in `/opt/tanabata`. Nothing needs a registry — the host builds the image
|
||||
locally. The workflow uses only shell steps, so the host needs just **git** and
|
||||
**docker** (no node, no rsync).
|
||||
|
||||
## What is a runner?
|
||||
|
||||
Gitea (like GitHub) only *coordinates* CI: it stores the workflow, queues jobs,
|
||||
and shows logs. It does **not** execute anything itself. A **runner** is a
|
||||
separate agent program that polls Gitea for queued jobs, runs the steps on a
|
||||
machine you control, and reports results back.
|
||||
|
||||
Gitea's official runner is **act_runner** (a single Go binary; it uses the
|
||||
`act` engine to interpret workflow YAML). One act_runner process can serve many
|
||||
repos. Each runner advertises one or more **labels**, and a job's `runs-on:`
|
||||
picks a runner by label. A label also decides *how* a job runs — the **executor**:
|
||||
|
||||
- **docker executor** — each job runs in a fresh container from an image (e.g.
|
||||
`node:20-bookworm`). Isolated and reproducible; the usual default. Label form
|
||||
at registration: `ubuntu:docker://node:20-bookworm`.
|
||||
- **host / shell executor** — the job runs directly on the host as the runner's
|
||||
user, using host-installed tools. Label form: `host:host`. This is what we use,
|
||||
because the deploy needs the host's Docker daemon and `/opt/tanabata`.
|
||||
|
||||
So `runs-on: host` in the workflow ⇒ "run this job on a runner that registered a
|
||||
`host` label" ⇒ our shell executor on the prod box.
|
||||
|
||||
## One-time setup
|
||||
|
||||
### 1. Enable Actions in Gitea
|
||||
|
||||
Gitea 1.21+ has Actions on by default. Otherwise add to `app.ini` and restart:
|
||||
|
||||
```ini
|
||||
[actions]
|
||||
ENABLED = true
|
||||
```
|
||||
|
||||
### 2. A runner user on the host
|
||||
|
||||
Pick (or create) the Linux user the runner runs as. It must be able to use Docker
|
||||
and own the deploy dir — so the workflow needs no `sudo`:
|
||||
|
||||
```bash
|
||||
sudo useradd -r -m -d /home/gitea-runner gitea-runner # or reuse an existing user
|
||||
sudo usermod -aG docker gitea-runner # host Docker access
|
||||
```
|
||||
|
||||
The host needs `git` and a Docker engine with the Compose plugin:
|
||||
|
||||
```bash
|
||||
sudo apt install -y git docker.io docker-compose-plugin # Debian/Ubuntu
|
||||
```
|
||||
|
||||
### 3. Clone the repo to /opt/tanabata once
|
||||
|
||||
The workflow only does `git fetch` + `reset --hard`, so the clone (and its auth)
|
||||
is established here, once. Use a **read-only deploy key** so the host never holds
|
||||
write credentials:
|
||||
|
||||
```bash
|
||||
# As the runner user, create a key and add the PUBLIC half to the repo in Gitea:
|
||||
# Repo → Settings → Deploy Keys → Add (read-only)
|
||||
sudo -u gitea-runner ssh-keygen -t ed25519 -f /home/gitea-runner/.ssh/tanabata_deploy -N ''
|
||||
|
||||
# Clone with that key (SSH URL of your Gitea repo):
|
||||
sudo -u gitea-runner GIT_SSH_COMMAND='ssh -i /home/gitea-runner/.ssh/tanabata_deploy' \
|
||||
git clone git@gitea.example.com:you/tanabata.git /opt/tanabata
|
||||
sudo chown -R gitea-runner:gitea-runner /opt/tanabata
|
||||
```
|
||||
|
||||
> HTTPS works too — clone with a URL that carries a read-only token. SSH deploy
|
||||
> keys are the cleaner, per-repo, read-only option.
|
||||
|
||||
After cloning, recurring `git fetch` reuses the remote + key stored in
|
||||
`/opt/tanabata/.git/config`, so the runner itself needs no standing credentials.
|
||||
|
||||
### 4. Register and run act_runner on the host
|
||||
|
||||
Get a registration token in Gitea. **Where you create it sets the runner's
|
||||
scope** (and `--name` is only a display label, unrelated to scope):
|
||||
|
||||
- **Repository** (Tanabata repo → Settings → Actions → Runners) → serves only
|
||||
this repo. **Use this.**
|
||||
- Organization → all repos in the org; Site (admin) → all repos on the instance.
|
||||
|
||||
> Security: this runner is a host/shell executor with access to the Docker
|
||||
> socket — effectively root on the host. Register it at the **repository** level
|
||||
> so only Tanabata's workflows can run on your prod server; a site-wide runner
|
||||
> would let any repo's workflow execute arbitrary commands here.
|
||||
|
||||
Then, as the runner user:
|
||||
|
||||
```bash
|
||||
# Download act_runner: https://gitea.com/gitea/act_runner/releases
|
||||
act_runner register --no-interactive \
|
||||
--instance https://gitea.example.com \
|
||||
--token <REGISTRATION_TOKEN> \
|
||||
--name prod-host \
|
||||
--labels host:host # <-- maps `runs-on: host` to the shell executor
|
||||
|
||||
# Run it (use a systemd unit in production so it survives reboots):
|
||||
act_runner daemon
|
||||
```
|
||||
|
||||
`--labels host:host` is what makes jobs run **on the host** instead of in a
|
||||
container. The instance URL must be reachable from the host (Gitea's published
|
||||
port / domain — not the in-container address). Registration writes a `.runner`
|
||||
file (the runner's credentials) in the working directory.
|
||||
|
||||
Minimal systemd unit (`/etc/systemd/system/act_runner.service`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Gitea act_runner
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
User=gitea-runner
|
||||
WorkingDirectory=/home/gitea-runner
|
||||
ExecStart=/usr/local/bin/act_runner daemon
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now act_runner
|
||||
```
|
||||
|
||||
### 5. Create /opt/tanabata/.env (secrets)
|
||||
|
||||
The workflow **never** writes `.env` — it lives on the host and holds the real
|
||||
secrets and the chosen DB mode. `.env` is git-ignored, so `git reset --hard`
|
||||
leaves it untouched. Create it once:
|
||||
|
||||
```bash
|
||||
cd /opt/tanabata
|
||||
sudo -u gitea-runner cp .env.example .env
|
||||
sudo -u gitea-runner $EDITOR .env # set JWT_SECRET, ADMIN_PASSWORD, DATABASE_URL, etc.
|
||||
```
|
||||
|
||||
See [`.env.example`](../.env.example) for every variable. For the bundled
|
||||
Postgres keep `COMPOSE_PROFILES=with-db`; to use a Postgres already on the host,
|
||||
set it empty and point `DATABASE_URL` at `host.docker.internal`.
|
||||
|
||||
> Data lives in named Docker volumes by default (or the `*_DIR` host paths you
|
||||
> set in `.env`, e.g. `/var/lib/tanabata/...`) — **not** in `/opt/tanabata`. So
|
||||
> `git reset --hard` on the code dir never touches your data.
|
||||
|
||||
## Deploying
|
||||
|
||||
Push to `master` (or hit **Run workflow** on the Actions tab). Watch progress
|
||||
under the repo's **Actions** tab. The first build pulls the Node/Go base images
|
||||
and takes a few minutes; later builds reuse the host's layer cache.
|
||||
|
||||
## Notes / alternatives
|
||||
|
||||
- **Docker-executor runner instead of host.** If you'd rather the runner itself
|
||||
run in a container, register with a Docker label and bind-mount
|
||||
`/var/run/docker.sock` and `/opt/tanabata` into the job (act_runner
|
||||
`config.yaml` → `container.valid_volumes`), then change `runs-on` accordingly.
|
||||
The host executor above is simpler for host deploys.
|
||||
- **Zero-downtime** isn't attempted: `compose up` recreates changed containers.
|
||||
For a single-node setup the brief restart is usually fine.
|
||||
Reference in New Issue
Block a user