const uid = process.getuid();
const username = uid === 0 ? 'root -- DANGER' : `non-root (uid: ${uid})`;
const uid = process.getuid();
const username = uid === 0 ? 'root -- DANGER' : `non-root (uid: ${uid})`;
const uid = process.getuid();
const username = uid === 0 ? 'root -- DANGER' : `non-root (uid: ${uid})`;
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
# CORRECT — npm install only re-runs when package.json changes
COPY package.json ./
RUN npm install
COPY . .
# CORRECT — npm install only re-runs when package.json changes
COPY package.json ./
RUN npm install
COPY . .
# CORRECT — npm install only re-runs when package.json changes
COPY package.json ./
RUN npm install
COPY . .
# WRONG — any code change forces a full reinstall
COPY . .
RUN npm install
# WRONG — any code change forces a full reinstall
COPY . .
RUN npm install
# WRONG — any code change forces a full reinstall
COPY . .
RUN npm install
curl http://localhost:3000
curl http://localhost:3000
curl http://localhost:3000
App is running
User ID : 0
Running as : root -- DANGER
App is running
User ID : 0
Running as : root -- DANGER
App is running
User ID : 0
Running as : root -- DANGER
docker exec -it $(docker ps -q) sh
cat /app/.env
docker exec -it $(docker ps -q) sh
cat /app/.env
docker exec -it $(docker ps -q) sh
cat /app/.env
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
DATABASE_PASSWORD=super_secret_password_123
API_KEY=sk-live-abc123xyz789
STRIPE_SECRET=rk_live_do_not_share_this
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY . .
RUN npm install
RUN chown -R appuser:appuser /app
USER appuser
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY . .
RUN npm install
RUN chown -R appuser:appuser /app
USER appuser
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY . .
RUN npm install
RUN chown -R appuser:appuser /app
USER appuser
App is running
User ID : 999
Running as : non-root (uid: 999)
App is running
User ID : 999
Running as : non-root (uid: 999)
App is running
User ID : 999
Running as : non-root (uid: 999)
echo "test" > /etc/passwd # Permission denied
apt-get install curl # Permission denied (lock file)
touch /bin/backdoor # Permission denied
echo "test" > /etc/passwd # Permission denied
apt-get install curl # Permission denied (lock file)
touch /bin/backdoor # Permission denied
echo "test" > /etc/passwd # Permission denied
apt-get install curl # Permission denied (lock file)
touch /bin/backdoor # Permission denied
.env
.env.*
*.pem
*.key
*.cert
.git
node_modules
Dockerfile*
README.md
.env
.env.*
*.pem
*.key
*.cert
.git
node_modules
Dockerfile*
README.md
.env
.env.*
*.pem
*.key
*.cert
.git
node_modules
Dockerfile*
README.md
docker run --rm secure-copy-app find /app -type f
docker run --rm secure-copy-app find /app -type f
docker run --rm secure-copy-app find /app -type f
/app/app.js
/app/package.json
/app/package-lock.json
/app/app.js
/app/package.json
/app/package-lock.json
/app/app.js
/app/package.json
/app/package-lock.json
docker run --rm secure-copy-app cat /app/.env
# cat: /app/.env: No such file or directory
docker run --rm secure-copy-app cat /app/.env
# cat: /app/.env: No such file or directory
docker run --rm secure-copy-app cat /app/.env
# cat: /app/.env: No such file or directory
# Stage 1: builder — uses the full Node.js image
FROM node:20 AS builder
WORKDIR /build
COPY package.json ./
RUN npm install
COPY app.js . # Stage 2: runtime — uses a minimal image
FROM node:20-slim
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /build/app.js ./app.js
COPY --from=builder /build/node_modules ./node_modules
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 3000
CMD ["node", "app.js"]
# Stage 1: builder — uses the full Node.js image
FROM node:20 AS builder
WORKDIR /build
COPY package.json ./
RUN npm install
COPY app.js . # Stage 2: runtime — uses a minimal image
FROM node:20-slim
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /build/app.js ./app.js
COPY --from=builder /build/node_modules ./node_modules
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 3000
CMD ["node", "app.js"]
# Stage 1: builder — uses the full Node.js image
FROM node:20 AS builder
WORKDIR /build
COPY package.json ./
RUN npm install
COPY app.js . # Stage 2: runtime — uses a minimal image
FROM node:20-slim
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /build/app.js ./app.js
COPY --from=builder /build/node_modules ./node_modules
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 3000
CMD ["node", "app.js"]
npm --version # sh: npm: not found
curl --version # sh: curl: not found
git --version # sh: git: not found
apt-get # sh: apt-get: not found
npm --version # sh: npm: not found
curl --version # sh: curl: not found
git --version # sh: git: not found
apt-get # sh: apt-get: not found
npm --version # sh: npm: not found
curl --version # sh: curl: not found
git --version # sh: git: not found
apt-get # sh: apt-get: not found
docker run \ --read-only \ --tmpfs /tmp \ --memory="128m" \ --cpus="0.5" \ --cap-drop=ALL \ --security-opt no-new-privileges:true \ -p 3000:3000 \ multistage-app
docker run \ --read-only \ --tmpfs /tmp \ --memory="128m" \ --cpus="0.5" \ --cap-drop=ALL \ --security-opt no-new-privileges:true \ -p 3000:3000 \ multistage-app
docker run \ --read-only \ --tmpfs /tmp \ --memory="128m" \ --cpus="0.5" \ --cap-drop=ALL \ --security-opt no-new-privileges:true \ -p 3000:3000 \ multistage-app
docker exec -it $(docker ps -q) sh -c "echo test > /app/hacked.txt"
# sh: /app/hacked.txt: Read-only file system
docker exec -it $(docker ps -q) sh -c "echo test > /app/hacked.txt"
# sh: /app/hacked.txt: Read-only file system
docker exec -it $(docker ps -q) sh -c "echo test > /app/hacked.txt"
# sh: /app/hacked.txt: Read-only file system
docker inspect $(docker ps -q) | grep -E '"Memory"|"NanoCpus"'
# "Memory": 134217728, (exactly 128 MB)
# "NanoCpus": 500000000, (exactly 0.5 CPU cores)
docker inspect $(docker ps -q) | grep -E '"Memory"|"NanoCpus"'
# "Memory": 134217728, (exactly 128 MB)
# "NanoCpus": 500000000, (exactly 0.5 CPU cores)
docker inspect $(docker ps -q) | grep -E '"Memory"|"NanoCpus"'
# "Memory": 134217728, (exactly 128 MB)
# "NanoCpus": 500000000, (exactly 0.5 CPU cores) - Application runs as root
- Credentials are baked into the image
- Layer caching breaks on every code change