Port Mapping & Exposure #
Salah satu konsep yang paling sering dipakai tapi juga paling sering disalahpahami di Docker adalah port mapping dan port exposure. Banyak developer bisa menjalankan container dengan -p 8080:80, tapi tidak benar-benar memahami apa bedanya dengan EXPOSE 80 di Dockerfile, dan apa yang sebenarnya terjadi di level iptables saat port mapping aktif.
Kesalahan umum yang sering terjadi: -p 8080:80 ditulis tanpa sengaja expose database ke publik. Atau EXPOSE dianggap sudah cukup untuk membuat port bisa diakses. Atau -p 80 dipakai tanpa sadar bind ke 0.0.0.0 (semua interface). Semua ini berakar dari pemahaman yang setengah-setengah tentang dua konsep yang sebenarnya berbeda.
Artikel ini membahas port mapping dan exposure secara mendalam: definisi yang tepat, perbedaan EXPOSE dan -p, sintaks lengkap, apa yang terjadi di level kernel, dan pola keamanan yang benar. Setelah membaca artikel ini, kamu akan tahu persis kapan harus pakai EXPOSE, kapan harus pakai -p, dan bagaimana menghindari jebakan umum yang sering jadi sumber bug keamanan.
Apa Itu Port Exposure? #
Port exposure adalah cara container mendeklarasikan port yang digunakan oleh aplikasinya. Ini adalah metadata — bukan mekanisme yang benar-benar membuka akses.
# Dockerfile
FROM nginx:alpine
EXPOSE 80
EXPOSE 80 artinya: “aplikasi di dalam image ini listen di port 80”. Tapi ini TIDAK:
- Membuka port 80 ke host.
- Membuat port 80 bisa diakses dari luar container.
- Mengubah konfigurasi iptables atau firewall.
EXPOSE adalah kontrak metadata antara image dan user (atau tool). Fungsinya:
| Fungsi | Penjelasan |
|---|---|
| Dokumentasi | Orang yang baca Dockerfile tahu port mana yang dipakai aplikasi |
| Tooling hint | Docker Compose, Kubernetes, atau tool lain bisa auto-detect |
| Image metadata | docker inspect menampilkan exposed ports |
# Lihat EXPOSE dari image
docker inspect nginx --format '{{json .Config.ExposedPorts}}'
# {"80/tcp":{}}
# Lihat container yang sedang jalan - mana yang ter-expose
docker ps --format '{{.Names}}: {{.Ports}}'
EXPOSEtanpa-p= port tidak bisa diakses dari host. Ini misconception paling umum. Banyak orang mengiraEXPOSEsudah cukup. Faktanya, tanpa-p(atau--publish), port yang di-EXPOSE hanya bisa diakses dari container lain di network yang sama — bukan dari host atau internet.
Apa Itu Port Mapping? #
Port mapping (atau port publishing) adalah mekanisme yang sebenarnya membuka akses dari host ke container. Docker menggunakan iptables DNAT untuk meneruskan traffic dari port host ke port container.
docker run -d --name web -p 8080:80 nginx
Artinya: semua traffic yang masuk ke host port 8080 akan diteruskan ke container port 80.
flowchart LR
U[User / Client] -->|localhost:8080| H[Host: 0.0.0.0:8080]
H -->|iptables DNAT| IPT[iptables NAT]
IPT -->|172.17.0.2:80| CT[Container: 80]
CT -->|Response| IPT
IPT -->|SNAT| H
H --> U
Karakteristik port mapping:
- Benar-benar membuka akses — tidak hanya metadata.
- Menggunakan iptables DNAT — paket TCP yang masuk ke port host di-rewrite destination-nya ke IP container.
- Bisa bind ke IP spesifik — tidak harus
0.0.0.0. - Mendukung multiple protokol — TCP, UDP, SCTP.
- Random port — host port bisa dipilih otomatis oleh Docker.
Sintaks Lengkap #
-p <host_ip>:<host_port>:<container_port>/<protocol>
| Format | Contoh | Keterangan |
|---|---|---|
-p 8080:80 |
Bind ke 0.0.0.0:8080 |
Default, semua interface |
-p 127.0.0.1:8080:80 |
Bind ke localhost | Aman, tidak terekspos LAN/internet |
-p 192.168.1.10:8080:80 |
Bind ke IP LAN | Hanya bisa diakses dari LAN |
-p 80:80 |
Port host 80 | Harus root atau CAP_NET_BIND_SERVICE |
-p 8080:80/tcp |
TCP only | Default |
-p 8080:80/udp |
UDP | Untuk DNS, video streaming, dll |
-p 80 |
Random host port | Docker pilih port free |
-p 8000-8100:80 |
Range | Map range port ke satu container port |
# Random port - Docker pilih host port yang tersedia
docker run -d -p 80 nginx
docker ps
# PORTS
# 0.0.0.0:32768->80/tcp # host port 32768 (contoh)
# Lihat port yang dipakai
docker port <container>
-p 80(tanpa:container_port) bukan port mapping ke container port 80. Ini artinya: “bind container port 80 ke random host port”. Untuk map ke container port tertentu, sintaks lengkapnya:-p <host_port>:<container_port>.
Perbandingan EXPOSE vs -p #
| Aspek | EXPOSE |
-p / --publish |
|---|---|---|
| Di mana dideklarasikan | Dockerfile | CLI / docker-compose |
| Membuka port ke host | ❌ Tidak | ✅ Ya |
| Mengubah iptables | ❌ Tidak | ✅ Ya (DNAT) |
| Wajib agar bisa diakses dari host | ❌ Tidak | ✅ Ya |
| Wajib agar container bisa diakses antar-container | ❌ Tidak (cukup network yang sama) | ❌ Tidak |
| Bisa di-override saat runtime | ✅ (default behavior) | ✅ |
Bisa dilihat di docker inspect |
✅ | ✅ |
| Best practice | Ya, untuk dokumentasi | Hanya jika memang perlu diakses |
Eksperimen Sederhana #
# Container dengan EXPOSE tapi tanpa -p
docker run -d --name exp-only -P nginx
docker ps
# PORTS: 80/tcp <- hanya expose, tidak publish
# (Port 80/tcp muncul tapi tidak di-publish ke host)
curl http://localhost:80
# Connection refused
# Container dengan -p
docker run -d --name pub -p 8080:80 nginx
docker ps
# PORTS: 0.0.0.0:8080->80/tcp <- published
curl http://localhost:8080
# Works! Welcome to nginx.
-P (kapital) di CLI adalah auto-publish semua EXPOSE port ke random host port. Ini berguna untuk testing cepat.
Bagaimana Port Mapping Bekerja di Level Kernel #
Saat kamu menjalankan docker run -p 8080:80 nginx, Docker menambahkan aturan iptables DNAT. Mari kita lihat detailnya.
# Lihat rule DNAT untuk container kita
sudo iptables -t nat -L -n -v | grep 8080
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
sequenceDiagram
participant U as User
participant H as Host NIC
participant K as Kernel
participant I as iptables PREROUTING
participant B as Bridge
participant C as Container
U->>H: TCP ke host:8080
H->>K: Paket masuk
K->>I: Evaluasi rule DNAT
I->>I: dst jadi 172.17.0.2:80
I->>B: Forward ke bridge
B->>C: Sampai di eth0 container
C-->>B: HTTP response (src: 172.17.0.2:80)
B-->>K: Response kembali
K->>K: Conntrack: src jadi IP host
K->>H: Keluar dari eth0 host
H-->>U: HTTP response sampai di user
Yang perlu kamu garis bawahi:
- DNAT mengubah destination paket saat masuk.
- Conntrack (connection tracking) otomatis me-reverse NAT saat balasan datang.
- Source port terlihat di container sebagai port ephemeral (misal
8080 -> 32768, bukan 8080 -> 80). Container tidak sadar dirinya diakses via port host 8080.
Conntrack adalah magic yang membuat NAT dua arah bekerja. Ia mencatat state koneksi (src IP, src port, dst IP, dst port) sehingga balasan bisa di-reverse-NAT dengan benar. Tanpa conntrack, kamu harus tulis rule terpisah untuk setiap arah.
Port Mapping di Docker Compose #
Di Docker Compose, port mapping dideklarasikan dengan key ports di level service.
version: "3.9"
services:
web:
image: nginx
ports:
- "8080:80" # host:container
- "127.0.0.1:8443:443" # bind ke localhost
- "9000:9000/udp" # UDP
- "80" # random host port
Format string "8080:80" adalah cara paling umum. Format panjang (mapping) juga bisa:
ports:
- target: 80
published: 8080
protocol: tcp
mode: host # atau "ingress" untuk Swarm
Modehostvsingress: Untuk single-host (development),mode: hostpublish langsung ke host. Untuk Swarm,mode: ingresspublish lewat Swarm routing mesh (siapapun yang menerima request di port 8080 akan meneruskannya ke container yang tersedia).
Port Range Mapping #
Untuk aplikasi yang butuh banyak port (game server, video streaming, multi-port service), Docker mendukung range mapping.
# Map range host port ke satu container port
docker run -p 7000-7010:7000-7010 my-server
# Docker Compose
services:
game-server:
image: my-game
ports:
- "7000-7010:7000-7010/udp"
Container akan listen di port 7000-7010 (di dalam container), dan Docker akan map setiap port ke host port dengan nomor yang sama. Cocok untuk:
- Minecraft server dan mod-nya
- WebRTC / video streaming
- Multi-port service (HTTP + WebSocket + custom protocol)
- Real-time multiplayer game
Port Exposure Tanpa Port Mapping (Antar Container) #
Penting untuk dipahami: container bisa saling bicara tanpa port mapping. Cukup ada di network yang sama.
# docker-compose.yml
services:
api:
image: my-api
# No ports exposed
web:
image: my-web
# No ports exposed
web bisa akses api lewat http://api:8080 karena:
- Keduanya di network Compose default.
- DNS internal Docker resolve
apike IP container.
Tidak perlu -p atau EXPOSE. Port mapping hanya dibutuhkan saat akses dari luar network container (host, internet, container lain di network berbeda).
flowchart LR
subgraph NET["docker-compose network"]
A[api: 8080]
B[web: 80]
end
A <-.->|internal: api:8080| B
EXT[External Client] -.->|butuh -p| A
Prinsip: minimizeportsmapping. Setiap-padalah attack surface tambahan. Service internal (database, cache, message broker) tidak boleh punya port mapping ke host. Hanya frontend service (web, API gateway) yang boleh.
Risiko Keamanan Port Mapping #
Port mapping membuka akses ke container. Tanpa konfigurasi yang hati-hati, database atau service internal bisa terekspos ke publik.
1. Default Bind ke 0.0.0.0 #
# ✗ Anti-pattern: -p tanpa IP = bind ke 0.0.0.0
docker run -d -p 3306:3306 mysql
# Kalau host punya IP publik, MySQL terekspos ke internet!
# ✓ Solusi: bind ke localhost
docker run -d -p 127.0.0.1:3306:3306 mysql
# Atau bahkan tanpa -p, kalau tidak perlu diakses dari host
2. Database Terekspos di Production #
# ✗ Anti-pattern: postgres dengan -p di production
services:
postgres:
image: postgres
ports:
- "5432:5432" # <-- bahaya di production!
# ✓ Solusi: postgres tanpa ports
services:
postgres:
image: postgres
# Tidak ada 'ports' - hanya container di network Compose yang bisa akses
api:
image: my-api
depends_on:
- postgres
3. Image yang Listen di Banyak Port #
# Image yang listen di 10+ port (misal Kubernetes dashboard)
docker run -p 8001:8001 -p 8443:8443 -p 10250:10250 ...
# Setiap port = attack surface
Rule of thumb production:
- Wajib port mapping: web server (80/443), API gateway, reverse proxy.
- Tidak boleh port mapping: database (Postgres, MySQL, Mongo, Redis), cache internal, message broker (Kafka, RabbitMQ), admin tools (pgAdmin, RedisInsight, Kibana).
- Hati-hati: monitoring agents, log shippers - hanya jika memang perlu diakses dari host.
Best Practice Port Mapping & Exposure #
1. Selalu Cantumkan EXPOSE di Dockerfile #
# Setiap image yang punya port aplikasi
EXPOSE 8080
Ini membantu:
- Orang yang baca Dockerfile tahu port mana.
- Tool auto-detect (Compose, K8s) bisa kerja.
- Image registry (Docker Hub) menampilkan info port.
2. Minimalkan Port Mapping di Production #
# Production - hanya port yang perlu
services:
frontend:
ports:
- "443:443" # HTTPS publik
api:
# Tidak ada ports - diakses via internal network
postgres:
# Tidak ada ports
3. Bind ke IP Spesifik untuk Service Sensitif #
# Database - bind ke localhost saja
docker run -p 127.0.0.1:5432:5432 postgres
# Admin tool - bind ke LAN IP tertentu
docker run -p 192.168.1.100:8080:8080 adminer
4. Gunakan Random Port untuk Testing #
# CI/CD testing - random port agar tidak konflik paralel
docker run -d -P my-test-image
docker port <container> 80
# Akan return port random yang dipakai
5. Kombinasikan dengan Reverse Proxy untuk TLS #
# Traefik di host network untuk HTTPS termination
services:
traefik:
image: traefik
network_mode: host
# ... konfigurasi TLS
# Backend services tanpa ports - diakses via Traefik
app:
image: my-app
# No ports
Perintah Berguna untuk Port Mapping #
# Lihat semua port mapping container
docker port <container>
# Lihat port yang di-EXPOSE di image
docker inspect <image> --format '{{json .Config.ExposedPorts}}'
# Lihat port mapping container yang sedang jalan
docker ps --format "table {{.Names}}\t{{.Ports}}"
# Cek apakah port tertentu ter-bind di host
ss -tlnp | grep 8080
# atau
netstat -tlnp | grep 8080
# Test koneksi dari host
curl -v http://localhost:8080
# Test koneksi dari container lain di network sama
docker exec <other-container> curl http://<target>:80
# Lihat rule iptables yang Docker tambahkan
sudo iptables -t nat -L DOCKER -n -v
Port Mapping dan K8s / Production #
Pada production Kubernetes, konsep port mapping bekerja secara berbeda:
Servicetype ClusterIP = internal only, no external port.Servicetype NodePort = publish di setiap node IP di port30000-32767.Servicetype LoadBalancer = cloud load balancer di depan.Ingress= HTTP routing layer (lebih fleksibel dari LoadBalancer).
Konsep EXPOSE di Dockerfile tetap berlaku — K8s membacanya untuk dokumentasi, tapi tidak otomatis membuat Service. Kamu tetap harus deklarasi Service atau Ingress secara manual.
Prinsip port mapping Docker vs Kubernetes: Di Docker Compose,ports:adalah cara utama. Di Kubernetes,Service+Ingressadalah cara utama. Keduanya bekerja mirip (meneruskan traffic), tapi di K8s ada layer tambahan (load balancing, TLS termination, path routing) yang biasanya di-handle oleh Ingress controller.
Anti-Pattern: Tanda Bahwa Kamu Salah Pakai Port Mapping #
✗ Database (Postgres, MySQL, Mongo, Redis) punya -p di production
✗ Container punya banyak -p tanpa alasan jelas
✗ Port mapping tanpa IP host (bind ke 0.0.0.0) untuk service internal
✗ EXPOSE 80 tanpa -p dan bingung kenapa tidak bisa diakses
✗ -p 80 (tanpa container port) untuk port tertentu
✗ Pakai range port 1-65535 untuk "simplify"
✗ Hardcode port yang bentrok antar service
Tabel Ringkasan: Pilih -p atau EXPOSE
#
Untuk keputusan cepat, pakai tabel ini.
| Situasi | Pakai | Contoh |
|---|---|---|
| Image dokumentasi port | EXPOSE |
EXPOSE 8080 di Dockerfile |
| Web server publik | -p |
-p 80:80 atau -p 443:443 |
| Database production | (tidak perlu) | Jangan ports: di Compose |
| Container yang bicara ke container lain | (tidak perlu) | Cukup network sama |
| Admin UI diakses dari host | -p 127.0.0.1: |
-p 127.0.0.1:8080:8080 |
| Service di LAN internal | -p <lan_ip>: |
-p 192.168.1.10:8080:8080 |
| Testing paralel (CI/CD) | -p dengan random |
-P (auto) atau -p 80 |
| Game server multi-port | -p dengan range |
-p 7000-7010:7000-7010/udp |
| K8s production | Service + Ingress |
ClusterIP untuk internal |
Ringkasan #
- Port exposure (
EXPOSE) = metadata. Mendeklarasikan port yang dipakai aplikasi. TIDAK membuka akses.- Port mapping (
-p/--publish) = membuka akses. Membuat iptables DNAT untuk meneruskan traffic dari host ke container.- Default
-pbind ke0.0.0.0— artinya semua interface, termasuk IP publik. Untuk service internal, selalu bind ke127.0.0.1atau IP tertentu.- Sintaks lengkap:
-p <host_ip>:<host_port>:<container_port>/<protocol>. Bisa TCP, UDP, range, atau random port.- Docker Compose pakai key
portsdi level service. Modehost(default) atauingress(Swarm).- Antar container, port mapping tidak perlu. Cukup network yang sama + DNS internal.
-phanya untuk akses dari host atau luar network.- Best practice production: hanya port yang boleh diakses publik (web server) yang di-publish. Database, cache, broker, admin tool = tidak boleh publish.
- Anti-pattern: database publish, image random dengan banyak port,
EXPOSEdianggap cukup tanpa-p, lupa bind ke localhost untuk service internal.- Di Kubernetes, konsep
EXPOSEtetap berguna untuk dokumentasi, tapi akses publik di-handle oleh Service + Ingress.- Perintah penting:
docker port,docker inspect,iptables -t nat -L DOCKER,ss -tlnpuntuk debugging.- Prinsip utama: minimize port mapping. Setiap
-padalah attack surface tambahan. Buka hanya yang dibutuhkan.