Compare commits
1 Commits
main
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1ff624c98 |
@@ -193,15 +193,6 @@ curl -fsSL https://tailscale.com/install.sh | sh && tailscale up
|
||||
# files to disk during execution (cleaned up unconditionally on completion).
|
||||
# A multi-tenant runner would need to switch to stdin-piped env files.
|
||||
# (See https://docs.gitea.com/usage/actions/quickstart for the register step.)
|
||||
|
||||
# Runner workspace directory — required for DooD bind-mount resolution (ADR-015).
|
||||
# act_runner stores job workspaces here so that docker compose bind mounts resolve
|
||||
# to real host paths. The path must be identical on the host and inside job containers.
|
||||
mkdir -p /srv/gitea-workspace
|
||||
# Also add this volume line to the runner service in ~/docker/gitea/compose.yaml:
|
||||
# volumes:
|
||||
# - /srv/gitea-workspace:/srv/gitea-workspace
|
||||
# See runner-config.yaml (workdir_parent + valid_volumes + options) and ADR-015.
|
||||
```
|
||||
|
||||
### 3.2 DNS records
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# ADR-015: DooD workspace bind mount for Compose file bind-mount resolution
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The deploy workflows (`.gitea/workflows/nightly.yml`, `release.yml`) run job steps inside Docker containers via Docker-out-of-Docker (DooD): the Gitea runner mounts the host Docker socket, and act_runner spawns sibling containers for each job.
|
||||
|
||||
When a job step calls `docker compose -f docker-compose.observability.yml up`, Docker Compose resolves relative bind-mount sources against `$(pwd)` inside the job container and passes the resulting absolute paths to the **host** daemon. For example, `./infra/observability/prometheus/prometheus.yml` becomes `/some/path/infra/observability/prometheus/prometheus.yml`, and the host daemon tries to bind-mount that path from the **host filesystem**.
|
||||
|
||||
In the default DooD setup (`runner-config.yaml` with only `valid_volumes: ["/var/run/docker.sock"]`), job container workspaces live in the act_runner overlay2 layer. The host has no corresponding directory at the job container's `$(pwd)` path, so the daemon auto-creates an empty directory in its place. The container then fails to start because the mount target was expected to be a file, not a directory:
|
||||
|
||||
```
|
||||
error mounting "…/prometheus/prometheus.yml" to rootfs at "/etc/prometheus/prometheus.yml": not a directory
|
||||
```
|
||||
|
||||
This affected all five config file bind mounts in `docker-compose.observability.yml`.
|
||||
|
||||
## Decision
|
||||
|
||||
Configure act_runner to store job workspaces on a real host path (`/srv/gitea-workspace`) and mount that path into both the runner container and every job container at the **same absolute path**. The identity of the host path and container path is the key constraint: Compose resolves to an absolute path and hands it to the host daemon, which looks for that exact path on the host filesystem.
|
||||
|
||||
**runner-config.yaml changes:**
|
||||
|
||||
```yaml
|
||||
container:
|
||||
workdir_parent: /srv/gitea-workspace
|
||||
valid_volumes:
|
||||
- "/var/run/docker.sock"
|
||||
- "/srv/gitea-workspace"
|
||||
options: "-v /srv/gitea-workspace:/srv/gitea-workspace"
|
||||
```
|
||||
|
||||
**Runner compose.yaml change** (host side — not in this repo):
|
||||
|
||||
```yaml
|
||||
runner:
|
||||
volumes:
|
||||
- /srv/gitea-workspace:/srv/gitea-workspace
|
||||
```
|
||||
|
||||
With this in place, `$(pwd)` inside a job container resolves to `/srv/gitea-workspace/<owner>/<repo>/`, which is a real directory on the host. Compose-managed bind mounts from that directory work without any additional steps.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Alternative | Why rejected |
|
||||
|---|---|
|
||||
| **overlay2 `MergedDir` sync via privileged nsenter** (the previous approach, see PR #599 v1) | Required `--privileged --pid=host` (effective root on the host) plus fragile overlay2 driver assumption. Introduced stale-file risk on the host and a second stable path (`/srv/familienarchiv-*/obs-configs`) to maintain separately from the source tree. Replaced by this ADR. |
|
||||
| **Build configs into a dedicated Docker image** (pattern used for MinIO bootstrap, see `infra/minio/Dockerfile`) | Viable for static files that change infrequently. Requires a build step and an image rebuild every time a config changes. Appropriate for bootstrap scripts; too heavy for frequently-tuned observability configs. |
|
||||
| **Add workspace directory to runner-config `valid_volumes` only** (without `workdir_parent`) | `valid_volumes` whitelists paths that workflow steps may reference, but does not change where act_runner stores workspaces. Without `workdir_parent`, the workspace would still be in overlay2 and the bind-mount resolution problem would remain. |
|
||||
| **Map workspace under a different host path than container path** (e.g. host `/srv/workspace`, container `/workspace`) | Compose resolves to the container-internal path (e.g. `/workspace/…`) and passes that to the host daemon. The host daemon interprets the source as a host path. If host `/workspace` does not exist, the daemon creates an empty directory — the original bug. The paths must be identical. |
|
||||
|
||||
## Consequences
|
||||
|
||||
- `/srv/gitea-workspace` must exist on the VPS before the runner starts. The directory was created as part of this change; it is not created automatically.
|
||||
- The runner container's `compose.yaml` (maintained outside this repo at `~/docker/gitea/compose.yaml` on the VPS) must include the `- /srv/gitea-workspace:/srv/gitea-workspace` volume line. This is an out-of-band operational dependency; the prerequisite is documented in `runner-config.yaml`.
|
||||
- `workdir_parent` applies to all jobs on this runner. Any future workflow that calls `docker compose` with relative bind mounts benefits automatically without further configuration.
|
||||
- Job workspaces persist across runs under `/srv/gitea-workspace`. act_runner manages per-run subdirectory cleanup. Orphaned directories from interrupted runs should be cleaned up manually if disk space becomes a concern.
|
||||
- Workflows that previously relied on `OBS_CONFIG_DIR` env var or the `obs-configs` stable path on the host no longer need those. Both were removed in this PR.
|
||||
- This pattern does **not** apply to the `nsenter`-based Caddy reload step (ADR-012), which manages a host systemd service — a different problem class with no bind-mount equivalent.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-011 — single-tenant runner trust model
|
||||
- ADR-012 — nsenter via privileged container for host service management
|
||||
- Issue #598 — original observability stack bind-mount failure
|
||||
- `runner-config.yaml` — `workdir_parent`, `valid_volumes`, `options`
|
||||
@@ -19,39 +19,6 @@ Both containers live in the `gitea_gitea` Docker network on the VPS. The runner
|
||||
|
||||
The `gitea-runner` container mounts the host Docker socket (`/var/run/docker.sock`). When a workflow job runs, act_runner spawns a **sibling container** for each job. That job container also gets the Docker socket mounted (via `valid_volumes` in `runner-config.yaml`), enabling `docker compose` calls in workflow steps.
|
||||
|
||||
### Workspace bind-mount setup (DooD path resolution)
|
||||
|
||||
When a workflow step calls `docker compose up` with relative bind-mount sources (e.g. `./infra/observability/prometheus/prometheus.yml`), Compose resolves them against `$(pwd)` inside the job container and passes the resulting **absolute path** to the host Docker daemon. The host daemon then tries to bind-mount that path from the **host filesystem**.
|
||||
|
||||
In the default DooD setup the job container's workspace lives in the act_runner overlay2 layer — the host has no directory at that path, auto-creates an empty one, and the container fails with:
|
||||
|
||||
```
|
||||
error mounting "…/prometheus/prometheus.yml" to rootfs at "/etc/prometheus/prometheus.yml": not a directory
|
||||
```
|
||||
|
||||
**Solution (ADR-015):** store job workspaces on a real host path and mount it at the **same absolute path** inside the runner and every job container. `runner-config.yaml` configures this via `workdir_parent`, `valid_volumes`, and `options`.
|
||||
|
||||
**One-time host setup** (required on any fresh VPS):
|
||||
|
||||
```bash
|
||||
mkdir -p /srv/gitea-workspace
|
||||
# Then add to the runner service in ~/docker/gitea/compose.yaml:
|
||||
# volumes:
|
||||
# - /srv/gitea-workspace:/srv/gitea-workspace
|
||||
# Restart the runner container for the change to take effect.
|
||||
```
|
||||
|
||||
The path `/srv/gitea-workspace` is the canonical workspace root. It must be identical on the host and inside job containers — if the paths differ, Compose still resolves to the container-internal path, which the host daemon cannot find (the original bug).
|
||||
|
||||
**Disk management:** act_runner cleans per-run subdirectories on completion. Orphaned directories from interrupted runs accumulate under `/srv/gitea-workspace` and should be pruned manually if disk space becomes a concern:
|
||||
|
||||
```bash
|
||||
# List workspace directories older than 7 days
|
||||
find /srv/gitea-workspace -mindepth 3 -maxdepth 3 -type d -mtime +7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Running host-level commands from CI (nsenter pattern)
|
||||
|
||||
Job containers are unprivileged and do not share the host's PID/mount/network namespaces. Commands like `systemctl` that target the host daemon are therefore unavailable by default. When a workflow step needs to manage a host service (e.g. `systemctl reload caddy`), it uses the Docker socket to spin up a **privileged sibling container** in the host PID namespace:
|
||||
@@ -141,33 +108,6 @@ nsenter: failed to execute /bin/systemctl: No such file or directory
|
||||
|
||||
The first error means the Docker socket is not mounted into the job container — check `valid_volumes` in `/root/docker/gitea/runner-config.yaml` on the VPS. The second means the Alpine image is running but cannot enter the host mount namespace; verify `--privileged` and `--pid=host` are both present in the workflow step.
|
||||
|
||||
**Failure mode 4 — workspace bind-mount not configured (observability stack or any compose-with-file-mounts job)**
|
||||
|
||||
Symptom in CI log:
|
||||
```
|
||||
Error response from daemon: error while creating mount source path "…/prometheus/prometheus.yml": mkdir …: not a directory
|
||||
```
|
||||
|
||||
Or the service starts but immediately crashes because a config file was mounted as an empty directory.
|
||||
|
||||
Cause: `/srv/gitea-workspace` does not exist on the host, or the runner container's `compose.yaml` is missing the `- /srv/gitea-workspace:/srv/gitea-workspace` volume line.
|
||||
|
||||
Diagnosis:
|
||||
```bash
|
||||
ssh root@<vps>
|
||||
ls -la /srv/gitea-workspace # must exist and be a directory
|
||||
docker inspect gitea-runner | grep -A5 Mounts # must show /srv/gitea-workspace
|
||||
```
|
||||
|
||||
Recovery:
|
||||
```bash
|
||||
mkdir -p /srv/gitea-workspace
|
||||
# Add volume line to runner compose.yaml, then:
|
||||
docker compose -f ~/docker/gitea/compose.yaml up -d gitea-runner
|
||||
```
|
||||
|
||||
See `docs/DEPLOYMENT.md §3.1` and ADR-015 for the full setup rationale.
|
||||
|
||||
---
|
||||
|
||||
## Gitea vs GitHub Actions Differences
|
||||
|
||||
@@ -45,6 +45,7 @@ export default defineConfig(
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
|
||||
@@ -2,25 +2,15 @@
|
||||
container:
|
||||
# passed as DOCKER_HOST inside the job container
|
||||
docker_host: "unix:///var/run/docker.sock"
|
||||
# Job workspaces are stored here on the NAS and mounted at the same
|
||||
# absolute path inside job containers. Identical host ↔ container path
|
||||
# is the requirement: Docker Compose resolves relative bind mounts to
|
||||
# $(pwd) inside the job container and passes that absolute path to the
|
||||
# host daemon — the daemon must find the file at that exact host path.
|
||||
# Prerequisite: mkdir -p /srv/gitea-workspace on the host, and add
|
||||
# - /srv/gitea-workspace:/srv/gitea-workspace
|
||||
# to the runner service volumes in gitea's compose.yaml.
|
||||
workdir_parent: /srv/gitea-workspace
|
||||
# whitelists volumes that workflow steps may bind-mount
|
||||
# whitelists the socket path so workflows can mount it
|
||||
valid_volumes:
|
||||
- "/var/run/docker.sock"
|
||||
- "/srv/gitea-workspace"
|
||||
# appended to `docker run` when the runner spawns a job container
|
||||
# SECURITY: Mounting the Docker socket grants job containers root-equivalent
|
||||
# access to the host Docker daemon. Acceptable here because only trusted code
|
||||
# from this private repo runs on this runner. Do NOT use on a runner that
|
||||
# accepts untrusted PRs from external contributors.
|
||||
options: "-v /var/run/docker.sock:/var/run/docker.sock -v /srv/gitea-workspace:/srv/gitea-workspace"
|
||||
options: "-v /var/run/docker.sock:/var/run/docker.sock"
|
||||
# keep network mode default (bridge) — Testcontainers handles its own networking
|
||||
force_pull: false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user