Docker mempermudah banyak hal, tapi jika tidak dikonfigurasi dengan benar, container bisa menjadi celah keamanan yang serius. Container bukanlah sandbox yang sepenuhnya terisolasi — ada banyak cara container bisa mempengaruhi host jika tidak diatur dengan tepat.
Di artikel ini kita bahas praktik keamanan Docker yang wajib diterapkan di production.
1. Jangan Jalankan Container sebagai Root
Secara default, proses di dalam container berjalan sebagai root (UID 0). Jika container berhasil di-escape, penyerang langsung punya akses root ke host.
Solusi: Definisikan non-root user di Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# Buat user non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Set kepemilikan file
RUN chown -R appuser:appgroup /app
# Beralih ke user non-root
USER appuser
EXPOSE 3000
CMD ["node", "index.js"]
Verifikasi:
# Cek user yang berjalan di dalam container
docker exec container-name whoami
# Output: appuser (bukan root)
2. Gunakan Image Base yang Minimal
Semakin banyak tool yang ada di image, semakin besar attack surface-nya.
# Kurang aman — Ubuntu penuh dengan banyak tool
FROM ubuntu:22.04
# Lebih baik — image slim
FROM node:20-slim
# Terbaik — Alpine, ukuran sangat kecil, minimal tools
FROM node:20-alpine
# Terbaik untuk production — distroless (hanya runtime, tanpa shell!)
FROM gcr.io/distroless/nodejs20-debian12
Distroless adalah image tanpa shell, package manager, atau tool OS apapun. Ideal untuk production karena penyerang tidak bisa mengeksekusi perintah meski berhasil masuk ke container.
# Multi-stage dengan distroless
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["dist/index.js"]
3. Jangan Simpan Secret di Image
Secret yang hardcode di Dockerfile atau environment variable akan tercatat di layer history image dan bisa dilihat siapapun yang punya akses ke image.
# Jangan lakukan ini — secret akan tersimpan di layer image
ENV API_KEY=rahasia-123
# Dan ini — akan terlihat di docker history
RUN curl -H "Authorization: Bearer rahasia-123" https://api.contoh.com
Solusi: Gunakan environment variable saat runtime, bukan saat build:
# Inject secret saat docker run
docker run -e JWT_SECRET=$JWT_SECRET blog-api
# Atau via file .env
docker run --env-file .env blog-api
Di Docker Compose:
services:
api:
env_file:
- .env # jangan commit file ini!
Untuk secret yang sangat sensitif di production, gunakan Docker Secrets (Swarm) atau solusi eksternal seperti HashiCorp Vault atau AWS Secrets Manager.
4. Read-Only Filesystem
Jalankan container dengan filesystem read-only. Ini mencegah penyerang menulis file berbahaya ke dalam container:
docker run --read-only blog-api
Di Compose:
services:
api:
read_only: true
tmpfs:
- /tmp # beri akses tulis ke /tmp jika dibutuhkan app
- /var/run
5. Batasi Capabilities Linux
Container secara default punya beberapa Linux capabilities yang tidak selalu dibutuhkan. Buang semua dan tambahkan hanya yang benar-benar diperlukan:
services:
api:
cap_drop:
- ALL # buang semua capabilities
cap_add:
- NET_BIND_SERVICE # tambahkan kembali hanya yang dibutuhkan
# (diperlukan untuk bind ke port < 1024)
6. Batasi Resource (CPU dan Memory)
Tanpa batasan resource, satu container yang bermasalah bisa menghabiskan semua resource host dan menyebabkan container lain crash (noisy neighbor problem):
services:
api:
deploy:
resources:
limits:
cpus: '1.0' # maksimal 1 CPU core
memory: 512M # maksimal 512MB RAM
reservations:
cpus: '0.25'
memory: 128M
Atau dengan docker run:
docker run \
--cpus="1.0" \
--memory="512m" \
--memory-swap="512m" \ # nonaktifkan swap
blog-api
7. Scan Image untuk Vulnerability
Scan image secara rutin untuk menemukan CVE (Common Vulnerabilities and Exposures) yang diketahui di dependencies:
# Docker Scout (built-in di Docker Desktop)
docker scout cves blog-api:latest
# Trivy — open source, sangat komprehensif
trivy image blog-api:latest
# Grype — alternatif Trivy
grype blog-api:latest
Integrasikan scanning ke CI/CD pipeline:
# GitHub Actions
- name: Scan image dengan Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/username/blog-api:latest'
format: 'table'
exit-code: '1' # gagalkan pipeline jika ada critical CVE
severity: 'CRITICAL,HIGH'
8. Network Security
services:
api:
networks:
- public # terima request dari luar
db:
networks:
- internal # hanya bisa diakses dari api
networks:
public:
internal:
internal: true # tidak punya akses internet keluar
9. Update Image Secara Berkala
Image yang jarang di-update akan menumpuk vulnerability dari base image dan dependencies. Jadwalkan update rutin:
# Cek apakah ada update untuk image yang digunakan
docker pull postgres:16-alpine
# Build ulang semua image
docker compose build --pull --no-cache
Checklist Security Docker Production
- Container tidak berjalan sebagai root
- Menggunakan image minimal (Alpine atau Distroless)
- Tidak ada secret hardcode di Dockerfile atau image
- Filesystem read-only di mana memungkinkan
- Linux capabilities di-drop semua, tambah hanya yang perlu
- Batasan CPU dan memory sudah dikonfigurasi
- Image di-scan vulnerability secara reguler
- Network segmentasi sudah diterapkan (database tidak expose ke internet)
- Image di-update secara berkala
Kesimpulan
Keamanan Docker bukan sesuatu yang ditambahkan belakangan — harus menjadi bagian dari workflow sejak awal. Dengan menjalankan container sebagai non-root, menggunakan image minimal, dan membatasi resource serta capabilities, permukaan serangan menjadi jauh lebih kecil.
Di artikel berikutnya, kita bahas Docker dengan database — cara terbaik mengelola PostgreSQL dan MySQL di dalam container untuk production.
Kiki/🎮🍉⌨️🍩💻