# Jaringan Docker untuk Pemula: Bridge, DNS, Port, dan ufw

> Jaringan Docker untuk pemula: bridge network, DNS antar container, publish port, kenapa ufw tidak memblokirnya, dan arti localhost di dalam container.

URL: https://www.matthewswong.com/id/learn/devops-from-zero/docker-networking-explained/

DevOps dari nol · Pelajaran 16 dari 25 · Ditinjau pada 18 Oktober 2026 · Oleh Matthews Wong (https://www.matthewswong.com/id/about/)

> Jaringan Docker adalah cara container saling menjangkau dan menjangkau dunia di luar host. Setiap container punya network stack sendiri, termasuk loopback-nya. Di user-defined network, yang dibuat Compose untuk setiap project, container saling menemukan lewat nama berkat DNS bawaan Docker. Publish meneruskan port host ke port container, di semua alamat host kecuali Anda mengikatnya ke 127.0.0.1.

## Setelah pelajaran ini, Anda bisa

- Menjelaskan kenapa container di user-defined network bisa saling menemukan lewat nama, dan port mana yang dipakai untuk terhubung.
- Mem-publish port hanya di loopback, dan menjelaskan kenapa port yang di-publish bisa lolos dari ufw.
- Membedakan localhost milik container dari localhost milik host, dan memperbaiki aplikasi yang hanya listen di loopback-nya sendiri.

## Bagaimana request menemukan container

Aplikasi Compose dengan dua service di satu host: container saling menemukan lewat nama berkat DNS bawaan Docker, port yang di-publish diteruskan melewati ufw kecuali diikat ke loopback, dan aplikasi harus listen di luar loopback container-nya sendiri agar bisa menjawab.

1. **Kenali bagiannya.** Satu host Docker. Compose menaruh web dan db di user-defined bridge network yang punya DNS sendiri. Di luarnya ada ufw, nginx, port yang di-publish, dan internet.
2. **Saling temukan lewat nama.** web menanyakan db ke DNS bawaan Docker, lalu terhubung ke db:5432, yaitu port container. Tidak ada yang di-publish untuk ini: container dalam satu network langsung saling menjangkau port-nya.

   ```
   postgres://db:5432
   ```

   *Tebak:* ufw menolak port 3002, dan web di-publish dengan -p 3002:3000. Orang asing terhubung ke 3002. Apa yang terjadi? (ufw membuang paketnya / Paketnya tetap sampai ke container)
3. **Di-publish melewati firewall.** -p 3002:3000 listen di semua alamat host. Aturan NAT Docker membelokkan paket sebelum sampai ke chain yang dipakai ufw, jadi aturan deny tidak pernah melihatnya.

   ```
   ports:
     - "3002:3000"
   ```
4. **Ikat ke loopback.** 127.0.0.1:3002:3000 hanya listen di loopback host, jadi host lain tidak bisa menjangkaunya. nginx berjalan di host yang sama, jadi bisa, dan menjadi satu-satunya pintu masuk.

   ```
   - "127.0.0.1:3002:3000"
   ```

   *Tebak:* Di dalam web, aplikasi listen di 127.0.0.1:3000. Apakah request nginx lewat port yang di-publish mendapat jawaban? (Ya, 127.0.0.1 tetap 127.0.0.1 / Tidak, tidak ada yang menjawab)
5. **Loopback milik container.** Setiap container punya loopback sendiri. Docker meneruskan port yang di-publish ke alamat network container, jadi aplikasi yang hanya listen di 127.0.0.1 milik container tidak pernah menjawab.

   ```
   server.listen(3000, "127.0.0.1")
   ```
6. **Listen di semua alamat.** Image situs ini menyetel HOSTNAME=0.0.0.0, jadi Next.js listen di semua alamat container. Request yang diteruskan dijawab, dan 127.0.0.1:3000 milik health check tetap berfungsi di dalam.

   ```
   ENV HOSTNAME=0.0.0.0
   ```

## Apa itu jaringan Docker?

Jaringan Docker adalah sekumpulan network virtual yang menghubungkan container satu sama lain dan dengan dunia di luar host. Setiap container berjalan di network namespace-nya sendiri, yang dipakai Linux untuk memisahkan network device, protocol stack, routing table, aturan firewall, dan nomor port. Jadi container punya interface sendiri, loopback sendiri, dan port sendiri. Dokumentasi Docker menjelaskannya dari sisi container: yang terlihat hanya sebuah network interface dengan alamat IP, gateway, dan routing table.

Network yang diikuti sebuah container menentukan apa yang bisa dijangkaunya. Jalankan docker network ls di instalasi baru dan Anda akan melihat tiga: bridge, host, dan none. bridge adalah driver default. host menghilangkan isolasi antara container dan host. none hanya menyisakan loopback device untuk container. Overlay network, yang menghubungkan beberapa host Docker untuk Swarm, adalah topik tersendiri.

## Bagaimana container saling menemukan lewat nama?

Tidak di default bridge. Container yang dijalankan tanpa --network masuk ke network bernama bridge, tempat container hanya bisa saling menjangkau lewat alamat IP, kecuali Anda memakai opsi lama --link. Tutorial Docker sendiri menunjukkan hasilnya: ping lewat nama gagal dengan pesan bad address. Tutorial yang sama menyebut default bridge sebagai detail warisan Docker yang tidak disarankan untuk production.

Di user-defined network, jenis yang dibuat oleh docker network create, Docker menjalankan DNS server bawaan. Container saling me-resolve lewat nama atau alias, dan lookup untuk nama di luar diteruskan ke DNS server yang dikonfigurasi di host. Karena itu connection string antar container sebaiknya memakai nama, bukan alamat.

Compose melakukan ini untuk Anda. Untuk setiap project, Compose membuat network yang dinamai sesuai nama project, yang secara default adalah nama direktorinya, ditambah _default, dan network itu memakai driver bridge. Setiap service bergabung ke sana dan mendaftarkan namanya, jadi service bernama db bisa dijangkau di hostname db dari service lain mana pun dalam file itu.

## Port mana yang dipakai antar container?

Port container. Panduan networking Compose menyatakannya dengan jelas: komunikasi antar service memakai port container, dan port host hanya dipakai dari luar network. Kalau db di-publish sebagai 5433:5432, service lain tetap terhubung ke db:5432. Container dalam user-defined network yang sama praktis membuka semua port-nya satu sama lain, tanpa perlu publish sama sekali.

Network juga cara memisahkan sesuatu. Container di bridge network yang berbeda hanya bisa saling menjangkau lewat port yang di-publish. Di Compose, key networks di level atas memungkinkan proxy berbagi satu network dengan aplikasi, sementara aplikasi berbagi network lain dengan database, sehingga proxy sama sekali tidak punya jalur ke database.

## Apa yang sebenarnya terjadi saat port di-publish?

Publish, lewat -p HOST_PORT:CONTAINER_PORT atau entri ports di Compose, membuat Docker menambahkan aturan firewall yang menerjemahkan trafik dari port host ke port container. Tanpa alamat host, Docker mem-publish di semua alamat host, 0.0.0.0 dan [::], dan dokumentasinya menyebut ini tidak aman secara default: port terbuka untuk dunia luar, bukan hanya untuk host. Taruh 127.0.0.1 di depannya, maka hanya host Docker yang bisa menjangkaunya. Pengaturan ip di daemon bisa menjadikan loopback sebagai default.

EXPOSE 3000, di bagian akhir Dockerfile situs ini, tidak mem-publish apa pun. Referensi Docker menyebutnya dokumentasi antara orang yang mem-build image dan orang yang menjalankan container. Hanya docker run -P yang memakainya, dengan mem-publish setiap port yang di-expose ke port bernomor tinggi di host. Pelajaran 12 membahas EXPOSE dari sisi hardening; di sini intinya hanya ke mana trafik mengalir.

## Kenapa ufw tidak memblokir port yang di-publish?

Karena paketnya tidak pernah sampai ke aturan ufw. Dokumentasi Docker menjelaskannya: Docker merutekan trafik container di tabel nat, jadi paket dibelokkan sebelum sampai ke chain INPUT dan OUTPUT yang dipakai ufw, sehingga konfigurasi firewall Anda praktis diabaikan. Referensi Compose menyebut hal yang sama untuk port tanpa alamat host: port itu terikat ke semua interface dan melewati aturan firewall host. Aturan ufw yang menolak 3002 tidak menghentikan trafik ke container yang di-publish di 3002.

Jangan memperbaikinya dengan mengedit aturan milik Docker; dokumentasi Docker menyatakan aturan itu tidak boleh Anda ubah. Ikat setiap port yang hanya dibutuhkan proxy lokal ke 127.0.0.1, supaya tidak ada yang terbuka ke publik untuk difilter, dan biarkan proxy di host, yang dijaga ufw, menjadi satu-satunya pintu masuk. Itulah yang dilakukan file compose situs ini.

## Apakah localhost di dalam container itu host?

Bukan. Di dalam container, localhost dan 127.0.0.1 adalah loopback milik container itu sendiri, karena container punya network namespace sendiri. Driver none memperlihatkannya: container di sana hanya mendapat loopback device, tidak ada yang lain. Jadi connection string bertuliskan localhost:5432 di dalam container aplikasi menunjuk ke container aplikasi itu sendiri, bukan ke database di host atau di container lain.

Berlaku juga sebaliknya. Port yang di-publish diteruskan ke alamat network container, bukan ke loopback-nya, jadi aplikasi yang hanya listen di 127.0.0.1 di dalam container tidak pernah menjawab. Dalam uji coba di Docker Engine 29.8.1, curl ke port yang di-publish mendapat empty reply sampai aplikasinya listen di 0.0.0.0. Karena itu Dockerfile situs ini menyetel HOSTNAME=0.0.0.0, yang dibaca server standalone Next.js sebagai alamat untuk listen.

Untuk menjangkau service di host dari dalam container, Docker Desktop sudah me-resolve nama host.docker.internal untuk Anda; di tempat lain, tambahkan --add-host host.docker.internal=host-gateway, yang mengarah ke alamat internal host. Driver host network adalah opsi yang kasar: container berbagi network namespace dengan host, tidak punya alamat sendiri, dan Docker membuang setiap -p disertai peringatan. Driver ini jalan di Docker Engine di Linux, dan di Docker Desktop 4.34 ke atas setelah Anda mengaktifkannya di Settings.

## Konfigurasi nyata di balik pelajaran ini

`docker-compose.yml:24-26`

```yaml
    ports:
      # Bind to loopback only — nginx terminates TLS and proxies to this port.
      - "127.0.0.1:${APP_PORT:-3002}:3000"
```

Tiga bagian, baca dari kanan ke kiri: port container 3000, port host APP_PORT (3002 jika kosong), dan alamat host 127.0.0.1. Karena alamat host-nya adalah loopback milik host, host lain tidak bisa menjangkau port itu dan tidak ada yang bisa terlewat oleh ufw. nginx, di server yang sama, terhubung ke port itu dan menjadi satu-satunya pintu masuk.

`Dockerfile:45-46`

```dockerfile
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
```

Server standalone Next.js membaca PORT dan HOSTNAME sebelum start. 0.0.0.0 ini berada di dalam network namespace milik container: artinya semua alamat container, jadi trafik yang diteruskan mendapat jawaban. Alamat host mana yang terbuka diputuskan terpisah, oleh entri ports di atas.

`docker-compose.yml:29-38`

```yaml
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://127.0.0.1:3000/robots.txt",
        ]
```

Docker menjalankan perintah ini di dalam container, jadi 127.0.0.1:3000 adalah loopback milik container dan port asli aplikasi, bukan port 3002 milik host. Ini berfungsi karena aplikasi listen di 0.0.0.0, yang mencakup loopback container.

## Yang sering rusak

- **ufw menolak port itu, tetapi internet tetap bisa masuk.** Entri ports tanpa alamat host listen di semua alamat host, dan aturan NAT Docker merutekan trafiknya sebelum chain ufw berjalan. Aturan deny-nya ada; hanya saja tidak pernah melihat paketnya. Ikat ke 127.0.0.1, atau ke satu interface yang memang harus listen, lalu pastikan dengan docker port.
- **Empty reply dari port yang di-publish.** Port sudah di-publish, container berjalan, dan docker exec dengan wget ke 127.0.0.1 di dalamnya berhasil, tetapi curl dari host gagal. Aplikasinya hanya listen di loopback container, sedangkan trafik yang diteruskan tiba di alamat network container. Buat aplikasi listen di 0.0.0.0: HOSTNAME untuk Next.js, atau pengaturan host milik server Anda.
- **bad address: nama yang tidak ter-resolve.** Container yang dijalankan dengan docker run biasa berada di default bridge, yang tidak punya resolusi nama antar container. Buat sebuah network, jalankan kedua container di sana, dan nama yang sama langsung berfungsi. Di Compose, service dalam network project langsung bisa saling me-resolve lewat nama service.
- **localhost atau port host di connection string.** postgres://localhost:5432 di dalam container aplikasi menunjuk ke container aplikasi itu sendiri. postgres://db:5433, yang memakai port host hasil publish, juga meleset. Antar container, pakai nama service dan port container: db:5432.

### Network yang tersedia di instalasi Docker baru, ditambah yang dibuat Compose. Sesuai dokumentasi Docker.

| Network | Menemukan container lewat nama? | Port | Menurut Docker |
| --- | --- | --- | --- |
| Default bridge | Tidak, hanya alamat IP (atau --link yang sudah lama) | -p mem-publish ke host | Detail warisan, tidak disarankan untuk production |
| User-defined bridge (default Compose) | Ya, lewat nama atau alias, melalui DNS bawaan | Semua port terbuka antar anggota; -p untuk host | Lebih baik daripada default bridge |
| host | Tidak punya network sendiri: berbagi milik host | -p dibuang; aplikasi listen langsung di host | Menghilangkan isolasi antara container dan host |
| none | Tidak, hanya ada loopback device | Tidak ada yang bisa di-publish | Mengisolasi container sepenuhnya |

## Coba sendiri

Anda hanya butuh Docker di laptop; semua port di bawah tetap di 127.0.0.1. Setiap langkah menunjukkan satu aturan dari pelajaran ini, dan langkah terakhir membersihkan semuanya.

1. Lihat network yang ada di instalasi baru: bridge, host, dan none.

   ```bash
   docker network ls
   ```
2. Di default bridge, nama tidak ter-resolve. Hasilnya ping: bad address 'demo-a'.

   ```bash
   docker run -d --name demo-a alpine:3 sleep 600
   docker run --rm alpine:3 ping -c 1 demo-a
   ```
3. Di network yang Anda buat, lookup yang sama berhasil: wget mencetak halaman sambutan nginx. Dari default bridge, nama itu tetap gagal.

   ```bash
   docker network create demo-net
   docker run -d --name demo-web --network demo-net nginx:alpine
   docker run --rm --network demo-net alpine:3 wget -qO- http://demo-web
   docker run --rm alpine:3 wget -qO- -T 3 http://demo-web
   ```
4. Publish dengan dan tanpa alamat host, lalu bandingkan hasil docker port: 0.0.0.0 dan [::] untuk yang pertama, hanya 127.0.0.1 untuk yang kedua.

   ```bash
   docker run -d --name demo-all -p 8081:80 nginx:alpine
   docker port demo-all
   docker run -d --name demo-lo -p 127.0.0.1:8082:80 nginx:alpine
   docker port demo-lo
   curl -I http://127.0.0.1:8082
   ```
5. Jalankan server kecil yang listen di 127.0.0.1 milik container, dan beri waktu sedetik untuk start. curl lewat port yang di-publish gagal dengan balasan kosong atau reset, sedangkan request yang sama dari dalam container berhasil.

   ```bash
   docker run -d --name demo-inside -e HOST=127.0.0.1 -p 127.0.0.1:8083:3000 node:22-alpine \
     node -e "require('http').createServer((req, res) => res.end('hello\n')).listen(3000, process.env.HOST)"
   curl http://127.0.0.1:8083
   docker exec demo-inside wget -qO- http://127.0.0.1:3000
   ```
6. Jalankan lagi dengan listen di 0.0.0.0, perbaikan yang dilakukan Dockerfile situs ini lewat HOSTNAME. Beri waktu sedetik untuk start; sekarang curl mencetak hello.

   ```bash
   docker rm -f demo-inside
   docker run -d --name demo-inside -e HOST=0.0.0.0 -p 127.0.0.1:8083:3000 node:22-alpine \
     node -e "require('http').createServer((req, res) => res.end('hello\n')).listen(3000, process.env.HOST)"
   curl http://127.0.0.1:8083
   ```
7. Bersihkan container dan network-nya.

   ```bash
   docker rm -f demo-a demo-web demo-all demo-lo demo-inside
   docker network rm demo-net
   ```

## Kuis singkat

1. **Dua service Compose, api dan db, berada di network project yang sama. db di-publish sebagai 5433:5432. Alamat mana yang harus dipakai api?**
   A. localhost:5433
   B. db:5432
   C. db:5433
   Jawaban: B. Antar container, nama service di-resolve lewat DNS bawaan Docker dan yang dipakai adalah port container. Port host 5433 hanya untuk klien di luar network, dan localhost justru api itu sendiri.

2. **Sebuah VPS menjalankan ufw yang hanya mengizinkan 22, 80, dan 443. Sebuah container di-publish dengan -p 8080:80. Apakah internet bisa menjangkau port 8080?**
   A. Tidak, ufw membuangnya seperti port lain
   B. Hanya jika nginx mem-proxy-nya
   C. Ya, aturan NAT Docker merutekannya sebelum chain ufw
   Jawaban: C. Tanpa alamat host, port itu listen di semua alamat host, dan Docker membelokkan trafiknya di tabel nat sebelum sampai ke chain yang dipakai ufw. Mengikatnya sebagai 127.0.0.1:8080:80 menutupnya dari host lain.

3. **Sebuah aplikasi di dalam container listen di 127.0.0.1:3000 dan di-publish sebagai 127.0.0.1:8080:3000. curl http://127.0.0.1:8080 di host gagal. Apa perbaikannya?**
   A. Buat aplikasi listen di 0.0.0.0 di dalam container
   B. Tambahkan EXPOSE 3000 ke Dockerfile
   C. Pakai nomor yang sama untuk port host dan port container
   Jawaban: A. 127.0.0.1 milik container adalah loopback-nya sendiri, dan trafik yang diteruskan tiba di alamat network-nya. Listen di 0.0.0.0 mencakup keduanya. EXPOSE hanya mendokumentasikan port, dan nomor port-nya memang bukan masalah.

## Pikirkan dulu

- **Kenapa dua container yang dijalankan dengan docker run biasa tidak bisa saling menjangkau lewat nama?** Keduanya berada di default bridge, tempat container hanya bisa memakai alamat IP. Hanya user-defined network yang mendapat DNS bawaan Docker, dan Compose membuatnya untuk setiap project, jadi service-nya bisa saling me-resolve lewat nama.
- **File compose situs ini punya 127.0.0.1 di entri ports dan di health check. Apakah keduanya berarti mesin yang sama?** Tidak. Di entri ports itu loopback milik host, jadi hanya server itu yang bisa menjangkau port 3002. Health check berjalan di dalam container, jadi 127.0.0.1:3000 miliknya adalah loopback container dan port asli aplikasi.
- **Apa pengaruh EXPOSE 3000 di Dockerfile terhadap network?** Tidak ada, kalau berdiri sendiri. EXPOSE mendokumentasikan port yang diharapkan di-publish oleh image itu. Publish terjadi lewat -p, entri ports, atau -P, yang mem-publish setiap port yang di-expose ke port host bernomor tinggi.

## Pelajari lebih dalam

- [Service Discovery di Microservices: Cara Kerjanya](https://www.matthewswong.com/id/blog/service-discovery-microservices-explained/)
- [Cara Setting Nginx Reverse Proxy untuk Docker di Cloudflare](https://www.matthewswong.com/id/blog/cara-setting-nginx-reverse-proxy-docker-cloudflare/)
- [VPC Networking untuk Developer: Panduan Praktis Isolasi Jaringan Cloud](https://www.matthewswong.com/id/blog/vpc-networking-guide/)
- [Ekspos Layanan Self-Hosted dengan Aman via Cloudflare Tunnel](https://www.matthewswong.com/id/blog/cloudflare-tunnel-zero-trust-homelab/)

## Sumber

- [Dokumentasi Docker: ikhtisar networking (driver, layanan DNS)](https://docs.docker.com/engine/network/)
- [Dokumentasi Docker: driver bridge network](https://docs.docker.com/engine/network/drivers/bridge/)
- [Dokumentasi Docker: networking untuk container standalone](https://docs.docker.com/engine/network/tutorials/standalone/)
- [Dokumentasi Docker: networking di Compose](https://docs.docker.com/compose/how-tos/networking/)
- [Dokumentasi Docker: publish dan mapping port](https://docs.docker.com/engine/network/port-publishing/)
- [Dokumentasi Docker: packet filtering dan firewall (ufw)](https://docs.docker.com/engine/network/packet-filtering-firewalls/)
- [Dokumentasi Docker: referensi service Compose (ports)](https://docs.docker.com/reference/compose-file/services/)
- [Dokumentasi Docker: referensi Dockerfile, EXPOSE](https://docs.docker.com/reference/dockerfile/#expose)
- [Dokumentasi Docker: driver host network](https://docs.docker.com/engine/network/drivers/host/)
- [Dokumentasi Docker: driver none network](https://docs.docker.com/engine/network/drivers/none/)
- [Dokumentasi Docker: docker container run (--add-host host-gateway, -p)](https://docs.docker.com/reference/cli/docker/container/run/)
- [Dokumentasi Docker: panduan networking Docker Desktop (host.docker.internal)](https://docs.docker.com/desktop/features/networking/networking-how-tos/)
- [Dokumentasi Next.js: output standalone (PORT dan HOSTNAME)](https://nextjs.org/docs/app/api-reference/config/next-config-js/output)
- [man page: network_namespaces(7)](https://man7.org/linux/man-pages/man7/network_namespaces.7.html)

---

- HTML version: https://www.matthewswong.com/id/learn/devops-from-zero/docker-networking-explained/
- English: https://www.matthewswong.com/en/learn/devops-from-zero/docker-networking-explained/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
