Teams waste time fighting environment drift between local dev and CI. Scripts scatter across repos, CI has hidden steps, and debugging becomes trial-and-error. We fix this by making Taskfile the single source of truth and running everything in secure containers — locally and in CI.
- Teams with complex build/test pipelines
- Infrastructure-heavy projects (AWS, Terraform, CD)
- Organizations wanting reproducibility and quick onboarding
- Reproducibility — same Task runs identically everywhere
- Isolation — no host dependencies beyond Docker + Task
- Security by default — non-root, dropped caps, no-new-privileges
- Transparency — no CI-only magic; everything in
Taskfile.yml - Separation of concerns — CI (dev tasks) in Taskfile; CD (deploy) in Actions
--cap-drop=ALL— no privileged capabilities--security-opt no-new-privileges— prevent privilege escalation--user $(id -u):$(id -g)— non-root execution--workdir /workspace— consistent working directory- Project mount only:
/workspace(read-write)
Pull image once (quiet):
_docker/pull:
internal: true
cmds:
- |
if ! docker image inspect "{{.IMAGE}}" >/dev/null 2>&1; then
docker pull -q "{{.IMAGE}}" >/dev/null 2>&1 || {
echo "Failed to pull image: {{.IMAGE}}"
exit 1
}
fi
silent: true
requires:
vars: [IMAGE]Run securely (never pull during execution):
_docker/run:
internal: true
dir: "{{.git_root}}"
deps:
- task: _docker/pull
vars: { IMAGE: "{{.IMAGE}}" }
cmd: |
docker run --rm --init --pull=never {{if .TTY}}-it{{end}} \
--cap-drop=ALL \
--security-opt no-new-privileges \
--user $(id -u):$(id -g) \
--workdir /workspace \
{{if .ENVS}}{{range $e := .ENVS}}--env {{$e}} {{end}}{{end}}\
{{if .PORTS}}{{range $p := .PORTS}}--publish {{$p}} {{end}}{{end}}\
{{if .VOLUMES}}{{range $v := .VOLUMES}}--volume {{$v}} {{end}}{{end}}\
--volume "{{.git_root}}/{{.MOUNT_DIR}}:/workspace:rw" \
"{{.IMAGE}}" \
{{.CMD}}
silent: true
requires:
vars: [IMAGE, CMD, MOUNT_DIR]| Variable | Purpose | Example |
|---|---|---|
IMAGE |
Docker image (pin in CI) | node:20, golang:1.22@sha256:... |
CMD |
Command to execute | sh -c 'npm ci && npm test' |
MOUNT_DIR |
Project directory to mount | ".", "site", "infra" |
ENVS |
Environment variables | ["GOOS=linux","NPM_CONFIG_CACHE=/cache"] |
PORTS |
Port mappings for dev servers | ["3000:3000"] |
VOLUMES |
Additional mounts | ["$HOME/.ssh:/ssh:ro"] |
TTY |
Interactive mode | "true" for dev servers |
lint:
desc: "Run code linting"
cmds:
- task: _docker/run
vars:
IMAGE: "node:20"
MOUNT_DIR: "."
ENVS: ["NPM_CONFIG_CACHE=/workspace/.cache"]
CMD: "sh -c 'npm ci && npx eslint .'"
test:
desc: "Run test suite"
cmds:
- task: _docker/run
vars:
IMAGE: "golang:1.22"
MOUNT_DIR: "."
CMD: "go test ./..."
dev:
desc: "Development server with hot reload"
cmds:
- task: _docker/run
vars:
IMAGE: "node:20"
MOUNT_DIR: "."
PORTS: ["3000:3000"]
TTY: "true"
CMD: "sh -c 'npm ci && npm run dev -- --host 0.0.0.0'"Use the same tasks in GitHub Actions:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Mad-Pixels/github-workflows/actions/taskfile-runner@v1
with:
command: "lint"
- uses: Mad-Pixels/github-workflows/actions/taskfile-runner@v1
with:
command: "test"| ✅ Include in Taskfile | ❌ Keep in Actions |
|---|---|
| Code linting/formatting | AWS deployments |
| Unit/integration tests | Infrastructure provisioning |
| Building/compilation | Production secrets handling |
| Development servers | Cloud resource management |
| Static analysis | Terraform apply operations |
- 💰 Reduced maintenance — no more "works on my machine"
- 🚀 Faster delivery — fewer environment-specific bugs
- 🔐 Stronger security — containerized, non-root execution
- 📋 Clear audit trails — Git history = deployment history
- 🧠 Better onboarding — new engineers only need Docker and Task
If it works locally, it works in CI — guaranteed:
- Same Docker images and commands
- Same environment variables and mounts
- No CI-specific scripts or hidden steps
- Debug locally, not through failed pipelines
- Pin images by digest in CI for determinism:
node:20@sha256:... - Use project-local caches under
/workspace/.cache - Mount read-only where possible:
["$HOME/.ssh:/ssh:ro"] - Validate inputs in task descriptions and error messages
- Keep secrets out of Taskfile — handle via Actions only
| Name | Description |
|---|---|
| about | VueJS static site with containerized build and AWS deployment |
| dyno-doc | VueJS && Vitepress static site with docs |