Most Dockerfiles in production are larger, slower, and less secure than they need to be. They start from a full Ubuntu or Debian base image when a slim runtime would do. They install build dependencies that linger in the runtime image because the build is single-stage. They run as root because that was the path of least resistance during initial setup. They have a .dockerignore that ships node_modules into the build context, slowing every build by a factor of three. The image works. It also takes 90 seconds to push, holds the build cache wrong on most edits, and turns the team's CI pipeline into the bottleneck that everyone complains about and nobody schedules time to fix.
A production-ready Dockerfile is a different artifact. It uses multi-stage builds so the final image contains only the runtime, not the toolchain. It runs as a non-root user with the minimum capabilities the application needs. It uses a small base image (distroless, alpine, or slim) calibrated to the workload. It has a .dockerignore that excludes everything not needed for the build. It orders instructions so the cache is hit on routine edits and busted only on real changes. It has a corresponding Docker Compose for local development that matches the production behavior closely enough to debug confidently. The discipline is well-known and rarely applied past the initial Dockerfile because the cost of refactoring a working container is high. The /relay-docker skill is built to apply the discipline at the start: produce the Dockerfile correctly, before the suboptimal version ships.
Why generalist AI ships oversized Dockerfiles
Ask Cursor or ChatGPT for a Dockerfile for a Node.js application. You get something like FROM node:20, WORKDIR /app, COPY . ., RUN npm install, CMD ["npm", "start"]. The Dockerfile works. It also produces a 1.2GB image with the full Debian base, the npm cache, the dev dependencies, and the source files duplicated into the image because there is no .dockerignore. The container runs as root. The cache is busted on every source change because the COPY happens before the install. The output is technically correct and operationally bad, and a generalist tool cannot tell the difference because it cannot see how the image will be used.
The other failure mode is the local-versus-production gap. The Docker Compose file that the team uses for local development is often disconnected from the production Dockerfile: it uses different environment variables, different volume mounts, different networking. The disconnect is the source of the bugs that work locally and fail in production, which then take twice as long to diagnose because the local environment is no help. A generalist tool produces a Compose file that is correct in isolation; it does not produce one that mirrors the production Dockerfile, because that requires holding both artifacts together.
What a production Dockerfile actually requires
A production-ready Dockerfile has six properties. First, multi-stage build: a build stage with the toolchain, a runtime stage with only what the application needs. Second, a small base image: distroless or alpine for languages that support them, slim variants for everything else. Third, non-root user: a dedicated user with the minimum permissions, configured with USER before the entrypoint. Fourth, instruction ordering: dependency files copied first and installed in their own layer, source code copied last, so cache is preserved across source-only changes. Fifth, a .dockerignore that excludes node_modules, .git, build artifacts, IDE files, and tests. Sixth, a healthcheck so the container reports its readiness to whichever orchestrator is running it.
The Compose side has its own discipline. The Compose file mirrors the Dockerfile's runtime behavior: the same user, the same environment shape, the same port binding. Volume mounts are scoped tightly so local source changes are visible without leaking the host's tooling into the container. Networking is set up so service-to-service calls match the production names. The result is a local environment where bugs that appear in production also appear locally, and bugs that work locally also work in production. That is the point of containerizing for development, and it is what most teams half-implement.
How /relay-docker works
Step one: detect the language and runtime
Before generating any Dockerfile, /relay-docker reads the project to detect the language, the runtime, the package manager, and the build artifacts. Node.js with pnpm gets a different Dockerfile than Node.js with npm. Python with poetry gets a different one than Python with pip. Go applications get distroless because the binary is self-contained. Rust applications get the same. Java applications get a slim JRE rather than the full JDK. The detection drives the output.
Step two: build the multi-stage Dockerfile
The Dockerfile is built multi-stage by default. The build stage installs dependencies, runs the build, and produces the artifact. The runtime stage copies the artifact and only the runtime dependencies. The runtime user is created with a fixed UID and GID, the working directory is owned by the user, and the entrypoint runs as the user. The healthcheck is added based on the application type: a TCP probe for plain network services, an HTTP probe for HTTP applications, a custom probe if the project's framework provides one.
Step three: optimize the cache
Instructions are ordered for cache efficiency. The package manager files (package.json, lockfile, requirements.txt, pyproject.toml) are copied first, dependencies are installed in their own layer, and only then is the source code copied. The result: a routine source-only change rebuilds in seconds because the dependency layer is cached. A real dependency change busts the cache and rebuilds correctly. The .dockerignore is generated to match the runtime needs: every file the runtime image does not need is excluded from the build context, which makes the context small and the upload to the registry fast.
Step four: Docker Compose for local
Alongside the Dockerfile, the skill produces a docker-compose.yaml for local development. Compose uses the same Dockerfile target as production by default; volume mounts are configured for the source paths the developer edits. Service-to-service networking matches the production naming so the application code does not have a separate development codepath. Environment variables are loaded from a .env file with a documented .env.example so newcomers can spin up the stack without spelunking through configuration.
Distroless images (gcr.io/distroless) ship with no shell and almost no userspace. That is the point: less surface area means less to attack and less to keep updated. /relay-docker uses distroless for compiled languages where the binary is self-contained and falls back to slim for languages that need more runtime.
Tonone's /relay-docker skill produces production-ready Dockerfiles with multi-stage builds, security hardening, minimal images, and Docker Compose that matches production behavior.
When to use /relay-docker, and when not to
/relay-docker is the right call when containerizing a service for the first time, when an existing Dockerfile produces large or insecure images, or when the local Docker Compose setup is creating bugs that work locally and fail in production (or vice versa). The signal is when CI builds are slow because the image is large, when security scans flag the container as running root, or when the team can name local-versus-production divergence as a recurring source of debugging time.
Skip the skill for projects that do not need containers (a static site that deploys directly to a CDN). For full CI/CD pipeline configuration around the container, /relay-pipeline is the right call. For deployment configuration (Kubernetes manifests, deploy specs), /relay-deploy produces the full deploy artifact.
| Capability | Tonone | Generalist chatbot | Cursor / Copilot |
|---|---|---|---|
| Multi-stage build by default | Yes, build/runtime separation | Often single-stage | Whatever the SDK example does |
| Non-root runtime user | Yes, dedicated user with minimum perms | Often runs as root | Often runs as root |
| Small base image (distroless/slim/alpine) | Yes, calibrated to language | Often full base image | Whatever the prompt asks |
| .dockerignore generated | Yes, scoped to actual runtime needs | Often missing | Often missing |
| Docker Compose mirrors production | Yes, same target, same env, matched networking | Disconnected from production | Disconnected |
A worked example: Next.js Dockerfile
Suppose the brief is: containerize a Next.js application for production deploy. Run /relay-docker and the output is the multi-stage Dockerfile plus the Compose file.
# syntax=docker/dockerfile:1.7
# --- builder ---
FROM node:22-slim AS builder
ENV PNPM_HOME=/pnpm
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY pnpm-lock.yaml package.json ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
# --- runtime ---
FROM node:22-slim AS runtime
WORKDIR /app
# Non-root user
RUN groupadd -g 1001 nodejs && \
useradd -u 1001 -g nodejs -m -s /usr/sbin/nologin nextjs
# Copy only the runtime artifacts
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
CMD ["node", "server.js"]The image final size lands around 180MB compared to 1.2GB for the naive single-stage version. The container runs as a non-root user. Build cache is preserved across source-only changes thanks to the dependency layer ordering. The corresponding docker-compose.yaml reuses the runtime stage with development-specific volume mounts so local edits are reflected without rebuilding the image. That is the difference between a Dockerfile that ships and a Dockerfile that ages well.
Related skills
/relay-docker covers the container itself. For the CI/CD pipeline that builds and ships the container, /relay-pipeline is the right call. For deployment configuration that consumes the container (Kubernetes manifests, ECS task defs, Cloud Run services), /relay-deploy produces the full deploy spec.
Install
/relay-docker ships with the Relay agent in the Tonone for Claude Code package. Install Tonone, invoke /relay-docker from any Claude Code session, and the skill produces a production-ready Dockerfile and matching Compose for the project's stack.
1. Add to marketplace
2. Install Relay
Containers are the artifact that runs in production every minute the service is up. The skill is built so the artifact is correct on day one, not after the security audit forces a refactor.
Frequently asked questions
- What does /relay-docker do?
- It produces a production-ready Dockerfile with multi-stage builds, security hardening (non-root user), small base images, cache-aware instruction ordering, .dockerignore, healthcheck, and a Docker Compose file that matches the production Dockerfile for local development.
- What languages does /relay-docker support?
- Node.js, Python, Go, Rust, Java, Ruby, PHP, and Elixir are all supported with calibrated base images. Distroless is used for compiled languages where the binary is self-contained; slim variants are used for runtime languages.
- How is /relay-docker different from copying a Dockerfile from a tutorial?
- Tutorials produce single-stage Dockerfiles with full base images and root execution. /relay-docker produces multi-stage builds with non-root users, small bases, and the cache ordering that keeps CI fast.
- When should I use /relay-docker?
- When containerizing a service for the first time, when an existing Dockerfile is too large or runs as root, or when local Docker Compose is creating bugs that work locally and fail in production (or vice versa).
- Does /relay-docker generate Docker Compose?
- Yes. The Compose file mirrors the production Dockerfile so local development reduces the local-versus-production divergence. Volume mounts, networking, and environment shape match production.
- How do I install /relay-docker?
- Install Tonone for Claude Code via the get-started guide at tonone.ai/get-started. /relay-docker ships with the Relay agent and is invoked as a slash command in any Claude Code session. Tonone is free and MIT-licensed.
- Is /relay-docker free?
- Yes. The skill is part of Tonone, which is MIT-licensed. The only cost is Claude Code token usage during the work.
- Does /relay-docker support Docker Bake or buildx?
- Yes. The skill produces buildx-compatible Dockerfiles with cache mount syntax (`--mount=type=cache`) and supports Docker Bake config when the project uses it for multi-target builds.