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}}'
EXPOSE tanpa -p = port tidak bisa diakses dari host. Ini misconception paling umum. Banyak orang mengira EXPOSE sudah 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
Mode host vs ingress: Untuk single-host (development), mode: host publish langsung ke host. Untuk Swarm, mode: ingress publish 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 api ke 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: minimize ports mapping. Setiap -p adalah 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:

  • Service type ClusterIP = internal only, no external port.
  • Service type NodePort = publish di setiap node IP di port 30000-32767.
  • Service type 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 + Ingress adalah 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 -p bind ke 0.0.0.0 — artinya semua interface, termasuk IP publik. Untuk service internal, selalu bind ke 127.0.0.1 atau IP tertentu.
  • Sintaks lengkap: -p <host_ip>:<host_port>:<container_port>/<protocol>. Bisa TCP, UDP, range, atau random port.
  • Docker Compose pakai key ports di level service. Mode host (default) atau ingress (Swarm).
  • Antar container, port mapping tidak perlu. Cukup network yang sama + DNS internal. -p hanya 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, EXPOSE dianggap cukup tanpa -p, lupa bind ke localhost untuk service internal.
  • Di Kubernetes, konsep EXPOSE tetap berguna untuk dokumentasi, tapi akses publik di-handle oleh Service + Ingress.
  • Perintah penting: docker port, docker inspect, iptables -t nat -L DOCKER, ss -tlnp untuk debugging.
  • Prinsip utama: minimize port mapping. Setiap -p adalah attack surface tambahan. Buka hanya yang dibutuhkan.

← Sebelumnya: None Network   Berikutnya: DNS Internal →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact