docker-deployment
Skill7 starsChanged 20 days agoMITView on GitHub
Production-ready Docker configurations, multi-stage builds, and deployment best practices
- Reads credentials
- Deletes or force-pushes
- Installs packages
4 other projects use a version of this.
Adopted by
Most recently taken up by
- bingyue/fde-skillstheir own version, first seen Oct 13, 2026
- zhengliangliahu-cloud/skillstheir own version, first seen Oct 11, 2026
- fossasia/eventyaytheir own version, first seen Oct 8, 2026
- cohen-liel/hivemindtheir own version, first seen Mar 17, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- fossasia/eventyay★ 1.7ksame name
- cohen-liel/hivemind★ 108same name
- bingyue/fde-skills★ 5same name
- zhengliangliahu-cloud/skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- api-contract-testingSkill
- browser-testingSkill
- ci-cd-templatesSkill
- code-review-checklistSkill
- context-escalationSkill
- deep-analysisSkill
- deep-readSkill
- deliverySkill
- deployment-runbookSkill
- divergeSkill
- executeSkill
- experiment-loopSkill
- git-workflowSkill
- handoffSkill
- infrastructure-as-codeSkill
- investigateSkill
- kubernetes-opsSkill
- library-docsSkill
- multi-agent-orchestrationSkill
- observability-stackSkill
- remote-triggersSkill
- repo-contextSkill
- scheduled-tasksSkill
- security-scanSkill
- skill-creatorSkill
- sre-runbooksSkill
- ui-guidelinesSkill
- verified-handoffSkill
- worktree-workflowSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 542
Verify rather than assume — a hardening flag that silently failed to apply is the same as not setting it:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Docker Deployment Skill
Provides production-ready Docker configurations, multi-stage builds, and deployment best practices.
Purpose
This skill provides:
- Optimized Dockerfile patterns for different tech stacks
- Multi-stage build strategies
- Docker Compose configurations
- Container security best practices
- Docker registry integration
- Health checks and monitoring
When to Use
- "Create a Dockerfile for Node.js app"
- "Optimize Docker image size"
- "Set up Docker Compose for microservices"
- "Implement Docker health checks"
- "Deploy with Docker Swarm/Kubernetes"
Node.js Dockerfile (Multi-Stage Build)
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies (including dev dependencies for build)
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Prune dev dependencies
RUN npm prune --production
# Production stage
FROM node:20-alpine AS production
# Add non-root user for security
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy built artifacts and production dependencies
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
# Use non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Start application
CMD ["node", "dist/index.js"]Next.js Dockerfile
Prerequisite — output: 'standalone'. Next.js only emits .next/standalone when the app
opts into standalone output. On a default Next app that directory does not exist and the
COPY /app/.next/standalone below fails the build with
"/app/.next/standalone": not found. Set this in next.config.js before building:
// next.config.js
module.exports = {
output: 'standalone',
}FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Requires `output: 'standalone'` in next.config.js — see prerequisite above
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]Python FastAPI Dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
# Install into a virtualenv, NOT `pip install --user`.
# `--user` installs to /root/.local, and /root is mode 0700 — after the runtime
# stage switches to a non-root USER the interpreter cannot even traverse it, and
# the container dies on startup with "Permission denied" on the entrypoint.
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
# Copy requirements
COPY requirements.txt .
# Install Python dependencies into the virtualenv
RUN pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.11-slim
WORKDIR /app
# Create the non-root user first, so everything below is copied in already owned by it
RUN useradd -m -u 1001 appuser
# Copy the virtualenv from builder, owned by the user that will actually run it
COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
# Copy application code
COPY --chown=appuser:appuser . .
# Put the virtualenv first on PATH so `python` / `uvicorn` resolve to it
ENV PATH=/opt/venv/bin:$PATH
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Docker Compose - Full Stack Application
Two things this template does deliberately, because the checklist below demands them:
credentials are mounted as secret files under /run/secrets/ rather than passed as
environment variables, and every service declares CPU/memory limits
(deploy.resources.limits is honoured by docker compose up in Compose v2, not just Swarm).
Create every host path this file binds — with the right TYPE — before docker compose up.
Compose resolves env_file eagerly and aborts the whole stack if the path is missing, so an
absent api/.env fails before a single container starts. The remaining bind sources fail later
and worse: Docker auto-creates a missing bind source as a directory, so every source that is
meant to be a file gets a directory instead and the container dies with
not a directory: Are you trying to mount a directory onto a file (or vice-versa)?. That failure
is sticky — the bogus directory outlives the failed run, so even writing the correct file
afterwards fails with is a directory and every retry reproduces the same error until it is
removed. Measured on Compose v5.1.2 / Engine 29.4.0, docker compose config exits 0 with all
four of these missing, so it does not work as the pre-flight check.
This file binds six host paths — four files and two directories. Miss any one and the stack does not come up:
| Host source | Type | Bound by |
./secrets/postgres_password.txt | file | secrets.postgres_password → /run/secrets/postgres_password |
./api/.env | file | api env_file |
./init-db.sql | file | postgres → /docker-entrypoint-initdb.d/init.sql |
./nginx.conf | file | nginx → /etc/nginx/nginx.conf |
./api/uploads | directory | api → /app/uploads |
./ssl | directory | nginx → /etc/nginx/ssl |
depends_on gates on start, not on readiness — unless you ask for health. A bare
depends_on: [api] means condition: service_started: the dependent is released the moment
the container exists, not when it can serve. Only condition: service_healthy waits, and it
is only as truthful as the dependency's own healthcheck — a probe that exits 0 on a 503
reports healthy and releases dependents against a dead service. Measured on Docker Compose
v5.1.2 / Engine 29.4.0: service_healthy against a dependency that declares no healthcheck
is not silently downgraded — Compose fails closed with
dependency failed to start: container ... has no healthcheck configured. So the hazard here
is never a missing healthcheck; it is a lying one. That is why the api probe below uses
curl -f.
# 1. Secret — chmod 0600, gitignored, never baked into an image.
# Generate ONLY if absent. This block doubles as the repair procedure for a
# half-created stack, so it gets re-run — and an unguarded `>` would mint a new
# password while the existing `postgres-data` volume still holds the old one.
# Postgres only reads POSTGRES_PASSWORD when it initialises an EMPTY data
# directory, so the app then fails auth while `pg_isready` still reports healthy.
mkdir -p secrets
if [ ! -s secrets/postgres_password.txt ]; then
openssl rand -base64 32 > secrets/postgres_password.txt
fi
chmod 0600 secrets/postgres_password.txt
# 2. The directory-type bind sources (this also creates ./api for the env file below).
mkdir -p api/uploads ssl
# 3. The file-type bind sources. `rmdir` undoes a directory left behind by an earlier
# failed run and — unlike `rm -rf` — refuses to delete anything that is not empty.
for f in api/.env init-db.sql; do
if [ -d "$f" ]; then rmdir "$f"; fi
[ -e "$f" ] || : > "$f"
done
# 4. nginx.conf must be a VALID config, not merely present: nginx exits with
# `[emerg] no "events" section in configuration` on an empty file, so `touch` here
# only trades one startup failure for another. This placeholder starts clean —
# replace it with your real proxy rules. Do not add an `upstream` block until those
# services are actually up: nginx resolves upstream hostnames at startup and exits
# with `host not found in upstream "frontend:3000"` when they are not.
if [ -d nginx.conf ]; then rmdir nginx.conf; fi
if [ ! -s nginx.conf ]; then
cat > nginx.conf <<'NGINX'
events {}
http {
server {
listen 80;
location / { return 200 "nginx up - replace ./nginx.conf with your real config\n"; }
}
}
NGINX
fi
# 5. Assert what actually breaks. `docker compose config` does not check any of this.
rc=0
for p in secrets/postgres_password.txt api/.env init-db.sql nginx.conf; do
[ -f "$p" ] || { echo "NOT A FILE: $p"; rc=1; }
done
for p in api/uploads ssl; do
[ -d "$p" ] || { echo "NOT A DIR: $p"; rc=1; }
done
[ "$rc" -eq 0 ] && echo "all bind sources present with the right type"Recovering a stack that already failed this way takes more than deleting the directories.
The nginx failure is the kind one — it is loud, and re-running the block above repairs it. The
postgres one is silent: its entrypoint runs /docker-entrypoint-initdb.d/* only against an
empty PGDATA, so the bad run initialised the database, errored with
psql: ... could not read from input file: Is a directory, and restart: unless-stopped brought
it straight back up reporting healthy — with the schema never applied. Measured: after
replacing the directory with real SQL and up -d --force-recreate, the seeded table was still
absent; only docker compose down -v (which drops the postgres-data volume) let it apply. A
green docker compose ps is not evidence that your init script ran.
services:
# Frontend (Next.js)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://api:4000
depends_on:
api:
condition: service_healthy
networks:
- app-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
memory: 256M
# Backend API (Node.js)
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "4000:4000"
# Non-secret config only. Keep this file gitignored and .dockerignore'd.
env_file:
- ./api/.env
environment:
- REDIS_URL=redis://redis:6379
- NODE_ENV=production
# No credentials in the DSN. The app reads the password from the mounted
# secret file and assembles the connection string at startup.
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_NAME=myapp
- DATABASE_USER=postgres
- DATABASE_PASSWORD_FILE=/run/secrets/postgres_password
secrets:
- postgres_password
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Only exit code 0 is healthy. `-f` makes curl exit 22 on any >= 400 response,
# `-S` keeps the reason on stderr for `docker inspect`, and `|| exit 1`
# normalises every failure to one documented value. A probe WITHOUT `-f`
# (plain `curl -s`, or an http.get with no status check) exits 0 on a 503 —
# Compose then marks this container healthy and releases `frontend` against a
# dead API. See the `depends_on` note above.
#
# curl must exist INSIDE the api image: node:*-alpine ships busybox wget only
# and python:*-slim ships neither. Either add `RUN apk add --no-cache curl` to
# the api Dockerfile, or probe with a runtime already in the image, e.g.
# ["CMD","node","-e","require('http').get('http://localhost:4000/health',r=>process.exit(r.statusCode===200?0:1))"]
# `CMD-SHELL` also needs a shell, so distroless images must use the exec form.
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:4000/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
networks:
- app-network
restart: unless-stopped
volumes:
- ./api/uploads:/app/uploads
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
memory: 512M
# PostgreSQL Database
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_DB=myapp
# The official image reads the password from this file instead of POSTGRES_PASSWORD
- POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
secrets:
- postgres_password
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
networks:
- app-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
memory: 1G
# Redis Cache
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- app-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
memory: 256M
# Nginx Reverse Proxy
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
# Bare list = `condition: service_started`: nginx starts as soon as these
# containers exist, NOT when they can serve. Neither declares a healthcheck,
# so `service_healthy` cannot be used here until one is added — Compose fails
# closed on a healthcheck-less dependency (see the depends_on note above).
- frontend
- api
networks:
- app-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
memory: 128M
networks:
app-network:
driver: bridge
volumes:
postgres-data:
redis-data:
secrets:
# Local/dev: a gitignored file, mounted read-only at /run/secrets/postgres_password.
# Swarm or a managed platform: swap for `external: true` and a real secret store
# (Vault, AWS Secrets Manager, SOPS) — never commit the value.
postgres_password:
file: ./secrets/postgres_password.txtDocker Security Best Practices
Minimal Base Image
# Use distroless for minimal attack surface
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/index.js"]Multi-Layer Security
FROM node:20-alpine
# Security: Run as non-root
RUN addgroup -g 1001 app && \
adduser -D -u 1001 -G app app
WORKDIR /app
# Security: Use specific versions
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# Security: Set file permissions
COPY --chown=app:app . .
# The writable data dir must exist AND be owned by `app` before USER, because a
# named volume inherits the ownership of the image path it is mounted over. Skip
# this and the volume lands root-owned and the non-root process cannot write to it.
RUN mkdir -p /app/data && chown app:app /app/data
# Security: Run as the non-root user created above.
# This does NOT drop Linux capabilities. Measured: with only `USER app` the
# bounding set is unchanged (CapBnd 00000000a80425fb); running as non-root merely
# empties the *effective* set, and the container can regain those caps via a
# setuid or file-capability binary. Dropping them is a runtime control — see below.
USER app
# Declares the writable data dir. VOLUME does NOT make the filesystem read-only.
# Measured on this image with no runtime flags, the app user can still write /tmp,
# /var/tmp and /dev/shm — and because `COPY --chown=app:app` handed it ownership of
# its own code it can overwrite /app/index.js and persist across a restart.
# A read-only rootfs is a runtime control — see below.
VOLUME ["/app/data"]
EXPOSE 3000
CMD ["node", "index.js"]Capability dropping and a read-only rootfs cannot be set in a Dockerfile. They are runtime controls, so a Dockerfile comment claiming them delivers nothing. Apply them where the container is actually started, or they are not in effect:
# docker compose — verified to yield CapBnd 0000000000000000, NoNewPrivs 1,
# a read-only rootfs, and a writable /app/data + /tmp
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
read_only: true
tmpfs: ["/tmp"]
volumes:
- appdata:/app/data# docker run equivalent
docker run --cap-drop=ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp -v appdata:/app/data myapp:latestVerify rather than assume — a hardening flag that silently failed to apply is the same as not setting it:
docker run --rm --cap-drop=ALL --security-opt no-new-privileges:true --read-only myapp:latest \
sh -c 'grep -E "^(CapBnd|NoNewPrivs)" /proc/self/status'
# Expect: CapBnd 0000000000000000 and NoNewPrivs 1.dockerignore
# Dependencies
node_modules
npm-debug.log
# Testing
coverage
.jest
*.test.js
# Environment & secrets
.env
.env.local
.env.*.local
secrets/
# Git
.git
.gitignore
# CI/CD
.github
.gitlab-ci.yml
# Documentation
README.md
docs/
# Build artifacts
dist
build
*.log
# IDE
.vscode
.idea
*.swpDocker Registry & CI/CD
GitHub Container Registry
# .github/workflows/docker-publish.yml
name: Docker Build & Push
on:
push:
branches: [ main ]
tags: [ 'v*' ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxHealth Checks
Node.js Health Endpoint
// health.ts
export function setupHealthCheck(app: Express) {
app.get('/health', async (req, res) => {
const checks = {
uptime: process.uptime(),
timestamp: Date.now(),
database: await checkDatabase(),
redis: await checkRedis(),
memory: process.memoryUsage(),
}
const isHealthy = checks.database && checks.redis
res.status(isHealthy ? 200 : 503).json(checks)
})
}
async function checkDatabase(): Promise<boolean> {
try {
await db.raw('SELECT 1')
return true
} catch {
return false
}
}
async function checkRedis(): Promise<boolean> {
try {
await redis.ping()
return true
} catch {
return false
}
}Monitoring & Logging
Docker Compose with Logging
services:
app:
image: myapp:latest
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels:
- "prometheus.scrape=true"
- "prometheus.port=9090"Prometheus Metrics Endpoint
// metrics.ts
import promClient from 'prom-client'
const register = new promClient.Registry()
promClient.collectDefaultMetrics({ register })
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'status_code'],
buckets: [10, 50, 100, 500, 1000, 5000],
})
register.registerMetric(httpRequestDuration)
export function setupMetrics(app: Express) {
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType)
res.end(await register.metrics())
})
}Best Practices Checklist
- ✅ Use multi-stage builds to minimize image size
- ✅ Run containers as non-root user
- ✅ Use specific base image versions (not
latest) - ✅ Implement health checks
- ✅ Set resource limits (CPU/memory)
- ✅ Use
.dockerignoreto exclude unnecessary files - ✅ Scan images for vulnerabilities (Snyk, Trivy)
- ✅ Use secrets management (not env vars for sensitive data)
- ✅ Implement proper logging
- ✅ Add monitoring and metrics
Integration with Agents
Works best with:
- devops-automation agent - Generates Docker configs
- security-auditor agent - Scans for container vulnerabilities
- performance-optimizer agent - Optimizes image size and startup time
Tools & Resources
- Docker: Official container platform
- Docker Compose: Multi-container orchestration
- Trivy: Vulnerability scanner
- Dive: Image layer analyzer
- Hadolint: Dockerfile linter
References
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

