Infrastructure
Infrastructure Standards
CI/CD Platform
Woodpecker CI — self-hosted on the tools VPS (Tailscale IP 100.81.122.65; no public subdomain confirmed in repo)
Container registry — Gitea at gitea.level147.net
Deployment — Docker Compose on target hosts via Woodpecker deploy agents
Woodpecker Agent Taxonomy
Naming convention
{role}-{location}-{backend}| Segment | Values | Notes |
|---|---|---|
role | build, deploy | What the agent does |
location | workstation, web, tools | Which physical host |
backend | docker, local | Woodpecker backend type |
Current agents
WOODPECKER_HOSTNAME | Host | Backend | Purpose |
|---|---|---|---|
build-workstation-docker | nerdstation (Windows workstation) | docker | Builds container images via Docker Desktop |
build-workstation-local | nerdstation (Windows workstation) | local | Bare-metal tasks (GPU, native tools) — future |
deploy-web-docker | web server | docker | Runs docker compose deploys via Docker socket |
Agent labels
Labels are how pipelines target agents. Use labels not hostnames in .woodpecker/*.yml.
# Build stepwhen: - event: push branch: developagent: labels: type: build
# Deploy stepagent: labels: type: deploy host: webStandard label keys:
| Key | Values | Purpose |
|---|---|---|
type | build, deploy | Primary role |
host | web, tools, workstation | Target host |
Volume requirement
Every agent compose must mount a named volume at /etc/woodpecker to persist the registration token. Without it, the agent re-registers as a new identity on every restart, creating ghost entries.
volumes: - woodpecker-agent-config:/etc/woodpecker
volumes: woodpecker-agent-config:Image pinning
Always pin agent image to the same major version as the server. Never use :next in compose files — use the explicit semver tag (e.g. v3.14.1).
Environment Naming
| Name | Branch | Image tag | Purpose |
|---|---|---|---|
staging | develop | :staging | Auto-deploy on push; integration testing |
prod | main | :prod | Manual gate via PR to main |
Never use production, development, or dev as environment names — these cause confusion with develop branch.
Docker Image Naming
gitea.level147.net/{owner}/{project}:{env}Examples:
gitea.level147.net/shaun/better:staginggitea.level147.net/shaun/level147-web:prod
Rules:
- All lowercase, hyphens not underscores
- Project name matches the Gitea repository name
- Tag is always the environment name (
stagingorprod), never a commit hash in the tag field (Woodpecker metadata provides traceability)
Docker Compose Conventions
Service / container naming
Service name = container name = human-readable role.
services: app: # not "better" or "web" container_name: better-staging # explicit, matches image name pattern worker: container_name: better-worker-staging redis: container_name: better-redisVolume naming
{service}-{purpose}Examples: woodpecker-agent-config, postgres-data, redis-data
Network naming
| Network | Purpose |
|---|---|
proxy-net | Traefik-proxied services (external: true on consumer stacks) |
app-net | Internal service-to-service within a project |
Pipeline File Structure
.woodpecker/├── verify.yml # lint, test, type-check (no external side effects)├── build.yml # docker build + push (depends_on: verify)└── deploy.yml # docker compose pull + up (depends_on: build)depends_on between pipeline files eliminates race conditions. The verify → build → deploy chain is mandatory — never merge build and deploy into one file.
Secrets Management
All secrets injected via Woodpecker repo secrets, never hardcoded in .woodpecker/*.yml.
Standard secret names across projects:
| Secret | Purpose |
|---|---|
GITEA_TOKEN | Registry push/pull (PAT with write:package) |
DEPLOY_HOST | SSH/socket target for deploy agent |
Build-time env vars passed via --build-arg must be declared in both the pipeline and the Dockerfile ARG block.
Host Inventory
| Hostname | Role | LAN IP | Tailscale IP |
|---|---|---|---|
| tools | CI server, Gitea, Portainer | 10.240.0.171 | 100.81.122.65 |
| web | App hosting, deploy agent | 10.240.0.68 | 100.101.148.89 |
| nerdstation | Workstation, build agent | — | 100.75.13.119 |
Internal services should use LAN IPs where possible. Tailscale IPs are used for cross-network access (e.g. workstation → tools gRPC). Never hardcode Tailscale IPs in production service configs — they change on device re-enrolment.
v0.1.0 · d0d7a20 · 2026-06-26