Blog

  • Belajar Golang dari Nol #16: Struktur Project Go yang Rapi

    Belajar Golang dari Nol #16: Struktur Project Go yang Rapi

    Di Belajar Golang dari Nol #15 kita sudah menaruh API produk di server. Binary sudah jalan sebagai service. Domain sudah dipasang. Secara fungsi, aplikasinya hidup dan bisa dipakai orang lain.

    Tapi coba buka lagi folder projectnya. Kemungkinan besar isinya masih sedikit file yang gemuk. Satu main.go yang menampung hampir semua hal. Selama masih dikerjakan sendiri, itu belum terasa mengganggu. Masalah baru muncul saat aplikasinya tumbuh atau ada orang kedua yang ikut menyentuh kodenya.

    Bagian ini membahas cara merapikan project Go supaya tetap enak dipakai jangka panjang. Kita tidak menambah fitur baru. Kita memindahkan kode yang sudah ada ke tempat yang lebih masuk akal.

    Tanda project Go mulai berantakan

    Sebelum merapikan, kenali dulu gejalanya. Kalau salah satu tanda di bawah ini terasa familiar, project Anda sudah waktunya ditata.

    • File main.go tembus ratusan baris. Setiap kali ingin mengubah satu hal kecil, Anda harus scroll jauh untuk menemukannya.
    • Handler menempel langsung ke query database. Fungsi yang tugasnya membalas HTTP juga menulis SELECT dan INSERT di dalamnya.
    • Susah dites. Untuk menguji satu aturan sederhana seperti “harga tidak boleh nol”, Anda terpaksa menyalakan database dulu.
    • Dua orang mengedit file yang sama. Setiap pull request berakhir dengan konflik di file yang itu-itu saja.
    • Aturan bisnis tercecer. Validasi nama produk ada di handler, validasi stok ada di tempat lain, dan tidak ada yang tahu mana yang benar.

    Inti masalahnya satu. Terlalu banyak tanggung jawab dijejalkan ke satu tempat.

    Prinsip: pisahkan berdasarkan tanggung jawab

    Banyak pemula memisahkan file berdasarkan jenisnya. Semua struct masuk folder models. Semua fungsi HTTP masuk folder handlers. Semua query masuk folder database. Kelihatan rapi di awal, tapi setiap kali menambah satu fitur Anda harus membuka tiga folder berbeda.

    Cara yang lebih tahan lama adalah memisahkan berdasarkan tanggung jawab, lalu mengelompokkannya per fitur. Untuk aplikasi web biasa, tiga lapis sudah cukup.

    Lapis 1: handler

    Tugasnya menerima request dan membalas response. Dia membaca body JSON, mengambil parameter dari URL, memanggil lapis di bawahnya, lalu menulis hasilnya kembali sebagai JSON. Handler tidak boleh tahu soal SQL. Handler juga tidak memutuskan apakah sebuah harga valid.

    Lapis 2: service

    Tempat aturan bisnis tinggal. Nama produk wajib diisi. Harga harus lebih dari nol. Stok minus dianggap nol. Service tidak tahu apa-apa soal HTTP. Dia tidak tahu status code, tidak tahu header, tidak tahu request. Karena itu, service bisa dipakai ulang dari mana saja, termasuk dari CLI atau worker.

    Lapis 3: repository

    Satu-satunya bagian yang bicara ke database. Query SQL hanya boleh ada di sini. Kalau suatu hari Anda ganti dari PostgreSQL ke MySQL, atau menambah cache, yang berubah cuma lapis ini.

    Arah panggilannya selalu satu arah: handler memanggil service, service memanggil repository. Tidak pernah sebaliknya. Aturan sederhana ini yang menjaga project tetap waras.

    Struktur folder yang disarankan

    Untuk API produk yang kita bangun sejak bagian 11, struktur folder yang saya sarankan seperti ini.

    tokoapi/
    ├── cmd/
    │   └── api/
    │       └── main.go
    ├── internal/
    │   ├── config/
    │   │   └── config.go
    │   ├── middleware/
    │   │   ├── log.go
    │   │   └── recover.go
    │   └── produk/
    │       ├── handler.go
    │       ├── model.go
    │       ├── repository.go
    │       ├── service.go
    │       └── service_test.go
    ├── go.mod
    └── go.sum
    

    Kenapa ada folder cmd

    Folder cmd berisi titik masuk program. Satu subfolder untuk satu binary. Sekarang isinya baru cmd/api. Nanti kalau Anda butuh program tambahan, misalnya pengirim laporan harian, tinggal buat cmd/worker/main.go tanpa mengganggu API. Keduanya berbagi kode yang sama di internal.

    Perintah build dan run jadi sedikit lebih panjang, tapi masih sederhana.

    go run ./cmd/api
    go build -o bin/api ./cmd/api
    

    Kenapa ada folder internal

    Ini bukan sekadar kebiasaan penamaan. internal adalah fitur bawaan Go. Package yang ada di dalam folder bernama internal hanya bisa diimport oleh kode yang berada di dalam module yang sama. Project lain yang mengimport module Anda akan langsung ditolak oleh compiler.

    Manfaatnya jelas. Anda bebas mengubah isi internal/produk kapan saja tanpa takut merusak project orang. Kalau suatu saat Anda memang ingin membagikan sebagian kode ke publik, pindahkan package itu keluar dari internal, misalnya ke folder pkg. Selama belum yakin, taruh saja semuanya di internal.

    Refactor bertahap dari kode bagian 12 dan 13

    Jangan bongkar semuanya sekaligus. Pindahkan satu lapis, jalankan aplikasinya, pastikan masih normal, baru lanjut ke lapis berikutnya.

    Urutan yang paling aman adalah dari bawah ke atas. Mulai dari model, lalu repository, lalu service, terakhir handler. Alasannya sederhana. Lapis bawah tidak bergantung pada lapis atas, jadi memindahkannya tidak akan membuat kode lain rusak. Kalau Anda mulai dari handler, semua yang dipanggilnya masih berserakan dan Anda akan terjebak memindahkan banyak hal sekaligus.

    Satu file berisi konfigurasi juga sebaiknya ikut dipindah ke internal/config. Isinya membaca environment variable yang sudah kita siapkan waktu deploy di bagian 15, lalu mengembalikannya sebagai struct. Dengan begitu, tidak ada lagi os.Getenv yang tersebar di tengah kode.

    model.go

    Mulai dari yang paling gampang. Pindahkan struct produk ke filenya sendiri.

    package produk
    
    import "time"
    
    type Produk struct {
    	ID         int64     `json:"id"`
    	Nama       string    `json:"nama"`
    	Harga      int64     `json:"harga"`
    	Stok       int       `json:"stok"`
    	DibuatPada time.Time `json:"dibuat_pada"`
    }
    

    Perhatikan nama packagenya: produk, bukan models. Karena package sudah bernama produk, penulisan dari luar jadi produk.Produk. Tidak perlu menambah awalan atau akhiran apa pun pada nama struct.

    repository.go

    Sekarang pindahkan semua query yang tadinya menempel di handler. Repository menyimpan *sql.DB sebagai field, bukan mengambilnya dari variabel global.

    package produk
    
    import (
    	"context"
    	"database/sql"
    )
    
    type Repository struct {
    	db *sql.DB
    }
    
    func NewRepository(db *sql.DB) *Repository {
    	return &Repository{db: db}
    }
    
    func (r *Repository) Ambil(ctx context.Context, id int64) (Produk, error) {
    	query := `SELECT id, nama, harga, stok, dibuat_pada FROM produk WHERE id = $1`
    
    	var p Produk
    	err := r.db.QueryRowContext(ctx, query, id).
    		Scan(&p.ID, &p.Nama, &p.Harga, &p.Stok, &p.DibuatPada)
    	if err != nil {
    		return Produk{}, err
    	}
    
    	return p, nil
    }
    
    func (r *Repository) Simpan(ctx context.Context, p Produk) (Produk, error) {
    	query := `INSERT INTO produk (nama, harga, stok)
    		VALUES ($1, $2, $3)
    		RETURNING id, dibuat_pada`
    
    	err := r.db.QueryRowContext(ctx, query, p.Nama, p.Harga, p.Stok).
    		Scan(&p.ID, &p.DibuatPada)
    	if err != nil {
    		return Produk{}, err
    	}
    
    	return p, nil
    }
    

    service.go

    Semua if validasi yang tadinya berserakan di handler kita kumpulkan di sini. Errornya dibuat sebagai variabel supaya bisa dicek dari luar.

    package produk
    
    import (
    	"context"
    	"errors"
    	"strings"
    )
    
    var (
    	ErrNamaKosong = errors.New("nama produk tidak boleh kosong")
    	ErrHargaSalah = errors.New("harga produk harus lebih dari nol")
    )
    
    type PenyimpanProduk interface {
    	Ambil(ctx context.Context, id int64) (Produk, error)
    	Simpan(ctx context.Context, p Produk) (Produk, error)
    }
    
    type Service struct {
    	repo PenyimpanProduk
    }
    
    func NewService(repo PenyimpanProduk) *Service {
    	return &Service{repo: repo}
    }
    
    func (s *Service) Buat(ctx context.Context, p Produk) (Produk, error) {
    	p.Nama = strings.TrimSpace(p.Nama)
    
    	if p.Nama == "" {
    		return Produk{}, ErrNamaKosong
    	}
    	if p.Harga <= 0 {
    		return Produk{}, ErrHargaSalah
    	}
    	if p.Stok < 0 {
    		p.Stok = 0
    	}
    
    	return s.repo.Simpan(ctx, p)
    }
    
    func (s *Service) Detail(ctx context.Context, id int64) (Produk, error) {
    	return s.repo.Ambil(ctx, id)
    }
    

    Ada satu hal penting di sini. Field repo bertipe PenyimpanProduk, bukan *Repository. Alasannya kita bahas sebentar lagi di bagian testing.

    handler.go

    Sisa tugas handler tinggal tiga: baca request, panggil service, tulis JSON.

    package produk
    
    import (
    	"encoding/json"
    	"net/http"
    	"strconv"
    )
    
    type Handler struct {
    	svc *Service
    }
    
    func NewHandler(svc *Service) *Handler {
    	return &Handler{svc: svc}
    }
    
    func (h *Handler) Buat(w http.ResponseWriter, r *http.Request) {
    	var body Produk
    	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
    		tulisJSON(w, http.StatusBadRequest, map[string]string{"error": "body tidak valid"})
    		return
    	}
    
    	hasil, err := h.svc.Buat(r.Context(), body)
    	if err != nil {
    		tulisJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
    		return
    	}
    
    	tulisJSON(w, http.StatusCreated, hasil)
    }
    
    func (h *Handler) Detail(w http.ResponseWriter, r *http.Request) {
    	id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
    	if err != nil {
    		tulisJSON(w, http.StatusBadRequest, map[string]string{"error": "id tidak valid"})
    		return
    	}
    
    	hasil, err := h.svc.Detail(r.Context(), id)
    	if err != nil {
    		tulisJSON(w, http.StatusNotFound, map[string]string{"error": "produk tidak ditemukan"})
    		return
    	}
    
    	tulisJSON(w, http.StatusOK, hasil)
    }
    
    func tulisJSON(w http.ResponseWriter, status int, data any) {
    	w.Header().Set("Content-Type", "application/json")
    	w.WriteHeader(status)
    	json.NewEncoder(w).Encode(data)
    }
    

    Handler ini jadi jauh lebih pendek daripada versi bagian 12. Tidak ada SQL, tidak ada validasi. Kalau ada bug di aturan harga, Anda tahu persis harus membuka file mana.

    Merangkai dependensi tanpa framework

    Di bahasa lain, urusan seperti ini sering diserahkan ke library dependency injection. Di Go, caranya jauh lebih sederhana dan tidak butuh library sama sekali.

    Polanya cuma dua langkah. Pertama, simpan dependensi sebagai field di dalam struct. Kedua, buat fungsi constructor yang menerimanya dari luar. Itu yang sudah kita lakukan lewat NewRepository, NewService, dan NewHandler.

    Semua rangkaiannya dipasang di satu tempat: main.go.

    package main
    
    import (
    	"log"
    	"net/http"
    
    	"github.com/vandy/tokoapi/internal/config"
    	"github.com/vandy/tokoapi/internal/middleware"
    	"github.com/vandy/tokoapi/internal/produk"
    )
    
    func main() {
    	cfg := config.Muat()
    
    	db, err := config.BukaDB(cfg.DatabaseURL)
    	if err != nil {
    		log.Fatalf("gagal konek database: %v", err)
    	}
    	defer db.Close()
    
    	repo := produk.NewRepository(db)
    	svc := produk.NewService(repo)
    	h := produk.NewHandler(svc)
    
    	mux := http.NewServeMux()
    	mux.HandleFunc("POST /produk", h.Buat)
    	mux.HandleFunc("GET /produk/{id}", h.Detail)
    
    	handler := middleware.Log(middleware.Recover(mux))
    
    	log.Printf("server jalan di %s", cfg.Alamat)
    	if err := http.ListenAndServe(cfg.Alamat, handler); err != nil {
    		log.Fatal(err)
    	}
    }
    

    Sekarang main.go bisa dibaca dalam satu tarikan napas. Baca config, buka database, rangkai tiga lapis, daftarkan route, pasang middleware dari bagian 13, nyalakan server. Orang baru yang bergabung ke tim bisa memahami alur aplikasi hanya dari file ini.

    Kenapa struktur ini bikin testing gampang

    Di bagian 14 kita menulis test dan sempat repot karena kodenya menempel ke database. Struktur baru ini menyelesaikan masalah itu.

    Kuncinya ada di interface PenyimpanProduk tadi. Karena Service menyimpan interface, bukan struct konkret, kita bisa memberinya repository palsu saat testing. Ini pemakaian nyata dari interface yang kita pelajari di bagian 9.

    package produk
    
    import (
    	"context"
    	"errors"
    	"testing"
    )
    
    type repoPalsu struct {
    	tersimpan Produk
    }
    
    func (r *repoPalsu) Ambil(ctx context.Context, id int64) (Produk, error) {
    	return Produk{ID: id, Nama: "Kopi Susu", Harga: 18000}, nil
    }
    
    func (r *repoPalsu) Simpan(ctx context.Context, p Produk) (Produk, error) {
    	p.ID = 1
    	r.tersimpan = p
    	return p, nil
    }
    
    func TestBuatMenolakNamaKosong(t *testing.T) {
    	svc := NewService(&repoPalsu{})
    
    	_, err := svc.Buat(context.Background(), Produk{Nama: "   ", Harga: 18000})
    
    	if !errors.Is(err, ErrNamaKosong) {
    		t.Fatalf("harusnya ErrNamaKosong, dapat: %v", err)
    	}
    }
    

    Test ini jalan dalam hitungan milidetik. Tidak ada database yang perlu dinyalakan, tidak ada tabel yang perlu dibersihkan. Anda bisa menjalankannya di laptop maupun di CI tanpa setup tambahan.

    Perhatikan juga bahwa repoPalsu tidak perlu mengimplementasikan seluruh method Repository. Cukup method yang ada di interface. Semakin kecil interfacenya, semakin mudah dipalsukan.

    Kapan struktur ini tidak perlu

    Bagian ini sering dilewatkan orang, padahal penting.

    Kalau program Anda cuma 200 baris, satu file main.go itu sah. Script yang mengubah CSV jadi JSON tidak butuh folder internal. Tool kecil yang memanggil satu API tidak butuh lapis service. Membuat lima lapis untuk kode sependek itu justru bikin Anda lebih lama menemukan sesuatu.

    Pegangan yang saya pakai kira-kira begini:

    • Di bawah 300 baris, satu file. Belum ada yang perlu dipisah.
    • Mulai ada dua atau tiga fitur. Pecah per fitur dulu, belum perlu tiga lapis penuh.
    • Sudah ada database, aturan bisnis, dan lebih dari satu orang yang mengerjakan. Baru pakai struktur tiga lapis seperti di atas.

    Struktur mengikuti kebutuhan, bukan gengsi. Kalau Anda tidak bisa menjelaskan kenapa sebuah folder ada, kemungkinan folder itu memang belum perlu ada.

    Konvensi penamaan file dan folder

    Go punya beberapa kebiasaan yang sebaiknya diikuti supaya kode Anda terasa familiar bagi programmer Go lain.

    • Nama folder dan package huruf kecil semua. Tanpa underscore, tanpa strip, tanpa huruf besar. Tulis produk, bukan Produk atau produk_service.
    • Nama package sebaiknya kata tunggal. config, middleware, produk. Hindari nama kosong makna seperti utils, helpers, atau common, karena isinya cepat berubah jadi tempat sampah.
    • Nama file boleh pakai underscore. Contohnya produk_handler.go. Tapi karena packagenya sudah bernama produk, cukup handler.go.
    • Akhiran _test.go wajib untuk file test. Ini aturan compiler, bukan selera.
    • Jangan mengulang nama package di nama tipe. Cukup produk.Service, jangan produk.ProdukService.

    Satu catatan jujur soal struktur folder. Tim Go tidak pernah merilis struktur project resmi. Repository populer bernama golang-standards/project-layout sering dikutip sebagai standar, padahal itu proyek komunitas dan banyak developer Go senior menganggapnya kelewat rumit untuk aplikasi biasa.

    Jadi jangan bingung kalau Anda menemukan lima tutorial dengan lima struktur berbeda. Yang penting bukan meniru persis, melainkan konsisten di dalam satu project dan punya alasan untuk setiap folder yang Anda buat.

    Rangkuman

    Merapikan project bukan soal estetika. Tujuannya supaya perubahan berikutnya lebih murah. Tiga hal yang perlu Anda bawa dari bagian ini: pisahkan berdasarkan tanggung jawab, rangkai dependensi lewat constructor di main.go, dan pakai interface supaya service bisa dites tanpa database.

    Coba refactor project Anda sendiri sekarang. Pindahkan satu lapis dulu, jalankan, baru lanjut.

    Di bagian 17 kita masuk ke topik yang selalu muncul begitu aplikasi dipakai banyak orang: Context di Go: Timeout, Pembatalan, dan Request yang Sehat. Parameter ctx yang sejak tadi kita bawa ke mana-mana akhirnya akan terpakai sungguhan di sana.

    Kalau Anda butuh bantuan menata ulang aplikasi Go yang sudah terlanjur besar atau ingin membangun sistem baru dengan fondasi yang benar sejak awal, tim kami siap membantu lewat layanan pengembangan sistem dan aplikasi.

  • Perintah Dasar Docker yang Wajib Dikuasai Pemula

    Perintah Dasar Docker yang Wajib Dikuasai Pemula

    Perintah dasar Docker yang benar-benar kamu pakai setiap hari sebenarnya tidak banyak. Intinya ada di daftar ini: docker ps -a untuk melihat semua container, docker images untuk melihat image, docker stop, docker start, dan docker restart untuk mengatur hidup matinya container, docker rm dan docker rmi untuk bersih-bersih, lalu docker logs, docker inspect, dan docker system df untuk memeriksa kondisi. Kuasai belasan perintah ini dan kamu sudah bisa mengelola container harian tanpa panik.

    Artikel ini bagian ketiga dari seri Belajar Docker dari Nol. Kita tidak akan menghafal semua perintah satu per satu. Kita jalankan tiga container sekaligus, periksa kondisinya, lalu bersihkan semuanya dengan urutan yang benar. Cara ini lebih nempel di kepala daripada membaca daftar perintah panjang.

    Prasyarat Sebelum Latihan Perintah Docker

    Pastikan Docker sudah terinstal dan bisa jalan tanpa error. Di tutorial ini saya memakai Docker Engine 28.3 di Ubuntu 24.04, tapi semua perintah di sini sama saja di Windows dengan Docker Desktop atau di macOS. Cek dulu versimu:

    docker --version

    Output yang diharapkan kurang lebih seperti ini:

    Docker version 28.3.2, build 578ccf6

    Kamu juga perlu paham beda image dan container, karena hampir semua perintah di artikel ini menyasar salah satunya. Kalau masih ragu bedanya, baca dulu bagian sebelumnya: Perbedaan Docker Image dan Container + Praktik Pertama. Singkatnya, image itu cetakan, container itu hasil cetakannya yang berjalan.

    Peta Perintah Dasar Docker untuk Pemakaian Harian

    Supaya tidak bingung perintah mana untuk apa, pakai tabel ini sebagai peta. Kolom target penting: banyak pemula tertukar antara perintah untuk container dan perintah untuk image.

    Perintah Target Fungsi
    docker ps Container Melihat container yang sedang berjalan
    docker ps -a Container Melihat semua container, termasuk yang sudah berhenti
    docker images Image Melihat daftar image di komputer
    docker stop Container Menghentikan container yang berjalan
    docker start Container Menyalakan lagi container yang berhenti
    docker restart Container Stop lalu start dalam satu perintah
    docker rm Container Menghapus container yang sudah berhenti
    docker rmi Image Menghapus image yang tidak dipakai container mana pun
    docker logs Container Melihat output aplikasi di dalam container
    docker inspect Keduanya Melihat detail lengkap dalam format JSON
    docker system df Disk Melihat total pemakaian disk oleh Docker

    Pola yang perlu kamu ingat: perintah berakhiran i seperti rmi bekerja pada image. Sisanya kebanyakan bekerja pada container. Urutan hidup container juga selalu sama: dibuat, berjalan, berhenti, dihapus. docker rm hanya mau menghapus container yang sudah di posisi berhenti, dan docker rmi hanya mau menghapus image yang sudah tidak dipakai container mana pun. Urutan ini yang akan kita praktikkan.

    Praktik: Jalankan Tiga Container Sekaligus

    Kita jalankan tiga container dari tiga image berbeda supaya terasa mengelola lebih dari satu layanan, mirip kondisi nyata di server. Jalankan tiga perintah ini satu per satu:

    docker run -d --name web-satu nginx:1.27-alpine
    docker run -d --name cache-satu redis:7.4-alpine
    docker run -d --name web-dua httpd:2.4-alpine

    Opsi -d membuat container berjalan di belakang layar, dan --name memberi nama supaya gampang dipanggil. Detail mode detach kita bahas di bagian berikutnya dari seri ini. Sekarang cek yang sedang berjalan:

    docker ps

    Outputnya kurang lebih begini, tiga baris untuk tiga container:

    CONTAINER ID   IMAGE               COMMAND                  STATUS          NAMES
    f3a1b2c4d5e6   httpd:2.4-alpine    "httpd-foreground"       Up 10 seconds   web-dua
    a9b8c7d6e5f4   redis:7.4-alpine    "docker-entrypoint.s…"   Up 25 seconds   cache-satu
    1c2d3e4f5a6b   nginx:1.27-alpine   "/docker-entrypoint.…"   Up 40 seconds   web-satu

    Sekarang hentikan salah satunya, lalu bandingkan docker ps dengan docker ps -a:

    docker stop web-dua
    docker ps
    docker ps -a

    docker ps hanya menampilkan dua container. Tapi docker ps -a tetap menampilkan tiga, dengan web-dua berstatus Exited (0). Ini konsep yang paling sering bikin pemula bingung: container yang berhenti itu tidak hilang. Dia masih ada di disk, lengkap dengan namanya. Kamu bisa menyalakannya lagi kapan saja:

    docker start web-dua

    Kalau aplikasi di dalam container terasa aneh dan kamu ingin muat ulang, pakai docker restart web-dua. Isinya tetap sama, prosesnya saja yang dimulai ulang.

    Bersihkan Semua Container dengan Urutan yang Benar

    Selesai latihan, bersihkan. Urutannya selalu stop dulu, baru hapus container, baru hapus image kalau memang tidak dibutuhkan lagi:

    docker stop web-satu cache-satu web-dua
    docker rm web-satu cache-satu web-dua
    docker rmi nginx:1.27-alpine redis:7.4-alpine httpd:2.4-alpine

    Perhatikan bahwa satu perintah bisa menerima banyak nama sekaligus. Setelah itu docker ps -a harus kosong dan docker images tidak lagi menampilkan ketiga image tadi. Kalau containermu banyak dan malas mengetik nama satu per satu, ada jurus cepat:

    docker stop $(docker ps -q)
    docker rm $(docker ps -aq)

    Opsi -q artinya quiet, hanya mencetak ID container. Hasilnya disuapkan ke stop dan rm. Hati-hati, perintah ini menyapu semua container di mesinmu, jadi jangan dipakai di server yang ada container pentingnya.

    Membaca Output docker logs dan docker inspect

    Dua perintah ini adalah mata kamu ke dalam container. Di pekerjaan tim Arrazy sehari-hari, saat backend Go atau Laravel milik klien berjalan di container dan ada yang aneh, dua perintah inilah yang pertama kami buka sebelum menebak-nebak. Jalankan satu container lagi untuk mencoba:

    docker run -d --name web-tes nginx:1.27-alpine
    docker logs web-tes

    docker logs menampilkan semua output yang dicetak aplikasi di dalam container. Untuk nginx, kamu akan melihat baris seperti ini:

    /docker-entrypoint.sh: Configuration complete; ready for start up
    2026/07/27 03:15:42 [notice] 1#1: start worker processes

    Kalau container mati mendadak, docker logs biasanya berisi pesan error penyebabnya. Ini tempat pertama untuk mencari petunjuk. Dua opsi yang sering dipakai: docker logs -f web-tes untuk mengikuti log secara live seperti tail -f, dan docker logs --tail 50 web-tes untuk mengambil 50 baris terakhir saja.

    Sementara docker inspect menampilkan konfigurasi dan status lengkap dalam format JSON. Outputnya panjang sekali, jadi biasanya kita ambil bagian tertentu dengan opsi --format:

    docker inspect --format '{{.State.Status}}' web-tes
    docker inspect --format '{{.NetworkSettings.IPAddress}}' web-tes

    Perintah pertama menjawab pertanyaan paling dasar: container ini sebenarnya hidup atau tidak. Outputnya satu kata seperti running atau exited. Perintah kedua mencetak alamat IP internal container, misalnya 172.17.0.2. Dua field lain yang berguna saat debugging: {{.State.ExitCode}} untuk tahu kode keluar container yang mati, dan {{.Config.Env}} untuk melihat environment variable yang aktif di dalamnya.

    Cek Pemakaian Disk dengan docker system df

    Biasakan menjalankan perintah ini sejak awal belajar, karena Docker diam-diam rakus disk. Setiap docker pull menambah image ratusan MB, dan container yang berhenti tetap menyita ruang. Banyak orang baru sadar saat disk server penuh dan aplikasi ikut tumbang.

    docker system df

    Outputnya seperti ini:

    TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
    Images          4         1         512.4MB   463.1MB (90%)
    Containers      2         1         24.5MB    12.1MB (49%)
    Local Volumes   1         0         88.9MB    88.9MB (100%)
    Build Cache     0         0         0B        0B

    Kolom paling penting adalah RECLAIMABLE, yaitu ruang yang bisa kamu ambil kembali karena dipakai oleh image dan container yang menganggur. Kalau angkanya sudah besar, bersihkan dengan:

    docker system prune

    Perintah ini menghapus semua container yang berhenti, network yang tidak terpakai, dan image tanpa tag. Docker akan minta konfirmasi dulu sebelum mengeksekusi. Yang perlu diwaspadai adalah varian docker system prune -a, karena ikut menghapus semua image yang sedang tidak dipakai container, termasuk image yang baru kamu pull dan berencana dipakai nanti.

    Troubleshooting: Error yang Sering Dialami Pemula

    Error “conflict: the container name is already in use”

    docker: Error response from daemon: Conflict. The container name "/web-satu"
    is already in use by container "1c2d3e4f5a6b".

    Penyebabnya hampir selalu sama: kamu pernah menjalankan container dengan nama itu, lalu menghentikannya, dan mengira dia sudah hilang. Padahal container berhenti masih ada dan namanya masih terkunci. Cek dengan docker ps -a, pasti ada container lama dengan nama tersebut. Solusinya pilih salah satu: hapus yang lama dengan docker rm web-satu lalu jalankan ulang, atau nyalakan lagi yang lama dengan docker start web-satu kalau isinya masih relevan.

    Error “cannot remove a running container” Saat docker rm

    Error response from daemon: cannot remove container "web-satu":
    container is running: stop the container before removing or force remove

    Docker menolak menghapus container yang masih hidup, dan itu perilaku yang bagus. Cara aman: docker stop web-satu dulu, baru docker rm web-satu. Ada jalan pintas docker rm -f web-satu yang langsung mematikan paksa lalu menghapus, tapi jadikan ini pilihan terakhir. Kill paksa tidak memberi kesempatan aplikasi menutup koneksi atau menyimpan data dengan rapi, dan itu berisiko untuk container database.

    Error “No such container” Padahal Baru Dibuat

    Biasanya karena dua hal. Pertama, salah ketik nama, cek ejaan persisnya lewat kolom NAMES di docker ps -a. Kedua, container memang gagal dibuat sejak awal karena perintah docker run sebelumnya error, tapi errornya tidak terbaca. Scroll ke atas dan baca pesan dari perintah run terakhirmu.

    Error “image is being used by stopped container” Saat docker rmi

    Error response from daemon: conflict: unable to delete nginx:1.27-alpine (must be forced)
    - image is being used by stopped container 1c2d3e4f5a6b

    Image tidak bisa dihapus selama masih ada container yang lahir darinya, walaupun containernya sudah berhenti. Ingat urutan bersih-bersih: hapus dulu containernya dengan docker rm, baru docker rmi akan berhasil.

    Rangkuman dan Lanjut ke Bagian Berikutnya

    Sampai sini kamu sudah pegang perintah dasar Docker yang dipakai hampir setiap hari: melihat kondisi lewat ps -a, images, logs, dan inspect, mengatur container lewat stop, start, restart, membersihkan lewat rm dan rmi dengan urutan yang benar, plus memantau disk lewat system df. Pola pikirnya sederhana: periksa dulu keadaan, baru bertindak. Kebiasaan yang sama kami pegang saat mengelola sistem aplikasi milik klien di server produksi.

    Di bagian berikutnya, “Docker Port Mapping, Mode Detach, dan Environment Variable”, kita bedah opsi -d, -p, dan -e yang tadi sempat kita pakai tanpa penjelasan mendalam, supaya container-mu bisa diakses dari browser dan dikonfigurasi tanpa mengubah image. Artikelnya terbit menyusul, pantau daftarnya di halaman Belajar Docker dari Nol.

    Referensi

  • Sertifikat Digital dan Penjurian Online: Cara Kerja Sistemnya

    Sertifikat Digital dan Penjurian Online: Cara Kerja Sistemnya

    Setelah peserta selesai mendaftar, sistem event sertifikasi menangani dua pekerjaan besar. Pertama, penjurian online. Juri memasukkan nilai lewat form, sistem merekap otomatis sesuai bobot kriteria, lalu ranking muncul real time. Kedua, sertifikat digital. Begitu hasil final, sertifikat digenerate otomatis dari template, dikirim massal ke peserta, dan dilengkapi nomor seri atau QR code yang bisa diverifikasi siapa saja.

    Dua tahap ini yang paling sering bikin panitia begadang. Tahap pendaftarannya sendiri sudah pernah kami bahas tuntas di artikel platform pendaftaran online sertifikasi, jadi di sini kita fokus ke apa yang terjadi sesudahnya: bagaimana nilai diolah dan bagaimana sertifikat sampai ke tangan peserta.

    Kenapa penjurian manual selalu berakhir kacau

    Pola kejadiannya hampir selalu sama. Setiap juri pegang lembar penilaian sendiri, entah kertas atau file Excel masing-masing. Selama acara berlangsung semuanya terasa aman. Masalah baru muncul saat rekap.

    • Panitia harus mengumpulkan file dari semua juri, menyalin angka satu per satu ke rekap induk, biasanya tengah malam setelah acara selesai.
    • Salah ketik satu angka tidak ketahuan. Nilai 78 tersalin jadi 87, ranking berubah, dan tidak ada yang sadar sampai hasil diumumkan.
    • Format tiap juri beda. Ada yang isi desimal, ada yang bulat, ada yang lupa mengisi satu kriteria. Panitia menebak-nebak maksudnya.
    • Saat peserta protes hasil, panitia tidak punya jejak. Siapa yang memberi nilai berapa, kapan diinput, apakah pernah diubah, semuanya gelap.

    Untuk event internal kecil, kekacauan ini masih bisa ditoleransi. Untuk program sertifikasi yang membawa nama lembaga, satu kesalahan rekap bisa jadi masalah kredibilitas yang panjang.

    Cara kerja penjurian online

    Sistem penjurian online pada dasarnya memindahkan lembar nilai juri ke satu database terpusat. Alurnya seperti ini.

    Form nilai per juri per peserta

    Setiap juri login dengan akun sendiri, lalu melihat daftar peserta yang jadi tanggung jawabnya. Untuk tiap peserta, juri mengisi form dengan kriteria yang sudah ditetapkan panitia. Form ini bisa membatasi rentang nilai, mewajibkan semua kriteria terisi, dan menolak input yang aneh. Kesalahan format hilang di sumbernya, bukan diperbaiki belakangan.

    Bobot kriteria dihitung sistem

    Misalnya kelancaran 40 persen, tajwid 35 persen, adab 25 persen. Rumusnya disetel sekali di awal, lalu sistem yang menghitung nilai akhir setiap peserta. Tidak ada lagi rumus Excel yang tergeser satu baris dan merusak seluruh kolom.

    Rekap dan ranking real time

    Begitu juri menekan simpan, nilai langsung masuk rekap. Panitia bisa memantau progres dari dashboard: juri mana yang sudah selesai menilai, peserta mana yang nilainya belum lengkap, dan ranking sementara saat itu juga. Rekap tengah malam berubah jadi sekadar membuka satu halaman.

    Jejak audit

    Setiap input tercatat: siapa juri yang menilai, nilai berapa, jam berapa, dan apakah pernah direvisi. Saat ada peserta yang keberatan dengan hasil, panitia tinggal membuka riwayatnya. Ini juga melindungi juri, karena penilaian mereka terdokumentasi apa adanya.

    Model penilaiannya sendiri fleksibel. Ada event yang jurinya menilai langsung di lokasi sambil peserta tampil. Ada juga yang penilaiannya asinkron, misalnya juri menonton rekaman atau memeriksa berkas peserta dari kotanya masing-masing dalam rentang beberapa hari. Dua-duanya bisa jalan di sistem yang sama, karena intinya tetap satu: semua nilai bermuara ke satu database, bukan tersebar di file pribadi tiap juri.

    Sertifikat digital yang digenerate otomatis

    Setelah nilai final, pekerjaan berikutnya adalah sertifikat. Cara manualnya kita semua tahu: buka file desain, ganti nama satu per satu, export PDF, ulangi ratusan kali, lalu kirim satu per satu lewat email. Untuk 500 peserta, itu pekerjaan berhari-hari dengan peluang salah ketik nama di mana-mana.

    Sistem sertifikat digital bekerja dengan tiga komponen.

    • Template. Desain sertifikat dibuat sekali, dengan placeholder untuk nama peserta, skor atau predikat, nomor sertifikat, dan tanggal. Data diambil langsung dari database, jadi nama di sertifikat sama persis dengan nama saat pendaftaran.
    • Pengiriman massal. Sertifikat dikirim otomatis ke email atau WhatsApp peserta, atau peserta login dan mengunduh sendiri dari akunnya. Ratusan sertifikat terkirim dalam hitungan menit, bukan hari.
    • Nomor seri unik. Setiap sertifikat punya nomor yang tercatat di database. Ini yang membedakan sertifikat digital dari sekadar PDF berdesain bagus, karena nomor inilah pintu masuk verifikasi.

    Efek sampingnya yang jarang disadari: panitia jadi punya arsip permanen. Peserta yang kehilangan file sertifikatnya dua tahun kemudian tinggal minta dikirim ulang atau mengunduh sendiri, tanpa panitia harus membongkar folder desain lama. Semua sertifikat yang pernah terbit tercatat, lengkap dengan siapa penerimanya dan kapan diterbitkan.

    Verifikasi keaslian, fitur yang sering diremehkan

    Sertifikat berbentuk PDF atau gambar sangat mudah dipalsukan. Siapa pun yang bisa memakai aplikasi edit gambar bisa mengganti nama di sertifikat orang lain. Kalau sertifikat lembaga Anda dipakai orang untuk melamar kerja atau mendaftar program lain, pihak penerima tidak punya cara mengecek keasliannya. Yang dirugikan bukan cuma penerima, tapi nama lembaga Anda sendiri.

    Solusinya sederhana: halaman verifikasi publik. Setiap sertifikat memuat QR code atau nomor seri. Siapa pun bisa memindai QR itu atau mengetik nomornya di website penyelenggara, lalu sistem menampilkan data aslinya. Nama peserta, program yang diikuti, tanggal, dan status kelulusan. Kalau nomor tidak ditemukan atau datanya beda, berarti sertifikat itu palsu.

    Buat lembaga yang serius membangun reputasi, fitur ini nilainya besar. Sertifikat Anda jadi bisa dipercaya pihak ketiga tanpa harus menghubungi panitia. Semakin mudah diverifikasi, semakin tinggi bobot sertifikat itu di mata orang lain. Halaman verifikasi juga bekerja dua arah: lembaga bisa mencabut atau menandai sertifikat yang bermasalah, dan status terbarunya langsung terlihat oleh siapa pun yang mengecek.

    Studi kasus: Sertifikasi Hafiz Indonesia Emas

    Salah satu contoh nyata sistem event sertifikasi yang kami bangun adalah platform untuk Yayasan Hafiz Indonesia Emas. Programnya berskala nasional, dan sebelum ada sistem terpusat, risikonya jelas: informasi mudah tercecer dan kerja admin lambat.

    Platform yang kami kembangkan mencakup landing page informasi program, form pendaftaran online supaya data peserta masuk terstruktur, admin dashboard untuk verifikasi dan monitoring peserta, plus CMS agar tim yayasan bisa memperbarui konten program sendiri. Sistemnya dibangun dengan Vue 3, Golang, dan MySQL, dan sudah live dipakai.

    Yang menarik dari kasus ini: fondasi data peserta yang rapi adalah syarat mutlak sebelum bicara penjurian dan sertifikat. Rekap nilai hanya akurat kalau daftar pesertanya akurat. Sertifikat hanya benar kalau nama di database benar sejak awal. Detail lengkapnya bisa dibaca di halaman portofolio Sertifikasi Hafiz Indonesia Emas.

    Kapan spreadsheet masih cukup

    Tidak semua event butuh sistem. Kalau kondisi Anda seperti ini, Excel dan Google Form masih masuk akal.

    • Event internal dengan peserta di bawah 50 orang dan hanya berjalan sekali.
    • Jurinya satu atau dua orang yang duduk bersebelahan, jadi rekap bisa dicek langsung di tempat.
    • Sertifikatnya formalitas internal yang tidak akan dipakai peserta di luar organisasi.

    Sebaliknya, mulai pertimbangkan sistem kalau salah satu ini terjadi: peserta ratusan orang, juri lebih dari tiga dan menilai dari lokasi berbeda, event berulang tiap periode, atau sertifikatnya dipakai peserta sebagai bukti kompetensi di luar. Di titik itu, biaya membangun sistem hampir selalu lebih murah daripada biaya reputasi karena salah rekap atau sertifikat yang gampang dipalsukan.

    Pertanyaan yang sering muncul

    Apakah juri harus paham teknologi?

    Tidak. Form penilaian yang baik dirancang semirip mungkin dengan lembar nilai kertas: daftar peserta, kolom kriteria, tombol simpan. Kalau juri bisa mengisi Google Form, mereka bisa memakai sistem penjurian. Sesi orientasi singkat sebelum acara biasanya sudah cukup.

    Bagaimana kalau internet juri putus saat menilai?

    Ada beberapa lapisan pengaman. Nilai yang sudah disimpan tetap aman di server, jadi yang berisiko hanya input yang sedang berjalan. Sistem bisa menyimpan draft berkala, dan admin bisa diberi akses menginput ulang dari catatan cadangan juri kalau benar-benar darurat. Praktik yang umum, panitia menyediakan koneksi cadangan di lokasi penjurian.

    Format sertifikat digital apa saja yang bisa dibuat?

    Umumnya PDF, karena mudah diunduh, dicetak, dan dilampirkan ke lamaran. Bisa juga gambar PNG atau JPG untuk dibagikan di media sosial. Yang lebih penting dari formatnya adalah kelengkapannya: nama sesuai data resmi, nomor seri unik, dan QR code yang mengarah ke halaman verifikasi.

    Mau diskusi soal sistem event Anda?

    Setiap program sertifikasi punya alur penilaian yang beda. Ada yang jurinya tersebar di banyak kota, ada yang kriterianya berlapis, ada yang butuh sertifikat terbit di hari yang sama. Kalau Anda sedang merancang event sertifikasi, lomba, atau pelatihan dan ingin tahu sistem seperti apa yang masuk akal untuk skala Anda, ceritakan saja kebutuhannya lewat halaman kontak Arrazy. Kami bantu petakan dulu alurnya sebelum bicara soal biaya.

  • Apa Itu MVP: Mulai dari Versi Kecil Biar Aplikasimu Tidak Boncos

    Apa Itu MVP: Mulai dari Versi Kecil Biar Aplikasimu Tidak Boncos

    MVP atau minimum viable product itu versi paling kecil dari aplikasimu yang sudah bisa dipakai user beneran untuk menyelesaikan satu masalah inti. Kata kuncinya ada di “bisa dipakai beneran”. MVP bukan aplikasi setengah jadi yang tombolnya banyak tapi setengahnya belum berfungsi. MVP adalah aplikasi kecil yang utuh. Fiturnya sedikit, tapi satu alur pentingnya jalan dari awal sampai akhir.

    Kenapa konsep ini penting buat kamu yang punya ide aplikasi? Karena kesalahan paling mahal dalam membangun aplikasi bukan salah pilih teknologi atau salah pilih vendor. Kesalahan paling mahal adalah membangun terlalu banyak fitur sebelum tahu apakah ada orang yang mau memakainya. MVP adalah cara mengecilkan taruhan itu. Kamu keluar uang secukupnya dulu, belajar dari user nyata, baru menambah fitur kalau memang terbukti dibutuhkan.

    Pola gagal yang terlalu sering terjadi

    Ceritanya hampir selalu sama. Sebuah bisnis punya ide aplikasi. Semangatnya tinggi, jadi daftar fiturnya panjang. Login dengan tiga cara, dashboard admin lengkap, notifikasi otomatis, sistem poin, laporan bisa diekspor ke Excel, aplikasi Android dan iOS sekaligus. Semua diminta masuk versi 1 karena “sekalian saja, biar tidak bolak balik”.

    Yang terjadi kemudian bisa ditebak. Scope sebesar itu butuh waktu lama. Rilis yang dijanjikan empat bulan molor jadi setahun. Di tengah jalan muncul revisi, budget bengkak, dan semangat mulai habis. Pas akhirnya rilis, data pemakaian menunjukkan hal yang menyakitkan. User cuma memakai sebagian kecil fitur. Sisanya, yang menghabiskan sebagian besar biaya dan waktu, hampir tidak pernah disentuh.

    Ini bukan cerita satu dua perusahaan. Ini pola. Dan pahitnya, uang yang sudah terlanjur keluar untuk fitur yang tidak dipakai itu tidak bisa ditarik kembali. Itulah yang orang sebut boncos.

    Cara menentukan fitur inti MVP

    Pertanyaan kuncinya sederhana. Kalau aplikasimu cuma boleh punya satu alur, alur mana yang bikin orang mau bayar atau mau pakai? Jawaban dari pertanyaan itu adalah fitur inti MVP kamu. Semua yang lain bisa antre.

    Biar kebayang, dua contoh konkret:

    • Aplikasi laundry. Alur yang bikin pelanggan senang adalah pesanan tercatat rapi dan mereka dapat nota digital yang jelas. Jadi MVP-nya cukup terima pesanan dan nota digital. Sistem poin reward, langganan bulanan, dan promo otomatis belum perlu. Itu semua baru berguna kalau pelanggannya sudah ada dan sudah balik lagi.
    • Marketplace produk lokal. Alur intinya adalah pembeli menemukan produk dan bisa memesan. MVP-nya cukup katalog produk plus tombol order lewat WA. Payment gateway, wallet, dan sistem rating penjual bisa menyusul. Kalau lewat WA saja tidak ada yang order, payment gateway hanya akan jadi fitur mahal yang menganggur.

    Perhatikan polanya. MVP bukan versi murahan dari ide besarmu. MVP adalah jawaban jujur atas pertanyaan “masalah mana yang paling sakit buat user, dan apa cara paling langsung untuk menyelesaikannya”.

    Cara praktis melakukannya: tulis semua fitur yang kamu bayangkan di satu daftar. Lalu untuk tiap fitur, tanya “kalau fitur ini tidak ada, apakah user tetap bisa menyelesaikan masalah intinya?” Kalau jawabannya ya, coret dari versi 1. Kamu akan kaget betapa pendek daftar yang tersisa. Dan itu bagus, karena daftar pendek itu bisa rilis dalam hitungan minggu, bukan tahun.

    Fitur yang hampir selalu bisa ditunda

    Dari pengalaman kami mengerjakan sistem dan aplikasi custom untuk berbagai bisnis, ada beberapa jenis fitur yang hampir selalu bisa digeser ke versi berikutnya tanpa mengurangi nilai MVP:

    • Notifikasi canggih. Push notification terjadwal, reminder otomatis, email digest. Di awal, notifikasi manual atau WA biasa sudah cukup.
    • Dashboard analitik lengkap. Grafik tren, filter multi dimensi, ekspor laporan. Selama datanya tersimpan rapi, laporan bisa ditarik manual dulu. Dashboard cantik menyusul saat datanya sudah banyak.
    • Multi bahasa. Kecuali pasarmu memang lintas negara dari hari pertama, satu bahasa dulu.
    • Integrasi macam macam. Sinkron ke akuntansi, ke marketplace, ke sistem lain. Tiap integrasi itu proyek kecil sendiri. Tunda sampai alur intinya terbukti dipakai.
    • Role dan permission berlapis. Admin, supervisor, staf, viewer, masing masing dengan hak akses detail. Di MVP, dua role biasanya cukup: admin dan user.

    Fitur di daftar ini bukan fitur jelek. Sebagian besar memang akan dibutuhkan nanti. Poinnya adalah urutan. Semua fitur ini baru bernilai setelah alur intinya hidup dan dipakai orang.

    Yang justru tidak boleh dihemat

    Nah, ini bagian yang sering disalahpahami. MVP itu minimum di fitur, bukan minimum di kualitas. Ada tiga hal yang tidak boleh ikut dipangkas:

    • Keamanan dasar. Password yang di-hash, akses data yang dibatasi, form yang divalidasi. Kebocoran data di aplikasi kecil tetap kebocoran data. Reputasi bisnismu tidak kenal istilah “masih MVP”.
    • Backup. Data pesanan dan data pelanggan adalah aset. Backup rutin itu murah. Kehilangan data karena server bermasalah itu mahal, kadang tidak tergantikan.
    • Fondasi kode yang bisa dikembangkan. Struktur kode yang rapi, database yang didesain dengan sadar, teknologi yang wajar. Ini yang membedakan MVP dengan prototype asal jadi.

    Poin ketiga ini penting sekali. Ada jenis MVP murahan yang dibuat asal jalan, tanpa memikirkan versi 2. Kelihatannya hemat di awal. Tapi begitu bisnismu mau menambah fitur, vendor mana pun yang melihat kodenya akan bilang hal yang sama: lebih cepat bikin ulang dari nol daripada menambal. Artinya uang untuk versi 1 hangus total, dan kamu bayar dua kali. MVP yang benar itu seperti rumah tumbuh. Kecil dulu tidak apa apa, tapi pondasinya harus siap menerima lantai berikutnya.

    Ini juga salah satu alasan penawaran antar vendor bisa terlihat jomplang untuk scope yang kelihatannya sama, dan kami pernah membahasnya lebih dalam di artikel kenapa harga aplikasi custom beda jauh antar vendor.

    Setelah MVP rilis, kerja belum selesai

    Rilis MVP itu bukan garis finish. Justru di sinilah nilai sebenarnya dari pendekatan ini mulai terasa. Setelah rilis, ada tiga hal yang perlu kamu lakukan:

    • Ukur pemakaian nyata. Fitur mana yang dipakai, di mana user berhenti, alur mana yang membingungkan. Data ini jauh lebih jujur daripada asumsi di rapat.
    • Dengar user langsung. Tanya sepuluh user pertama apa yang bikin mereka bertahan dan apa yang bikin kesal. Keluhan yang sama muncul berulang? Itu kandidat fitur versi 2.
    • Tambah fitur bertahap. Satu atau dua fitur per rilis, berdasarkan data dan masukan tadi. Bukan berdasarkan daftar panjang yang dulu dicoret.

    Dengan cara ini, versi 2 dibiayai oleh pelajaran dari versi 1. Kalau MVP-mu sudah menghasilkan pemasukan, lebih bagus lagi, karena pengembangan berikutnya bisa dibiayai aplikasinya sendiri. Bandingkan dengan pola gagal di awal artikel tadi. Di sana semua uang dipertaruhkan sebelum ada satu pun pelajaran masuk. Di sini tiap rupiah berikutnya dikeluarkan setelah ada bukti.

    Ada bonus yang jarang disadari. Fitur yang lahir dari permintaan user nyata hampir selalu lebih dipakai daripada fitur yang lahir dari brainstorming internal. Jadi selain lebih hemat, aplikasimu juga jadi lebih tepat sasaran.

    Pertanyaan yang sering muncul soal MVP

    MVP kira kira berapa lama dibuat?

    Tergantung kompleksitas alur intinya, tapi umumnya jauh lebih singkat daripada aplikasi full. MVP dengan satu alur inti yang jelas biasanya bisa selesai dalam hitungan minggu sampai dua tiga bulan. Bandingkan dengan proyek semua-fitur-sekaligus yang gampang molor melewati satu tahun. Semakin tajam kamu mendefinisikan satu alur inti, semakin cepat MVP-mu bisa dipakai orang.

    Apakah MVP berarti aplikasinya jelek?

    Tidak. MVP yang benar itu kecil tapi utuh. Alur intinya jalan mulus, tampilannya layak, datanya aman. Yang dikurangi hanyalah jumlah fitur, bukan kualitas dari fitur yang ada. Kalau ada yang menawarkan MVP dengan alasan boleh asal jadi karena “namanya juga minimum”, itu salah paham terhadap konsepnya.

    Kapan MVP tidak cocok dipakai?

    Ada beberapa kasus. Kalau kamu membangun sistem internal yang kebutuhannya sudah pasti dan prosesnya sudah baku bertahun tahun, eksplorasi ala MVP kurang relevan, langsung saja bangun sesuai proses yang ada. Begitu juga aplikasi di ranah yang regulasinya ketat, misalnya yang menyangkut data kesehatan atau transaksi keuangan berizin, karena standar minimumnya sudah ditentukan regulator, bukan olehmu. Untuk kebanyakan ide aplikasi bisnis lainnya, mulai dari MVP hampir selalu pilihan yang lebih aman.

    Mulai dari kecil itu bukan kompromi

    Mulai dari versi kecil bukan tanda idemu diturunkan kelasnya. Justru itu cara paling waras memperlakukan uang sendiri. Ide besarmu tetap jadi tujuan. MVP hanya memastikan jalannya ke sana dibayar per tahap, dengan bukti di tiap tahapnya, bukan sekali bayar besar di depan berdasarkan tebakan.

    Kalau kamu sedang di titik ini, punya ide aplikasi tapi ragu fitur mana yang masuk versi 1 dan mana yang bisa ditunda, kami bisa bantu memilahnya. Ceritakan idemu lewat halaman kontak Arrazy dan kita diskusikan scoping MVP-nya bersama, tanpa biaya dan tanpa keharusan lanjut. Lebih enak ngobrol dulu sebelum keluar uang daripada menyesal setelahnya.

  • Belajar Golang dari Nol #15: Deploy API Go ke Server Production

    Belajar Golang dari Nol #15: Deploy API Go ke Server Production

    Di Belajar Golang dari Nol #14 kita menutup API produk dengan testing. Database sudah jalan, middleware auth sudah pasang, dan test-nya hijau semua. Tapi API itu masih hidup di satu tempat: laptop kamu.

    Bagian ini kita pindahkan ke server. Target akhirnya jelas. API bisa diakses dari internet lewat domain, pakai HTTPS, tetap hidup walau SSH ditutup, dan otomatis nyala lagi kalau server reboot.

    Saya tulis langkahnya berurutan. Ikuti pelan-pelan. Setiap langkah ada cara mengeceknya sebelum lanjut ke langkah berikutnya.

    Kenapa deploy aplikasi Go itu jauh lebih santai

    Ingat bagian 1 waktu kita bahas kenapa Go dipilih? Salah satu alasannya muncul persis di titik ini. Go itu bahasa yang dikompilasi jadi satu file binary. Bukan script yang butuh interpreter, bukan bytecode yang butuh virtual machine.

    Artinya di server kamu tidak perlu install Go. Tidak perlu install runtime apa pun. Tidak ada folder vendor yang harus ikut diupload. Tidak ada perintah install dependency di server yang bisa gagal karena versi beda.

    Bandingkan dengan aplikasi PHP atau Node.js. Di sana kamu perlu install PHP dengan ekstensi yang cocok, atau Node dengan versi yang cocok, lalu jalankan composer install atau npm install di server. Setiap komponen itu satu kemungkinan gagal.

    Di Go, yang kamu kirim ke server cuma satu file. Itu saja. Sisanya tinggal mengatur cara menjalankannya.

    Build binary untuk server Linux

    Server kamu hampir pasti pakai Linux dengan arsitektur amd64. Laptop kamu mungkin macOS, mungkin Windows, mungkin Linux juga. Tidak masalah. Go bisa membuat binary untuk sistem lain langsung dari laptop kamu. Ini namanya cross compile.

    Caranya cukup mengatur dua variabel sebelum perintah build:

    GOOS=linux GOARCH=amd64 go build -o api-produk

    GOOS menentukan sistem operasi target, GOARCH menentukan arsitektur prosesornya. Hasilnya file bernama api-produk yang siap jalan di server Linux, walaupun kamu build dari MacBook.

    Kalau server kamu pakai prosesor ARM, misalnya beberapa VPS murah sekarang, ganti jadi GOARCH=arm64. Cek dulu di server dengan perintah uname -m. Kalau hasilnya x86_64 berarti amd64. Kalau aarch64 berarti arm64.

    Di Windows PowerShell sintaksnya sedikit berbeda:

    $env:GOOS="linux"; $env:GOARCH="amd64"; go build -o api-produk

    Mengecilkan ukuran binary

    Binary Go biasanya berukuran belasan sampai puluhan megabyte. Itu wajar karena semua yang dibutuhkan sudah ikut di dalamnya. Kalau mau lebih ramping, buang informasi debug:

    GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o api-produk

    Flag -s membuang tabel simbol, -w membuang informasi DWARF. Ukuran biasanya turun sekitar 25 sampai 30 persen. Ini opsional. Efek sampingnya, stack trace saat panic jadi kurang detail. Untuk API kecil yang lognya sudah rapi, ini pertukaran yang masuk akal.

    Siapkan kode untuk production

    Sebelum diupload, ada satu hal yang harus dibereskan di kode. Selama 14 bagian sebelumnya kita menulis port dan koneksi database langsung di dalam kode. Untuk belajar itu tidak apa-apa. Untuk production itu masalah.

    Kenapa? Karena kredensial database production tidak boleh masuk ke Git. Dan karena port di server bisa saja berbeda dengan di laptop. Kalau nilai-nilai ini di-hardcode, kamu harus edit kode dan build ulang setiap kali konfigurasinya berubah.

    Solusinya pakai environment variable. Bikin satu helper kecil yang membaca env dengan nilai cadangan:

    func getEnv(key, fallback string) string {
    	if value := os.Getenv(key); value != "" {
    		return value
    	}
    	return fallback
    }

    Lalu pakai di fungsi main:

    func main() {
    	port := getEnv("PORT", "8080")
    	dsn := getEnv("DB_DSN", "root:@tcp(127.0.0.1:3306)/toko?parseTime=true")
    
    	db, err := sql.Open("mysql", dsn)
    	if err != nil {
    		log.Fatal("gagal koneksi database: ", err)
    	}
    	defer db.Close()
    
    	if err := db.Ping(); err != nil {
    		log.Fatal("database tidak merespons: ", err)
    	}
    
    	log.Printf("server jalan di port %s", port)
    	log.Fatal(http.ListenAndServe(":"+port, router(db)))
    }

    Sekarang di laptop kamu tetap bisa jalan tanpa set apa-apa karena ada nilai default. Di server, nilai aslinya nanti diisi lewat file environment yang kita buat sebentar lagi.

    Satu catatan penting. Jangan lupa db.Ping(). Fungsi sql.Open tidak benar-benar menyambung ke database, dia cuma menyiapkan koneksi. Tanpa Ping, aplikasi kamu akan terlihat sukses jalan padahal kredensialnya salah.

    Kirim binary ke server

    Sekarang upload. Pakai scp dari folder tempat binary tadi dibuat:

    scp api-produk deploy@103.10.20.30:/home/deploy/

    Ganti deploy dengan user di server kamu dan IP-nya dengan IP VPS kamu. Setelah selesai, masuk ke server:

    ssh deploy@103.10.20.30

    Beri izin eksekusi, lalu jalankan langsung untuk tes pertama:

    chmod +x /home/deploy/api-produk
    DB_DSN="user:password@tcp(127.0.0.1:3306)/toko?parseTime=true" ./api-produk

    Kalau muncul log “server jalan di port 8080”, berarti binary-nya sehat. Biarkan terminal itu terbuka. Buka terminal baru di laptop kamu dan tes dari luar:

    curl http://103.10.20.30:8080/produk

    Kalau balasannya JSON produk, selamat, API kamu sudah hidup di internet. Kalau timeout, kemungkinan besar firewall server belum membuka port 8080. Tidak perlu dibuka permanen, karena sebentar lagi Nginx yang akan menangani lalu lintas dari luar.

    Sekarang tekan Ctrl+C di server untuk mematikannya. Kita akan jalankan dengan cara yang benar.

    Bikin API tetap hidup dengan systemd

    Masalah dari ./api-produk tadi jelas. Begitu SSH ditutup, prosesnya ikut mati. Kalau aplikasi crash, tidak ada yang menghidupkan lagi. Kalau server reboot, API kamu diam saja.

    Jawabannya systemd. Ini pengelola service bawaan hampir semua Linux modern. Dia yang akan menyalakan, mengawasi, dan menghidupkan ulang aplikasi kamu.

    Pertama, simpan kredensial di file environment terpisah:

    sudo nano /etc/api-produk.env

    Isinya begini, satu baris satu variabel, tanpa spasi di sekitar tanda sama dengan dan tanpa tanda kutip:

    PORT=8080
    DB_DSN=user:password@tcp(127.0.0.1:3306)/toko?parseTime=true

    Kunci filenya supaya tidak bisa dibaca user lain:

    sudo chmod 640 /etc/api-produk.env
    sudo chown root:deploy /etc/api-produk.env

    Sekarang buat file unit systemd:

    sudo nano /etc/systemd/system/api-produk.service

    Isi lengkapnya:

    [Unit]
    Description=API Produk Go
    After=network.target mysql.service
    
    [Service]
    Type=simple
    User=deploy
    Group=deploy
    WorkingDirectory=/home/deploy
    EnvironmentFile=/etc/api-produk.env
    ExecStart=/home/deploy/api-produk
    Restart=always
    RestartSec=5
    StandardOutput=journal
    StandardError=journal
    
    [Install]
    WantedBy=multi-user.target

    Mari bedah bagian yang penting.

    • After=network.target menunda start sampai jaringan siap.
    • User=deploy menjalankan aplikasi sebagai user biasa, bukan root. Ini penting untuk keamanan. Kalau aplikasi kamu dibobol, penyerang tidak langsung dapat akses root.
    • WorkingDirectory menentukan folder kerja. Berguna kalau aplikasi kamu membaca file relatif, misalnya template atau file migrasi.
    • EnvironmentFile menyuntikkan isi file env tadi ke proses.
    • ExecStart harus pakai path absolut. Systemd tidak mengenal path relatif.
    • Restart=always menghidupkan ulang aplikasi kalau mati karena alasan apa pun. RestartSec=5 memberi jeda 5 detik supaya tidak restart membabi buta saat database sedang bermasalah.
    • WantedBy=multi-user.target yang membuat service ikut nyala saat server boot.

    Muat ulang konfigurasi, aktifkan, lalu jalankan:

    sudo systemctl daemon-reload
    sudo systemctl enable api-produk
    sudo systemctl start api-produk
    sudo systemctl status api-produk

    Perintah status harus menampilkan active (running) berwarna hijau. Kalau merah, jangan panik. Baca lognya:

    sudo journalctl -u api-produk -n 50 --no-pager

    Tambahkan -f kalau mau memantau log secara langsung sambil mengetes API:

    sudo journalctl -u api-produk -f

    Semua yang kamu tulis dengan log.Printf di kode Go akan muncul di sini. Ini yang menggantikan terminal yang biasa kamu lihat waktu development.

    Nginx di depan sebagai reverse proxy

    API kamu sekarang jalan di port 8080. Tapi orang tidak mau mengetik nomor port. Mereka mau mengetik nama domain.

    Di sini Nginx masuk. Dia duduk di depan, menerima permintaan di port 80 dan 443, lalu meneruskannya ke aplikasi Go di port 8080. Ada tiga alasan kenapa ini layak:

    • Port 80 dan 443 butuh hak root. Aplikasi Go kamu jalan sebagai user biasa, jadi tidak boleh memakainya langsung.
    • Nginx yang mengurus sertifikat TLS. Kode Go kamu tidak perlu tahu soal HTTPS sama sekali.
    • Satu server bisa menampung banyak domain dan banyak aplikasi sekaligus.

    Buat konfigurasinya:

    sudo nano /etc/nginx/sites-available/api-produk

    Isi dengan server block sederhana:

    server {
        listen 80;
        server_name api.domainkamu.com;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

    Empat header itu bukan formalitas. Tanpa X-Real-IP dan X-Forwarded-For, aplikasi Go kamu akan melihat semua pengunjung datang dari 127.0.0.1. Rate limiting dan log akses jadi tidak berguna.

    Aktifkan konfigurasinya, cek dulu sintaksnya, baru muat ulang:

    sudo ln -s /etc/nginx/sites-available/api-produk /etc/nginx/sites-enabled/
    sudo nginx -t
    sudo systemctl reload nginx

    Selalu jalankan nginx -t sebelum reload. Kalau ada typo, dia akan bilang di baris berapa, dan Nginx yang sedang jalan tidak ikut tumbang.

    Pastikan domain kamu sudah diarahkan ke IP server lewat DNS record tipe A sebelum lanjut. Tes dengan curl http://api.domainkamu.com/produk.

    Pasang HTTPS gratis dengan Certbot

    API tanpa HTTPS itu tidak layak dipakai. Token auth yang kita buat di bagian 13 akan melintas dalam bentuk teks polos. Untungnya sertifikat gratis dari Let’s Encrypt cuma butuh dua perintah.

    sudo apt install certbot python3-certbot-nginx
    sudo certbot --nginx -d api.domainkamu.com

    Certbot akan minta email, minta persetujuan, lalu menawarkan redirect otomatis dari HTTP ke HTTPS. Pilih ya. Setelah selesai, dia mengedit sendiri file Nginx tadi dan menambahkan blok listen 443 lengkap dengan path sertifikat.

    Sertifikat Let’s Encrypt berlaku 90 hari. Certbot sudah memasang timer perpanjangan otomatis saat instalasi, jadi kamu tidak perlu mengingat tanggalnya. Kalau mau memastikan mekanismenya jalan:

    sudo certbot renew --dry-run

    Update versi tanpa drama

    Aplikasi kamu akan berubah. Alurnya cuma tiga langkah dan selalu sama:

    GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o api-produk
    scp api-produk deploy@103.10.20.30:/home/deploy/
    ssh deploy@103.10.20.30 "sudo systemctl restart api-produk"

    Downtime-nya sangat pendek, biasanya di bawah satu detik. Alasannya kembali ke sifat Go tadi. Systemd cuma perlu mematikan satu proses dan menyalakan satu binary. Tidak ada kompilasi di server, tidak ada cache yang harus dihangatkan, tidak ada dependency yang diunduh ulang.

    Satu tips kecil. Kalau scp gagal karena file sedang dipakai, matikan service dulu sebelum upload, atau upload dengan nama sementara lalu ganti nama setelah service berhenti.

    Kamu mungkin pernah dengar Docker sebagai alternatif. Docker memang jalan lain yang valid, terutama kalau aplikasi kamu butuh banyak layanan pendamping atau kamu berencana pindah ke Kubernetes. Tapi untuk satu API Go di satu VPS, systemd sudah cukup dan jauh lebih sedikit bagian yang harus dipelajari. Simpan Docker untuk saat kebutuhannya benar-benar muncul.

    Checklist sebelum bilang selesai

    Lima hal ini yang sering terlewat. Cek satu per satu:

    • Environment variable terpasang. Jalankan sudo systemctl show api-produk -p Environment atau cek langsung apakah aplikasi berhasil konek ke database production, bukan ke default.
    • Log terbaca. Pastikan journalctl -u api-produk memunculkan sesuatu saat kamu mengirim request. Kalau log kosong, kamu buta saat ada masalah.
    • Service nyala saat reboot. Jangan cuma percaya. Uji dengan sudo reboot, tunggu sebentar, lalu cek statusnya lagi. Kalau lupa systemctl enable, di sinilah ketahuannya.
    • Backup database berjalan. Minimal cron harian yang menjalankan mysqldump ke folder terpisah. Backup yang belum pernah dicoba direstore itu belum bisa disebut backup.
    • Firewall rapat. Buka hanya port 22, 80, dan 443. Port 8080 dan port database tidak boleh terlihat dari internet. Cek dengan sudo ufw status.

    Sejauh mana tutorial ini berlaku

    Saya perlu jujur soal batasnya. Cara di atas cocok untuk API kecil sampai menengah yang jalan di satu VPS. Itu mencakup sebagian besar aplikasi internal, dashboard perusahaan, dan produk yang baru mulai. Banyak sistem berjalan bertahun-tahun dengan setup persis seperti ini.

    Yang tidak dibahas di sini adalah situasi skala besar. Kalau kamu butuh beberapa server di belakang load balancer, deploy tanpa downtime sama sekali, rollback otomatis saat error rate naik, atau pipeline CI yang build dan deploy sendiri setiap kali kode di-merge, itu topik lain dengan alat lain. Silakan pindah ke sana kalau memang trafiknya sudah menuntut, bukan sebelum itu.

    Sekarang API produk kamu sudah hidup di server dengan domain dan HTTPS. Di bagian 16 kita kembali ke kode: Struktur Project Go yang Rapi untuk Aplikasi Nyata. Semua file yang selama ini menumpuk di satu folder akan kita tata supaya masih enak dibaca enam bulan lagi.

    Kalau kamu butuh bantuan membangun dan mengelola API produksi untuk kebutuhan bisnis, tim Arrazy Inovasi mengerjakan pengembangan sistem dan aplikasi dari perencanaan sampai deploy di server.

  • Belajar Linux dari Nol #3: Perintah Dasar Linux Navigasi

    Belajar Linux dari Nol #3: Perintah Dasar Linux Navigasi

    Perintah dasar Linux untuk navigasi sebenarnya cuma tiga: pwd untuk melihat posisi kamu sekarang, ls untuk melihat isi folder, dan cd untuk pindah folder. Kuasai tiga perintah ini plus konsep path, dan kamu sudah bisa berpindah ke mana saja di sistem Linux tanpa tersesat. Sisanya tinggal jam terbang.

    Artikel ini bagian ketiga dari seri Belajar Linux dari Nol. Semua contoh dijalankan di Ubuntu 24.04 dengan shell bash, tapi perintahnya sama persis di hampir semua distro Linux. Kalau kamu belum nyaman membuka terminal atau masih bingung apa itu shell dan prompt, baca dulu Belajar Linux dari Nol #2: Cara Menggunakan Terminal Linux, lalu kembali ke sini.

    Di Arrazy, tiga perintah ini kami pakai setiap hari saat deploy backend Go dan Laravel ke server klien. Sebelum menyentuh file konfigurasi apa pun, langkah pertama selalu sama: cek posisi dengan pwd, lihat isi direktori dengan ls, baru pindah dengan cd. Kebiasaan kecil ini menyelamatkan kami dari banyak salah edit file di server orang.

    Konsep Working Directory: Kamu Selalu Berada di Suatu Tempat

    Setiap kali terminal terbuka, kamu selalu “berdiri” di satu folder tertentu. Folder itu disebut working directory atau direktori kerja. Semua perintah yang kamu ketik akan dieksekusi relatif terhadap posisi ini. Kalau kamu mengetik ls, yang muncul adalah isi folder tempat kamu berdiri, bukan isi folder lain.

    Saat pertama kali membuka terminal di Ubuntu 24.04, working directory kamu adalah home directory, yaitu /home/namauser. Ini folder pribadi milik akun kamu, tempat Documents, Downloads, dan file pribadi lain berada.

    Path Absolut vs Path Relatif

    Path adalah alamat sebuah file atau folder. Ada dua cara menulisnya.

    Path absolut selalu dimulai dari akar sistem, yaitu / (dibaca “root”). Path ini lengkap dan tidak ambigu, di mana pun kamu berdiri hasilnya sama. Contoh:

    /home/budi/Documents/laporan.txt
    /etc/hosts
    /var/log

    Path relatif dihitung dari posisi kamu sekarang. Kalau kamu sedang berdiri di /home/budi, maka path relatif Documents/laporan.txt menunjuk ke file yang sama dengan path absolut di atas. Tapi kalau kamu berdiri di /var, path relatif yang sama akan dicari di /var/Documents/laporan.txt dan hampir pasti gagal.

    Cara cepat membedakan: kalau diawali /, itu absolut. Kalau tidak, itu relatif.

    Arti Simbol ~ . dan ..

    Ada tiga simbol pendek yang akan sangat sering kamu pakai:

    • ~ (tilde) adalah singkatan home directory kamu. Menulis ~/Documents sama dengan /home/budi/Documents kalau username kamu budi.
    • . (satu titik) berarti direktori saat ini, tempat kamu berdiri sekarang.
    • .. (dua titik) berarti direktori satu tingkat di atasnya, alias folder induk.

    Jadi kalau kamu berdiri di /home/budi/Documents, maka .. menunjuk ke /home/budi, dan ../.. menunjuk ke /home. Simbol ini bisa digabung dengan nama folder, misalnya ../Downloads berarti “naik satu tingkat, lalu masuk ke Downloads”.

    Praktik Perintah pwd, ls, dan cd

    Buka terminal dan ikuti langkah demi langkah. Jangan cuma dibaca, otot jari perlu ikut hafal.

    pwd: Cek Posisi Kamu Sekarang

    pwd singkatan dari print working directory. Perintah ini mencetak path absolut posisi kamu saat ini:

    budi@ubuntu:~$ pwd
    /home/budi

    Perhatikan prompt budi@ubuntu:~$. Tanda ~ di situ juga menunjukkan kamu sedang di home. Setiap kali ragu sedang berada di mana, ketik pwd. Ini refleks pertama yang wajib dibentuk.

    ls: Lihat Isi Direktori

    ls (list) menampilkan isi direktori saat ini:

    budi@ubuntu:~$ ls
    Desktop  Documents  Downloads  Music  Pictures  Public  snap  Templates  Videos

    Kamu juga bisa melihat isi folder lain tanpa harus pindah ke sana, cukup beri path-nya sebagai argumen:

    budi@ubuntu:~$ ls /var/log
    apt  auth.log  dpkg.log  kern.log  syslog  ...

    Variasi yang paling sering dipakai adalah ls -la. Opsi -l menampilkan format panjang (long listing) dengan detail tiap file, dan -a menampilkan semua file termasuk yang tersembunyi:

    budi@ubuntu:~$ ls -la
    total 96
    drwxr-x--- 15 budi budi 4096 Jul 27 09:15 .
    drwxr-xr-x  3 root root 4096 Jul 20 08:00 ..
    -rw-------  1 budi budi 1240 Jul 27 09:10 .bash_history
    -rw-r--r--  1 budi budi  220 Jul 20 08:00 .bash_logout
    -rw-r--r--  1 budi budi 3771 Jul 20 08:00 .bashrc
    drwxr-xr-x  2 budi budi 4096 Jul 21 10:30 Documents
    drwxr-xr-x  2 budi budi 4096 Jul 26 14:02 Downloads

    cd: Pindah Direktori

    cd (change directory) memindahkan posisi kamu. Coba rangkaian ini:

    budi@ubuntu:~$ cd Documents
    budi@ubuntu:~/Documents$ pwd
    /home/budi/Documents
    budi@ubuntu:~/Documents$ cd ..
    budi@ubuntu:~$ pwd
    /home/budi

    Kamu juga bisa lompat langsung pakai path absolut:

    budi@ubuntu:~$ cd /var/log
    budi@ubuntu:/var/log$ pwd
    /var/log

    Dua jurus cepat yang wajib tahu:

    • cd ~ atau cukup cd tanpa argumen: langsung pulang ke home directory dari mana pun.
    • cd - (tanda minus): kembali ke direktori sebelumnya. Berguna sekali saat bolak-balik antara dua folder.
    budi@ubuntu:/var/log$ cd ~
    budi@ubuntu:~$ cd -
    /var/log
    budi@ubuntu:/var/log$ cd -
    /home/budi

    Perhatikan bahwa cd - juga mencetak path tujuan sebelum memindahkan kamu. Saat mengelola beberapa sistem aplikasi klien di satu server, kami sering bolak-balik antara folder aplikasi dan folder log hanya dengan cd -, jauh lebih cepat daripada mengetik path lengkap berulang kali.

    Satu tips lagi: tekan tombol Tab saat mengetik nama folder. Terminal akan melengkapi namanya otomatis. Ketik cd Doc lalu Tab, dan bash melengkapinya jadi cd Documents/. Selain cepat, ini juga mencegah salah ketik.

    Cara Membaca Output ls -l

    Output ls -l terlihat rumit di awal. Mari bedah satu baris:

    -rw-r--r--  1 budi budi 3771 Jul 20 08:00 .bashrc

    Dari kiri ke kanan:

    Kolom Contoh Artinya
    Tipe dan permission -rw-r--r-- Karakter pertama: - berarti file biasa, d berarti direktori. Sembilan karakter berikutnya adalah hak akses baca/tulis/eksekusi
    Jumlah link 1 Jumlah hard link ke file ini, untuk sekarang boleh diabaikan
    Pemilik budi User yang memiliki file
    Grup budi Grup yang memiliki file
    Ukuran 3771 Ukuran file dalam byte
    Waktu modifikasi Jul 20 08:00 Kapan terakhir diubah
    Nama .bashrc Nama file atau direktori

    Untuk saat ini yang penting kamu bisa membedakan mana file dan mana direktori dari karakter pertama, serta tahu siapa pemiliknya. Detail sistem permission (rwx, chmod, chown) akan dibahas tuntas di bagian #9 seri ini. Kalau ingin ukuran file lebih mudah dibaca manusia, tambahkan opsi -h: perintah ls -lh menampilkan 3.7K alih-alih 3771.

    Hidden File: Kenapa ls Biasa Tidak Menampilkan Dotfile

    Di Linux, file atau folder yang namanya diawali titik dianggap tersembunyi. File seperti .bashrc, .bash_history, atau folder .config tidak muncul saat kamu mengetik ls biasa. Ini bukan fitur keamanan, cuma konvensi supaya file konfigurasi tidak memenuhi tampilan sehari-hari. File semacam ini biasa disebut dotfile.

    Untuk melihatnya, pakai opsi -a (all):

    budi@ubuntu:~$ ls -a
    .   .bash_history  .bashrc  .config  Desktop    Downloads
    ..  .bash_logout   .cache   .local   Documents  ...

    Perhatikan dua entri pertama: . dan ... Keduanya selalu ada di setiap direktori, dan artinya persis seperti yang dibahas di atas: direktori ini sendiri dan direktori induknya. Kalau mau menampilkan file tersembunyi tanpa dua entri itu, pakai -A (huruf besar).

    Dotfile penting karena hampir semua konfigurasi personal di Linux disimpan di sana. Nanti saat kamu mengatur alias, environment variable, atau kunci SSH, semuanya tinggal di dotfile dalam home directory.

    Troubleshooting: Error yang Paling Sering Dialami Pemula

    bash: cd: dokumen: No such file or directory

    Error paling umum di topik ini. Penyebabnya hampir selalu salah satu dari dua hal. Pertama, kapitalisasi. Linux membedakan huruf besar dan kecil, jadi Documents dan documents adalah dua nama yang berbeda:

    budi@ubuntu:~$ cd documents
    bash: cd: documents: No such file or directory
    budi@ubuntu:~$ cd Documents
    budi@ubuntu:~/Documents$

    Kedua, kamu memakai path relatif dari posisi yang salah. Misalnya kamu sudah berada di dalam ~/Documents tapi mengetik cd Documents lagi, padahal di dalamnya tidak ada folder bernama itu. Solusinya: jalankan pwd untuk cek posisi, lalu ls untuk melihat nama folder yang benar-benar ada, baru cd lagi. Manfaatkan juga Tab completion, karena nama yang dilengkapi otomatis pasti benar ejaannya.

    bash: cd: too many arguments (folder dengan spasi di namanya)

    Kalau nama folder mengandung spasi, misalnya Data Kuliah, perintah cd Data Kuliah akan gagal karena bash menganggapnya dua argumen terpisah:

    budi@ubuntu:~$ cd Data Kuliah
    bash: cd: too many arguments

    Solusinya bungkus dengan tanda kutip, atau escape spasinya dengan backslash:

    budi@ubuntu:~$ cd "Data Kuliah"
    budi@ubuntu:~/Data Kuliah$ cd ~
    budi@ubuntu:~$ cd Data\ Kuliah

    Tab completion otomatis menambahkan backslash ini untuk kamu. Satu alasan lagi untuk membiasakan diri menekan Tab.

    bash: cd: catatan.txt: Not a directory

    cd hanya bisa masuk ke direktori, bukan file. Kalau kamu mencoba cd ke sebuah file, error ini muncul. Cek dengan ls -l: kalau karakter pertama barisnya - dan bukan d, itu file biasa. Untuk melihat isinya kamu butuh perintah seperti cat atau editor, yang dibahas di bagian lain seri ini.

    ls: cannot open directory: Permission denied

    Beberapa direktori sistem hanya boleh dibuka oleh user tertentu, contohnya /root:

    budi@ubuntu:~$ ls /root
    ls: cannot open directory '/root': Permission denied

    Ini normal dan bukan kerusakan. Sistem sedang melindungi area milik user lain. Untuk latihan navigasi, cukup jelajahi home directory kamu sendiri dan direktori publik seperti /var/log atau /etc. Soal hak akses dan kapan boleh memakai sudo akan dibahas di bagian #9 dan #10.

    Rangkuman dan Lanjut ke Bagian Berikutnya

    Hari ini kamu sudah memegang fondasi navigasi: pwd untuk tahu posisi, ls dan variannya untuk melihat isi direktori termasuk dotfile, serta cd dengan path absolut, path relatif, ~, .., dan cd -. Kamu juga sudah bisa membaca output ls -l secara garis besar dan tahu cara keluar dari error path yang paling sering muncul.

    Latihan singkat sebelum lanjut: dari home, masuk ke /var/log pakai path absolut, kembali ke home dengan cd -, lalu masuk ke Documents pakai path relatif, dan naik lagi dengan cd ... Ulangi sampai jarimu bergerak tanpa mikir.

    Di bagian berikutnya, Belajar Linux dari Nol #4: Struktur Direktori Linux Lengkap, kita bahas peta besarnya: apa isi /etc, /var, /usr, /bin, dan kenapa Linux menyusun foldernya seperti itu. Artikelnya terbit menyusul, pantau daftar lengkapnya di halaman Belajar Linux dari Nol.

    Referensi

  • Contoh Invoice Usaha Jasa: Poin Wajib Biar Cepat Dibayar

    Contoh Invoice Usaha Jasa: Poin Wajib Biar Cepat Dibayar

    Banyak invoice telat dibayar bukan karena klien pelit, tapi karena invoicenya sendiri yang bermasalah. Tiga penyebab paling sering: tanggal jatuh tempo tidak ditulis eksplisit, rincian jasa terlalu umum sampai klien harus bertanya dulu sebelum menyetujui, dan invoice dikirim ke orang yang tidak punya wewenang pembayaran. Ketiganya bisa dibereskan hari ini juga, tanpa aplikasi apa pun.

    Artikel ini membahas invoice dari sudut yang jarang disentuh. Bukan sekadar daftar isian, tapi alat untuk mempercepat pembayaran. Setiap elemen kita bedah dengan satu pertanyaan yang sama: elemen ini mempercepat kamu dibayar, atau justru memperlambat.

    Kenapa invoice sering telat dibayar

    Coba lihat dari sisi klien. Di perusahaan yang agak rapi, invoice yang masuk harus melewati beberapa tahap: diterima, dicek kesesuaiannya dengan pekerjaan, disetujui oleh orang yang memesan jasa, lalu dijadwalkan transfernya oleh bagian keuangan. Setiap ketidakjelasan di invoice menambah satu babak tanya jawab di rantai itu.

    Contoh nyata. Kamu menulis “jasa desain” tanpa rincian. Bagian keuangan tidak tahu desain apa, jadi mereka tanya ke orang yang memesan. Orang itu sedang sibuk, balasnya tiga hari kemudian. Invoice kamu baru bergerak lagi minggu depan. Padahal masalahnya cuma satu baris rincian yang kurang lengkap.

    Hal yang sama berlaku untuk jatuh tempo. Invoice tanpa tanggal jatuh tempo akan masuk kategori “dibayar kapan sempat”. Nomor rekening yang ditulis kecil di pojok bawah tanpa nama pemilik bikin bagian keuangan ragu dan menunda transfer. Dan kalau invoice dikirim ke orang yang salah, misalnya ke staf marketing yang jadi kontak harian kamu, invoice itu bisa mengendap di inbox tanpa pernah sampai ke bagian keuangan.

    Anatomi invoice yang benar, poin per poin

    Berikut elemen yang wajib ada, plus alasannya dari sisi kecepatan bayar.

    Nomor invoice

    Pakai sistem penomoran yang konsisten, misalnya INV/2026/07/012. Artinya invoice ke-12 di bulan Juli 2026. Kenapa perlu sistem, bukan asal angka? Dua alasan. Pertama, bagian keuangan klien mengarsip invoice berdasarkan nomor. Invoice bernomor rapi lebih cepat diproses dan lebih mudah dilacak saat kamu menagih. Kedua, saat follow up kamu cukup sebut “invoice INV/2026/07/012” dan semua orang tahu dokumen mana yang dimaksud, tanpa perlu kirim ulang lampiran.

    Identitas pengirim dan penerima

    Tulis nama usaha kamu, alamat, nomor kontak, dan email. Lalu identitas klien: nama perusahaan dan alamatnya. Yang sering dilupakan: cantumkan juga nama PIC yang memesan dan menyetujui pekerjaan, misalnya “Up. Ibu Rina, Marketing Manager”. Ini penting karena bagian keuangan hampir selalu butuh konfirmasi internal sebelum membayar. Kalau nama PIC sudah tertulis, mereka langsung tahu harus konfirmasi ke siapa. Kalau tidak, invoice kamu menunggu sampai ada yang iseng mencari tahu.

    Rincian jasa yang spesifik

    Ini pembeda terbesar antara invoice yang langsung disetujui dan invoice yang bolak-balik dipertanyakan. Bandingkan dua baris ini:

    • “Jasa desain” senilai Rp3.000.000
    • “Jasa desain feed Instagram 12 post, termasuk revisi 2x, periode Juli 2026” senilai Rp3.000.000

    Baris pertama memancing pertanyaan. Baris kedua bisa dicocokkan langsung dengan kesepakatan awal, jadi bisa disetujui tanpa diskusi tambahan. Kalau ada beberapa item pekerjaan, pecah per baris dengan harga masing-masing. Jangan digabung jadi satu angka besar.

    Nilai dan termin pembayaran

    Tulis nilai total, lalu perjelas termin kalau ada. Misalnya DP 50 persen sudah dibayar di awal, maka invoice pelunasan harus menampilkan: nilai total proyek, DP yang sudah diterima beserta tanggalnya, dan sisa tagihan yang harus dibayar sekarang. Klien jadi tidak perlu membongkar riwayat transfer untuk memastikan angkanya benar. Semakin sedikit yang perlu mereka cek, semakin cepat kamu dibayar.

    Tanggal terbit dan jatuh tempo yang eksplisit

    Ini poin yang paling sering menentukan cepat lambatnya pembayaran. Jangan cuma menulis “pembayaran 14 hari setelah invoice diterima”. Kalimat itu memaksa klien menghitung sendiri, dan hasil hitungannya bisa beda dengan hitungan kamu. Tulis tanggal pasti: “Jatuh tempo: 14 Agustus 2026”. Tanggal eksplisit masuk ke jadwal pembayaran klien sebagai deadline, bukan sebagai wacana. Tanpa jatuh tempo, tidak ada yang bisa disebut telat, dan kamu kehilangan dasar untuk menagih.

    Metode bayar dan nomor rekening yang jelas

    Tulis nama bank, nomor rekening, dan atas nama siapa, dalam ukuran yang mudah dibaca. Kalau nama pemilik rekening berbeda dengan nama usaha di kop invoice, beri keterangan singkat kenapa. Bagian keuangan dilatih curiga pada ketidakcocokan seperti ini, dan kecurigaan artinya penundaan. Kalau kamu menerima beberapa metode, misalnya transfer bank dan QRIS, tampilkan semuanya supaya klien tinggal pilih yang paling cepat buat mereka.

    Denda keterlambatan atau diskon bayar cepat

    Ini opsional, dan ada aturan mainnya. Keduanya hanya pantas dipakai kalau sudah disepakati sejak awal, idealnya tertulis di penawaran atau kontrak. Denda yang tiba-tiba muncul di invoice justru merusak hubungan. Untuk klien korporat dengan siklus pembayaran panjang, klausul denda 1 sampai 2 persen per bulan keterlambatan cukup umum. Diskon bayar cepat, misalnya potongan 2 persen kalau dibayar dalam 7 hari, cocok untuk usaha yang butuh arus kas cepat dan margin masih memungkinkan. Kalau ragu, lebih baik fokus ke jatuh tempo yang tegas dulu.

    Contoh invoice jasa fotografi produk

    Berikut contoh utuh yang bisa kamu tiru strukturnya. Angka dan nama fiktif, tapi formatnya wajar dipakai di lapangan.

    • INVOICE No: INV/2026/07/018
    • Tanggal terbit: 24 Juli 2026
    • Jatuh tempo: 7 Agustus 2026
    • Dari: Studio Foto Cahaya, Jl. Melati No. 8, Purwokerto. WA 0812-xxxx-xxxx, email halo@studiocahaya.id
    • Kepada: PT Rasa Nusantara (produsen sambal kemasan), Jl. Industri No. 21, Semarang. Up. Bapak Dimas, Brand Manager
    • Rincian:
      • Foto produk 20 SKU sambal kemasan, latar putih, 2 angle per SKU: Rp4.000.000
      • Foto lifestyle 10 frame untuk konten Instagram, termasuk properti dan styling: Rp2.500.000
      • Edit dan retouch seluruh file, dikirim dalam format JPG resolusi penuh: Rp1.000.000
    • Total: Rp7.500.000
    • DP diterima 10 Juli 2026: Rp3.750.000
    • Sisa tagihan: Rp3.750.000
    • Pembayaran: Transfer ke BCA 1234567890 a.n. Cahaya Kreatif Studio
    • Catatan: Mohon konfirmasi setelah transfer via WA. Kuitansi pelunasan dikirim setelah dana diterima.

    Perhatikan polanya. Setiap pertanyaan yang mungkin muncul di kepala bagian keuangan sudah terjawab di dalam dokumen: tagihan ini untuk apa, siapa yang memesan, berapa yang sudah dibayar, sisa berapa, transfer ke mana, dan batas waktunya kapan.

    Etika kirim invoice dan jadwal reminder

    Invoice yang bagus tetap bisa telat dibayar kalau salah alur kirim. Tanyakan sejak awal proyek: invoice dikirim ke siapa, dan perlu cc ke siapa. Umumnya, kirim ke PIC yang memesan pekerjaan dengan cc ke bagian keuangan, atau sebaliknya sesuai instruksi klien. Kirim sebagai PDF, bukan file yang bisa diedit.

    Sertakan kalimat pengantar yang singkat dan sopan. Contoh: “Selamat pagi Pak Dimas, terlampir invoice INV/2026/07/018 untuk pelunasan sesi foto produk bulan Juli, sisa tagihan Rp3.750.000 dengan jatuh tempo 7 Agustus 2026. Mohon dibantu prosesnya, terima kasih.”

    Untuk reminder, pakai ritme yang wajar supaya tegas tanpa terkesan mengejar:

    • H-3 sebelum jatuh tempo: “Pak Dimas, izin mengingatkan invoice INV/2026/07/018 jatuh tempo tanggal 7 Agustus. Kalau sudah dijadwalkan, abaikan pesan ini ya. Terima kasih.”
    • Hari H: “Selamat pagi Pak, hari ini jatuh tempo invoice INV/2026/07/018. Mohon info kalau sudah diproses, supaya kami bisa siapkan kuitansinya.”
    • H+3: “Pak Dimas, invoice INV/2026/07/018 sudah melewati jatuh tempo 7 Agustus. Apakah ada kendala di prosesnya? Kalau perlu dokumen tambahan, kabari saja.”

    Tiga pesan itu pendek, menyebut nomor invoice, dan selalu memberi klien jalan keluar yang enak. Satu kebiasaan lagi yang sering dilewatkan: catat setiap invoice yang belum dibayar sebagai piutang di pembukuan, supaya kamu tahu persis siapa yang menunggak dan berapa lama. Cara mencatatnya tanpa ribet sudah kami bahas di panduan laporan keuangan sederhana untuk usaha kecil.

    Soal materai dan kuitansi

    Dua hal ini sering ditanyakan, jadi kita bahas singkat. Invoice sendiri tidak wajib bermaterai. Materai Rp10.000 umumnya dibutuhkan pada kuitansi atau dokumen penerimaan uang dengan nilai di atas Rp5 juta, terutama kalau klien korporat memintanya sebagai syarat pencairan. Kuitansi diterbitkan setelah dana benar-benar diterima, sebagai bukti pembayaran. Praktisnya: siapkan materai untuk proyek bernilai besar dengan klien perusahaan, dan tanyakan di awal apakah mereka butuh kuitansi bermaterai untuk pelunasan. Menyiapkannya di awal lebih cepat daripada bolak-balik di akhir.

    Kalau invoice per bulan sudah puluhan

    Semua tips di atas mudah dijalankan saat invoice kamu masih lima sampai sepuluh per bulan. Begitu naik ke puluhan, masalahnya berubah. Nomor invoice mulai dobel, status mana yang sudah dibayar dan mana yang belum tercecer di ingatan, dan reminder H-3 sering kelewat karena tidak ada yang mengingatkan kamu sendiri.

    Di titik itu, yang kamu butuhkan bukan template yang lebih rapi, tapi sistem yang menerbitkan invoice otomatis dengan penomoran konsisten, mencatat status bayar tiap tagihan, dan menandai mana yang lewat jatuh tempo. Kalau usaha jasa kamu sudah sampai di tahap ini, kami di Arrazy biasa membangun sistem aplikasi yang disesuaikan dengan alur kerja bisnis, termasuk pengelolaan invoice dan piutang. Ceritakan saja alur penagihan kamu sekarang, nanti kita lihat bagian mana yang paling layak diotomatiskan.

  • Cara Membalas Review Negatif di Google Maps Tanpa Drama

    Cara Membalas Review Negatif di Google Maps Tanpa Drama

    Dapat bintang 1 di Google Maps itu rasanya personal. Anda bangun usaha ini bertahun-tahun, lalu ada orang menghakiminya lewat dua kalimat pedas. Wajar kalau panas. Tapi sebelum jempol Anda mengetik pembelaan, ingat satu prinsip ini: balasan Anda bukan untuk si pemberi review. Balasan Anda dibaca oleh ratusan calon pelanggan yang sedang menimbang mau datang atau tidak. Mereka menilai cara Anda merespons, bukan sekadar isi komplainnya.

    Formula singkatnya begini. Tunggu minimal 24 jam supaya kepala dingin. Ucapkan terima kasih atas masukannya. Akui bagian yang memang benar. Jelaskan singkat tanpa nada membela diri. Ajak lanjut ke jalur pribadi seperti WhatsApp atau telepon. Tutup dengan profesional. Selesai. Sisanya di artikel ini adalah cara menerapkan formula itu ke berbagai jenis review, lengkap dengan contoh yang bisa langsung Anda pakai.

    Aturan 24 jam: jangan balas saat masih panas

    Review negatif yang baru masuk itu seperti kompor menyala. Kalau Anda pegang langsung, pasti terbakar. Balasan yang ditulis dalam kondisi emosi hampir selalu terbaca defensif, dan itu yang paling merusak di mata calon pelanggan.

    Jadi tahan dulu. Beri jeda minimal sehari. Pakai waktu itu untuk cek fakta: benarkah kejadiannya seperti yang ditulis, siapa yang melayani hari itu, ada bukti transaksinya atau tidak. Dengan data di tangan, balasan Anda jadi berbobot, bukan sekadar reaksi.

    Tapi jangan kebablasan juga. Review yang didiamkan lebih dari beberapa hari mengirim pesan lain: pemilik usaha ini tidak peduli. Calon pelanggan yang scroll review dan melihat komplain tanpa jawaban akan mengisi kesimpulannya sendiri, dan biasanya bukan kesimpulan yang menguntungkan Anda. Target yang sehat: balas dalam rentang 1 sampai 3 hari.

    Formula balasan yang bekerja

    Lima langkah ini berlaku untuk hampir semua review negatif. Urutannya penting.

    • Terima kasih dulu. Bukan basa-basi. Orang yang komplain di review setidaknya masih mau memberi tahu Anda ada masalah. Yang lebih berbahaya justru pelanggan kecewa yang diam lalu tidak pernah kembali.
    • Akui yang valid, tanpa drama. Kalau pelayanan hari itu memang lambat, katakan memang lambat. Pengakuan jujur satu kalimat lebih meyakinkan daripada tiga paragraf pembelaan.
    • Jelaskan singkat, bukan membela diri. Konteks boleh, alasan panjang jangan. “Hari itu dua staf kami sakit” cukup. Tidak perlu ditambah “tapi seharusnya Anda maklum”.
    • Ajak ke jalur pribadi. Detail penyelesaian sebaiknya lewat WhatsApp atau telepon, bukan di kolom balasan publik. Cantumkan kontaknya.
    • Tutup profesional. Satu kalimat harapan untuk bisa melayani lebih baik. Tanpa sindiran, tanpa kalimat pasif-agresif.

    Satu balasan dengan formula ini panjangnya cukup 3 sampai 5 kalimat. Lebih dari itu biasanya mulai terbaca seperti pembelaan.

    5 contoh balasan siap pakai

    Sesuaikan nama, detail, dan nomor kontak dengan usaha Anda. Yang penting nadanya: tenang, dewasa, dan jelas.

    1. Komplain valid: pelayanan memang lambat hari itu

    Terima kasih sudah meluangkan waktu memberi masukan, Pak Andri. Benar, Sabtu kemarin pelayanan kami lebih lambat dari biasanya karena dua staf berhalangan hadir dan antrean sedang penuh. Itu bukan standar yang ingin kami berikan. Kami sudah menambah jadwal staf untuk akhir pekan. Kalau berkenan, silakan hubungi kami di 0812-xxxx-xxxx supaya kami bisa menebus pengalaman kemarin. Semoga ada kesempatan melayani Bapak lebih baik.

    Perhatikan: tidak ada bantahan, tidak ada alasan bertele-tele, dan ada tindakan konkret yang sudah diambil.

    2. Komplain setengah benar

    Misalnya pelanggan bilang makanan datang 45 menit, padahal catatan Anda menunjukkan 25 menit tapi memang lebih lama dari normal. Akui bagian yang benar, luruskan sisanya dengan halus.

    Terima kasih atas masukannya, Kak. Benar bahwa pesanan Kakak hari itu keluar lebih lama dari standar kami, dan kami mohon maaf untuk itu. Dari catatan kami waktunya sekitar 25 menit, memang belum secepat yang seharusnya. Kami sedang membenahi alur dapur di jam sibuk. Boleh hubungi kami di 0812-xxxx-xxxx ya, kami ingin dengar detailnya langsung. Terima kasih sudah mampir.

    Meluruskan angka itu sah, asal disampaikan sebagai informasi, bukan sebagai tuduhan bahwa dia berbohong.

    3. Review bintang 1 tanpa penjelasan

    Kadang cuma bintang 1, nol kata. Tetap balas, karena calon pelanggan lain melihatnya.

    Halo Kak, terima kasih sudah memberi penilaian. Kami ingin tahu apa yang membuat pengalaman Kakak kurang baik supaya bisa kami perbaiki. Silakan hubungi kami di 0812-xxxx-xxxx atau balas review ini dengan ceritanya. Masukan sekecil apa pun kami hargai.

    Balasan seperti ini menunjukkan Anda terbuka pada kritik, bahkan yang tidak jelas sekalipun. Itu poin plus di mata pembaca.

    4. Review yang menyebut nama karyawan

    Lindungi tim Anda di ruang publik, tapi jangan menyangkal mentah-mentah. Proses internalnya urusan di dalam.

    Terima kasih atas laporannya, Bu Rina. Kami mohon maaf atas pengalaman yang kurang nyaman. Masukan ini sudah kami tindak lanjuti secara internal bersama tim yang bertugas hari itu. Untuk detailnya, kami akan senang kalau Ibu berkenan menghubungi kami langsung di 0812-xxxx-xxxx. Terima kasih sudah membantu kami menjaga kualitas pelayanan.

    Jangan pernah menghakimi karyawan di kolom balasan publik. Itu merusak dua hal sekaligus: moral tim dan citra Anda sebagai pemilik usaha.

    5. Review yang diduga palsu atau dari kompetitor

    Kadang muncul review dari akun yang tidak pernah jadi pelanggan. Langkah pertama: laporkan ke Google. Buka profil bisnis Anda, cari review tersebut, pilih opsi laporkan atau tandai sebagai tidak pantas, lalu pilih alasan seperti konflik kepentingan atau spam. Proses peninjauan Google bisa memakan waktu berhari-hari sampai berminggu-minggu, dan tidak semua laporan dikabulkan.

    Selama review itu masih tayang, tetap balas dengan sopan. Kenapa? Karena calon pelanggan tidak tahu review itu palsu. Yang mereka lihat hanyalah komplain dan cara Anda meresponsnya.

    Terima kasih atas ulasannya. Kami sudah memeriksa catatan pelanggan dan belum menemukan transaksi yang sesuai dengan keluhan ini. Kalau memang pernah berkunjung, kami mohon bantuannya menghubungi 0812-xxxx-xxxx dengan detail waktu kedatangan supaya bisa kami telusuri. Kami serius menangani setiap keluhan pelanggan yang benar-benar terjadi.

    Balasan seperti ini halus tapi tegas. Pembaca yang jeli akan menangkap sinyalnya tanpa Anda perlu menuduh siapa pun secara terbuka.

    Yang jangan dilakukan, sekeras apa pun godaannya

    Perang di kolom balasan. Debat panjang dengan reviewer adalah tontonan gratis untuk calon pelanggan, dan Anda hampir pasti terlihat sebagai pihak yang kalah. Satu balasan rapi, lalu pindahkan ke jalur pribadi. Titik.

    Membocorkan data pelanggan saat membela diri. Menyebut detail transaksi, riwayat pesanan, apalagi masalah pribadi pelanggan di ruang publik itu pelanggaran kepercayaan. Meski Anda benar secara fakta, di mata pembaca Anda jadi usaha yang tidak bisa dipercaya menjaga data.

    Membalas pakai akun pribadi. Balasan dari akun Google pribadi pemilik, apalagi ikut berdebat, terlihat tidak profesional. Semua respons harus lewat akun resmi profil bisnis. Kalau profil Google Business Anda belum tertata atau aksesnya masih berantakan, bereskan dulu lewat panduan mengelola profil Google Bisnis sebelum mulai membalas review satu per satu.

    Membeli review palsu untuk menimbun yang negatif. Ini jalan pintas yang mahal harganya. Google punya sistem deteksi review tidak wajar, dan sanksinya mulai dari penghapusan massal review sampai penangguhan profil bisnis. Kalau ketahuan pelanggan, kerusakannya lebih parah lagi: semua review positif Anda ikut diragukan.

    Mengubah pelanggan kecewa jadi review yang diedit

    Ini bagian yang jarang dimanfaatkan pemilik usaha. Review di Google Maps bisa diedit oleh penulisnya. Artinya bintang 1 hari ini tidak harus jadi bintang 1 selamanya.

    Urutannya tidak boleh dibalik. Pertama, selesaikan masalahnya beneran. Hubungi pelanggan lewat jalur pribadi, dengarkan sampai selesai, lalu beri solusi yang pantas. Bisa penggantian, pengulangan layanan, atau sekadar permintaan maaf yang tulus dengan penjelasan perbaikan yang sudah dilakukan.

    Kedua, setelah masalah benar-benar beres dan pelanggan terlihat puas, baru minta dengan sopan. Kalimatnya sederhana: “Kalau Bapak merasa masalahnya sudah tertangani, kami akan sangat terbantu kalau Bapak berkenan memperbarui ulasan di Google. Tapi tidak ada kewajiban sama sekali.” Berhenti di situ.

    Dua hal yang haram: memaksa dan memberi iming-iming. Menawarkan diskon atau hadiah agar review diubah melanggar kebijakan Google dan bisa berbalik jadi review baru yang menceritakan sogokan Anda. Biarkan perubahan review jadi keputusan mereka sendiri. Faktanya, pelanggan yang masalahnya ditangani dengan baik sering kali mengubah reviewnya tanpa diminta, dan review hasil editan seperti itu justru jadi bukti paling kuat bahwa usaha Anda bisa dipercaya.

    Review yang dibalas rapi itu aset, bukan beban

    Coba lihat dari kacamata calon pelanggan. Profil bisnis dengan rating 5.0 mulus tanpa satu pun kritik justru sering terasa mencurigakan. Yang meyakinkan adalah profil dengan beberapa review negatif yang semuanya dibalas dengan tenang, jujur, dan solutif. Itu bukti ada manusia dewasa di balik usaha tersebut.

    Jadi berhenti melihat review negatif sebagai serangan. Ia adalah panggung kecil tempat Anda menunjukkan karakter usaha di depan orang-orang yang belum pernah jadi pelanggan. Setiap balasan yang rapi menabung kepercayaan, dan tabungan itu terasa hasilnya saat orang membandingkan Anda dengan kompetitor yang membiarkan komplain menggantung tanpa jawaban.

    Kalau review negatif saja bisa diolah jadi nilai jual, apalagi review positif dan cerita pelanggan yang puas. Cara mengelolanya kami bahas tuntas di artikel cara mengumpulkan testimoni pelanggan jadi aset jualan. Dua-duanya bermuara ke hal yang sama: kepercayaan calon pelanggan dibangun dari cara Anda memperlakukan pelanggan yang sudah ada.

  • Belajar Golang dari Nol #14: Testing, Menguji Function dan Handler

    Belajar Golang dari Nol #14: Testing, Menguji Function dan Handler

    Di Belajar Golang dari Nol #13, API produk kita sudah punya middleware logging dan proteksi API key. Endpoint aman, setiap request tercatat. Tapi ada satu kebiasaan yang masih kita lakukan sejak bagian 11. Setiap selesai mengubah kode, kita jalankan server, buka Postman, lalu klik kirim request satu per satu untuk memastikan semuanya masih jalan.

    Cara itu melelahkan. Dan yang lebih bahaya, gampang kelewat. Kamu perbaiki handler POST, lupa cek handler GET, ternyata yang GET ikut rusak. Di bagian ini kita belajar cara yang lebih waras, yaitu menyuruh Go sendiri yang mengetes kode kita. Namanya automated testing, dan Go menyediakannya bawaan tanpa install apa pun.

    Kenapa Testing Bukan Kemewahan

    Banyak pemula menganggap testing itu urusan nanti, sesuatu yang dikerjakan kalau sempat. Padahal ada tiga alasan kenapa testing justru menghemat waktu.

    • Refactor tanpa takut. Mau ganti struktur kode, rapikan function, atau pindah logika ke file lain. Selama test lulus, kamu tahu perilaku kode tidak berubah. Tanpa test, setiap refactor terasa seperti jalan di atas es tipis.
    • Bukti kode jalan. Satu perintah go test menggantikan puluhan klik manual di Postman. Sekali tulis, tes bisa diulang ribuan kali secara gratis.
    • Syarat kerja tim. Di hampir semua tim backend profesional, kode tanpa test tidak akan lolos code review. Kalau kamu berencana kerja sebagai backend developer Go, kebiasaan menulis test bukan nilai plus lagi. Itu standar minimum.

    Kabar baiknya, Go serius soal ini. Tool testing sudah ada di dalam bahasa lewat package testing dan perintah go test. Tidak perlu install framework pihak ketiga seperti di bahasa lain.

    Aturan Dasar Testing di Go

    Go punya konvensi sederhana. Ikuti aturannya, dan semuanya jalan otomatis.

    • File test diberi akhiran _test.go. Contoh: kode ada di produk.go, testnya di produk_test.go. File ini tidak ikut ter-compile saat build biasa.
    • Function test harus diawali Test dengan huruf besar setelahnya, dan menerima satu parameter t *testing.T. Contoh: func TestHitungDiskon(t *testing.T).
    • Jalankan dengan go test di folder project. Tambahkan -v kalau mau lihat detail tiap test.
    go test        # jalankan semua test, hanya tampilkan ringkasan
    go test -v     # verbose, tampilkan tiap test dan hasilnya

    Tidak ada function main yang perlu dipanggil. Tidak ada registrasi test di mana pun. Go mencari sendiri semua file _test.go dan menjalankan semua function TestXxx di dalamnya.

    Test Pertama: Menguji Function Murni

    Target paling enak untuk test pertama adalah function murni, yaitu function yang hasilnya hanya bergantung pada input. Tidak menyentuh database, tidak menyentuh network. Dari seri sebelumnya kita sudah punya dua kandidat, yaitu validasi produk dan hitung diskon. Ini versi ringkasnya di produk.go.

    func ValidasiProduk(p Produk) error {
    	if p.Nama == "" {
    		return errors.New("nama produk tidak boleh kosong")
    	}
    	if p.Harga <= 0 {
    		return errors.New("harga harus lebih dari nol")
    	}
    	if p.Stok < 0 {
    		return errors.New("stok tidak boleh minus")
    	}
    	return nil
    }
    
    func HitungDiskon(harga, persen int) (int, error) {
    	if persen < 0 || persen > 100 {
    		return 0, errors.New("persen diskon harus 0 sampai 100")
    	}
    	return harga - (harga * persen / 100), nil
    }

    Sekarang buat file produk_test.go di folder yang sama, lalu tulis test pertama.

    package main
    
    import "testing"
    
    func TestHitungDiskon(t *testing.T) {
    	got, err := HitungDiskon(100000, 10)
    	if err != nil {
    		t.Fatalf("tidak mengharapkan error, dapat: %v", err)
    	}
    	want := 90000
    	if got != want {
    		t.Errorf("HitungDiskon(100000, 10) = %d, ingin %d", got, want)
    	}
    }

    Polanya selalu sama. Panggil function dengan input tertentu, simpan hasilnya di got, bandingkan dengan nilai harapan di want. Kalau beda, laporkan lewat t.Errorf. Perhatikan pesan errornya. Pesan yang baik menyebut input, hasil yang didapat, dan hasil yang diinginkan. Saat test gagal enam bulan lagi, pesan itu yang menyelamatkanmu dari menebak-nebak.

    Ada dua cara melaporkan kegagalan. t.Errorf menandai test gagal tapi lanjut ke baris berikutnya. t.Fatalf menandai gagal dan langsung berhenti. Pakai Fatalf kalau pengecekan berikutnya tidak ada artinya lagi, misalnya errornya saja sudah muncul.

    Table Driven Test, Idiom Paling Penting

    Satu function biasanya perlu diuji dengan banyak kombinasi input. Menyalin test di atas lima kali dengan angka berbeda jelas bukan solusi. Idiom Go untuk masalah ini bernama table driven test. Idenya, kumpulkan semua kasus dalam satu slice of struct, lalu loop.

    func TestHitungDiskonTable(t *testing.T) {
    	kasus := []struct {
    		nama     string
    		harga    int
    		persen   int
    		want     int
    		inginErr bool
    	}{
    		{"diskon normal 10 persen", 100000, 10, 90000, false},
    		{"tanpa diskon", 50000, 0, 50000, false},
    		{"diskon penuh", 80000, 100, 0, false},
    		{"persen minus harus gagal", 100000, -5, 0, true},
    		{"persen di atas 100 harus gagal", 100000, 150, 0, true},
    	}
    
    	for _, k := range kasus {
    		t.Run(k.nama, func(t *testing.T) {
    			got, err := HitungDiskon(k.harga, k.persen)
    			if k.inginErr {
    				if err == nil {
    					t.Errorf("mengharapkan error, tapi dapat nil")
    				}
    				return
    			}
    			if err != nil {
    				t.Fatalf("tidak mengharapkan error, dapat: %v", err)
    			}
    			if got != k.want {
    				t.Errorf("dapat %d, ingin %d", got, k.want)
    			}
    		})
    	}
    }

    Setiap kasus punya nama, dan t.Run menjalankannya sebagai subtest terpisah. Saat go test -v jalan, kamu bisa lihat kasus mana yang lulus dan mana yang gagal, lengkap dengan namanya.

    Kenapa pola ini enak dirawat. Menambah kasus baru cukup satu baris di tabel, bukan satu function baru. Logika pengujian ditulis sekali, tidak ada copy paste yang bisa melenceng. Dan tabelnya sendiri menjadi dokumentasi perilaku function.

    Menguji Kasus yang Harus Gagal

    Pemula sering hanya menguji jalur bahagia, yaitu input benar menghasilkan output benar. Padahal bug paling sering muncul di jalur sebaliknya. Input jelek harus ditolak, dan test harus memastikan penolakan itu benar-benar terjadi.

    func TestValidasiProdukStokMinus(t *testing.T) {
    	p := Produk{Nama: "Kopi Gayo", Harga: 45000, Stok: -3}
    	err := ValidasiProduk(p)
    	if err == nil {
    		t.Errorf("stok minus seharusnya gagal validasi, tapi error nil")
    	}
    }

    Perhatikan logikanya terbalik dari test biasa. Di sini kondisi gagal justru err == nil. Kalau suatu hari ada yang tidak sengaja menghapus pengecekan stok di ValidasiProduk, test ini langsung merah. Tanpa test ini, produk berstok minus bisa masuk database dan baru ketahuan saat pelanggan komplain.

    Menguji Handler HTTP dengan httptest

    Function murni sudah aman. Sekarang bagian yang lebih menarik, yaitu menguji handler API tanpa menjalankan server sama sekali. Go menyediakan package net/http/httptest untuk ini. Dua pemain utamanya adalah httptest.NewRequest untuk membuat request palsu dan httptest.NewRecorder untuk menangkap response.

    Supaya fokus ke teknik testingnya, kita pakai handler versi in-memory yang sederhana. Di project kamu, handler dari bagian 11 dan 12 bisa diuji dengan pola yang sama persis.

    var daftarProduk = []Produk{
    	{ID: 1, Nama: "Kopi Gayo", Harga: 45000, Stok: 10},
    	{ID: 2, Nama: "Teh Melati", Harga: 20000, Stok: 25},
    }
    
    func handlerListProduk(w http.ResponseWriter, r *http.Request) {
    	w.Header().Set("Content-Type", "application/json")
    	json.NewEncoder(w).Encode(daftarProduk)
    }
    
    func handlerTambahProduk(w http.ResponseWriter, r *http.Request) {
    	var p Produk
    	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
    		http.Error(w, "body JSON tidak valid", http.StatusBadRequest)
    		return
    	}
    	if err := ValidasiProduk(p); err != nil {
    		http.Error(w, err.Error(), http.StatusBadRequest)
    		return
    	}
    	p.ID = len(daftarProduk) + 1
    	daftarProduk = append(daftarProduk, p)
    	w.Header().Set("Content-Type", "application/json")
    	w.WriteHeader(http.StatusCreated)
    	json.NewEncoder(w).Encode(p)
    }

    Menguji handler GET produk

    func TestHandlerListProduk(t *testing.T) {
    	req := httptest.NewRequest(http.MethodGet, "/produk", nil)
    	rec := httptest.NewRecorder()
    
    	handlerListProduk(rec, req)
    
    	if rec.Code != http.StatusOK {
    		t.Fatalf("status = %d, ingin %d", rec.Code, http.StatusOK)
    	}
    
    	var hasil []Produk
    	if err := json.Unmarshal(rec.Body.Bytes(), &hasil); err != nil {
    		t.Fatalf("body bukan JSON valid: %v", err)
    	}
    	if len(hasil) == 0 {
    		t.Errorf("daftar produk kosong, ingin minimal 1 produk")
    	}
    }

    Alurnya tiga langkah. Buat request palsu, siapkan recorder, panggil handler seperti function biasa. Recorder menyimpan semua yang ditulis handler, yaitu status code di rec.Code dan body di rec.Body. Kita cek status 200, lalu pastikan bodynya JSON valid. Semua terjadi di memori, tanpa port terbuka, tanpa server jalan, selesai dalam hitungan milidetik.

    Menguji POST dengan body invalid

    Sekarang jalur gagalnya. Kirim produk dengan nama kosong, dan handler harus menjawab 400.

    func TestHandlerTambahProdukBodyInvalid(t *testing.T) {
    	body := strings.NewReader(`{"nama": ""}`)
    	req := httptest.NewRequest(http.MethodPost, "/produk", body)
    	rec := httptest.NewRecorder()
    
    	handlerTambahProduk(rec, req)
    
    	if rec.Code != http.StatusBadRequest {
    		t.Errorf("status = %d, ingin %d", rec.Code, http.StatusBadRequest)
    	}
    }

    Body request dibuat dari string biasa lewat strings.NewReader. Test ini menjaga kontrak API kita. Selama test ini lulus, klien yang mengirim data kacau dijamin dapat 400, bukan 500 atau lebih parah lagi, data kacau yang tersimpan diam-diam.

    Menguji Endpoint yang Dilindungi API Key

    Di bagian 13 kita membungkus handler dengan middleware cekAPIKey yang memeriksa header X-API-Key. Middleware juga bisa diuji dengan httptest. Kuncinya, bungkus handler dengan middleware dulu, lalu panggil lewat ServeHTTP.

    func TestMiddlewareAPIKey(t *testing.T) {
    	handler := cekAPIKey(http.HandlerFunc(handlerListProduk))
    
    	t.Run("tanpa API key ditolak", func(t *testing.T) {
    		req := httptest.NewRequest(http.MethodGet, "/produk", nil)
    		rec := httptest.NewRecorder()
    
    		handler.ServeHTTP(rec, req)
    
    		if rec.Code != http.StatusUnauthorized {
    			t.Errorf("status = %d, ingin %d", rec.Code, http.StatusUnauthorized)
    		}
    	})
    
    	t.Run("dengan API key benar lolos", func(t *testing.T) {
    		req := httptest.NewRequest(http.MethodGet, "/produk", nil)
    		req.Header.Set("X-API-Key", "rahasia123")
    		rec := httptest.NewRecorder()
    
    		handler.ServeHTTP(rec, req)
    
    		if rec.Code != http.StatusOK {
    			t.Errorf("status = %d, ingin %d", rec.Code, http.StatusOK)
    		}
    	})
    }

    Dua skenario dalam satu test. Request tanpa header harus mentok di 401, request dengan key yang benar harus tembus dan dapat 200. Test seperti ini penting karena bug di lapisan auth adalah bug keamanan. Kalau ada perubahan yang tidak sengaja mematikan pengecekan API key, test ini langsung gagal sebelum kode naik ke server.

    Coverage: Melihat Bagian yang Belum Teruji

    Go bisa menghitung berapa persen baris kode yang tersentuh test.

    $ go test -cover
    PASS
    coverage: 75.9% of statements
    ok      toko-api        0.018s

    Satu hal yang perlu diluruskan sejak awal. Angka coverage bukan tujuan, melainkan petunjuk. Coverage 75.9% artinya masih ada sekitar seperempat kode yang belum pernah dijalankan oleh test mana pun, dan di situlah bug bisa bersembunyi. Mengejar 100% dengan test asal-asalan jauh lebih buruk daripada 75% dengan test yang benar-benar memeriksa perilaku penting. Pakai coverage untuk menemukan lubang, bukan untuk pamer angka.

    Kebiasaan Sehat Menulis Test

    • Test kecil dan cepat. Satu test menguji satu perilaku. Test yang lambat akan malas dijalankan, dan test yang tidak dijalankan sama saja tidak ada.
    • Nama test menjelaskan skenario. TestHandlerTambahProdukBodyInvalid langsung terbaca maksudnya. TestProduk2 tidak menceritakan apa pun.
    • Jalankan sebelum commit. Biasakan go test ./... sebelum git commit. Perintah ini menjalankan test di semua package dalam project.
    • Kenali t.Helper. Kalau kamu membuat function bantuan untuk test, panggil t.Helper() di baris pertamanya. Dengan begitu saat gagal, Go menunjuk baris pemanggilnya, bukan baris di dalam helper. Cukup tahu dulu, kamu akan membutuhkannya saat file test mulai panjang.

    Latihan: produk_test.go Utuh

    Sebagai latihan penutup, gabungkan semuanya. Ini file test untuk function validasi CRUD produk dengan table driven, ditambah satu handler test.

    package main
    
    import (
    	"encoding/json"
    	"net/http"
    	"net/http/httptest"
    	"testing"
    )
    
    func TestValidasiProduk(t *testing.T) {
    	kasus := []struct {
    		nama     string
    		produk   Produk
    		inginErr bool
    	}{
    		{"produk valid", Produk{Nama: "Kopi Gayo", Harga: 45000, Stok: 10}, false},
    		{"nama kosong", Produk{Nama: "", Harga: 45000, Stok: 10}, true},
    		{"harga nol", Produk{Nama: "Kopi Gayo", Harga: 0, Stok: 10}, true},
    		{"harga minus", Produk{Nama: "Kopi Gayo", Harga: -100, Stok: 10}, true},
    		{"stok minus", Produk{Nama: "Kopi Gayo", Harga: 45000, Stok: -3}, true},
    	}
    
    	for _, k := range kasus {
    		t.Run(k.nama, func(t *testing.T) {
    			err := ValidasiProduk(k.produk)
    			if k.inginErr && err == nil {
    				t.Errorf("mengharapkan error, tapi dapat nil")
    			}
    			if !k.inginErr && err != nil {
    				t.Errorf("tidak mengharapkan error, dapat: %v", err)
    			}
    		})
    	}
    }
    
    func TestHandlerListProduk(t *testing.T) {
    	req := httptest.NewRequest(http.MethodGet, "/produk", nil)
    	rec := httptest.NewRecorder()
    
    	handlerListProduk(rec, req)
    
    	if rec.Code != http.StatusOK {
    		t.Fatalf("status = %d, ingin %d", rec.Code, http.StatusOK)
    	}
    
    	var hasil []Produk
    	if err := json.Unmarshal(rec.Body.Bytes(), &hasil); err != nil {
    		t.Fatalf("body bukan JSON valid: %v", err)
    	}
    }

    Jalankan dengan go test -v. Kalau semuanya benar, hasilnya seperti ini.

    $ go test -v
    === RUN   TestValidasiProduk
    === RUN   TestValidasiProduk/produk_valid
    === RUN   TestValidasiProduk/nama_kosong
    === RUN   TestValidasiProduk/harga_nol
    === RUN   TestValidasiProduk/harga_minus
    === RUN   TestValidasiProduk/stok_minus
    --- PASS: TestValidasiProduk (0.00s)
        --- PASS: TestValidasiProduk/produk_valid (0.00s)
        --- PASS: TestValidasiProduk/nama_kosong (0.00s)
        --- PASS: TestValidasiProduk/harga_nol (0.00s)
        --- PASS: TestValidasiProduk/harga_minus (0.00s)
        --- PASS: TestValidasiProduk/stok_minus (0.00s)
    === RUN   TestHandlerListProduk
    --- PASS: TestHandlerListProduk (0.00s)
    PASS
    ok      toko-api        0.017s

    Barisan PASS itu bukan sekadar hiasan. Itu bukti tertulis bahwa validasi produk dan handler API kamu berperilaku sesuai harapan, dan bukti itu bisa diperbarui kapan saja dengan satu perintah. Coba tambahkan sendiri test untuk handlerTambahProduk dengan kasus sukses, lalu ukur lagi dengan go test -cover dan lihat angkanya naik.

    Selanjutnya: Naik ke Server

    API produk kita sekarang punya endpoint CRUD, database, middleware, proteksi API key, dan barisan test yang menjaga semuanya. Tinggal satu langkah besar yang belum, yaitu keluar dari laptop. Di bagian 15, Deploy API Go ke Server: dari Laptop ke Production, kita akan membawa API ini ke server sungguhan supaya bisa diakses siapa pun.

    Kalau kamu ingin membangun API atau sistem aplikasi yang teruji dan siap production tanpa mengerjakan semuanya sendiri, tim Arrazy bisa membantu dari perancangan sampai deployment.

  • Belajar Kubernetes #2: Perbedaan Docker dan Kubernetes

    Belajar Kubernetes #2: Perbedaan Docker dan Kubernetes

    Perbedaan Docker dan Kubernetes sebenarnya sederhana. Docker adalah alat untuk membungkus aplikasi menjadi container dan menjalankannya di satu mesin. Kubernetes adalah alat untuk mengatur banyak container di banyak mesin sekaligus: menyalakan ulang container yang mati, menambah salinan saat trafik naik, dan membagi beban antar container. Keduanya bukan pesaing. Docker bekerja di level “bagaimana satu container dibuat dan dijalankan”, Kubernetes bekerja di level “bagaimana ratusan container dikelola bersama”.

    Artikel ini bagian kedua dari seri Belajar Kubernetes dari Nol. Kita tidak akan berhenti di teori. Di bagian praktik nanti, kamu akan menghapus pod nginx secara paksa dan melihat sendiri Kubernetes menghidupkannya lagi tanpa disuruh. Itu cara paling cepat memahami kenapa orkestrasi itu ada.

    Prasyarat Sebelum Mulai

    Tutorial ini melanjutkan setup dari Belajar Kubernetes #1: Kubernetes Adalah + Install Minikube. Pastikan Minikube v1.36 sudah terpasang dan cluster bisa jalan dengan minikube start. Versi Kubernetes yang dipakai Minikube saat artikel ini ditulis adalah v1.33. Kalau kamu belum paham konsep container sama sekali, mampir dulu ke seri Belajar Docker, karena semua contoh di sini menganggap kamu sudah pernah menjalankan docker run.

    Rekap Singkat: Apa yang Docker Kerjakan

    Container adalah cara mengemas aplikasi beserta semua dependensinya menjadi satu paket yang bisa jalan di mana saja. Docker mempopulerkan konsep ini lewat tiga hal: Dockerfile untuk mendefinisikan isi paket, image sebagai hasil build yang siap didistribusikan, dan Docker Engine yang menjalankan image tersebut menjadi container.

    Untuk satu server, Docker sudah sangat cukup. Kamu build image, push ke registry, tarik di server, jalankan. Selesai. Ditambah docker compose, kamu bahkan bisa mengelola beberapa container sekaligus, misalnya aplikasi Laravel plus MySQL plus Redis, semuanya didefinisikan dalam satu file YAML.

    Di Mana Docker Saja Mulai Kewalahan

    Masalah muncul ketika aplikasi tumbuh melewati satu mesin. Coba bayangkan beberapa situasi ini:

    • Container mati jam 2 pagi. Docker punya restart policy seperti --restart unless-stopped, tapi itu hanya bekerja selama mesinnya masih hidup. Kalau servernya sendiri yang tumbang, tidak ada yang memindahkan container ke server lain.
    • Trafik naik mendadak. Kamu perlu menambah salinan aplikasi di beberapa server. Dengan Docker murni, kamu SSH ke tiap server satu per satu, jalankan container, lalu atur load balancer manual.
    • Deploy versi baru tanpa downtime. Prosedurnya manual: nyalakan container versi baru, cek sehat, alihkan trafik, matikan versi lama. Salah urutan sedikit, user kena error.
    • Container perlu saling menemukan. Aplikasi di server A perlu tahu alamat database di server B. IP berubah tiap kali container dibuat ulang, jadi hardcode alamat bukan pilihan.

    Semua masalah di atas bisa diselesaikan dengan skrip buatan sendiri. Banyak tim melakukannya, sampai skripnya jadi lebih rumit daripada aplikasinya. Kubernetes lahir untuk menstandarkan solusi masalah-masalah ini.

    Kenapa Perlu Orkestrasi: Empat Kemampuan Inti

    Orkestrasi artinya ada satu sistem yang terus mengawasi kondisi container dan mencocokkannya dengan kondisi yang kamu inginkan. Kamu tidak lagi memberi perintah “jalankan container ini”, melainkan mendeklarasikan “saya mau 3 salinan aplikasi ini selalu hidup”. Sisanya urusan Kubernetes.

    Scaling: menambah salinan dengan satu perintah

    Di Kubernetes, menambah salinan aplikasi dari 1 menjadi 5 cukup satu perintah, dan Kubernetes yang memutuskan container baru ditaruh di mesin mana. Tidak perlu SSH ke server satu per satu.

    Self-healing: container mati dihidupkan sendiri

    Kubernetes membandingkan jumlah container yang hidup dengan jumlah yang kamu deklarasikan. Kurang satu, dia buat satu lagi. Ini bukan fitur tambahan, ini perilaku default. Kita buktikan di bagian praktik nanti.

    Rolling update: ganti versi tanpa downtime

    Saat kamu update image ke versi baru, Kubernetes mengganti container secara bertahap. Container baru dinyalakan dulu, dicek sehat, baru container lama dimatikan. Kalau versi baru bermasalah, ada perintah rollback untuk kembali ke versi sebelumnya. Detailnya kita bahas di bagian 8 seri ini.

    Service discovery: container saling menemukan lewat nama

    Kubernetes memberi nama DNS internal untuk sekelompok container. Aplikasi cukup memanggil http://backend tanpa peduli container backend sedang hidup di mesin mana atau IP-nya berapa. Load balancing antar salinan juga otomatis.

    Perbedaan Peran: Docker Membangun, Kubernetes Mengatur

    Cara paling mudah membedakan keduanya adalah lewat pembagian kerja di alur pengembangan:

    Aspek Docker Kubernetes
    Peran utama Membangun image dan menjalankan container Mengatur container di banyak mesin
    Lingkup Satu mesin Cluster berisi banyak mesin
    Unit kerja Container Pod (berisi satu atau lebih container)
    Kalau container mati Restart di mesin yang sama, kalau restart policy diset Dibuat ulang otomatis, bisa di mesin lain
    Scaling Manual per mesin Deklaratif, satu perintah untuk seluruh cluster
    Contoh perintah docker build, docker run kubectl apply, kubectl scale

    Dalam praktik keduanya dipakai bersama. Alur umumnya: developer menulis Dockerfile, docker build menghasilkan image, image dipush ke registry, lalu Kubernetes menarik image itu dan menjalankannya sebagai container di cluster. Docker hidup di laptop developer dan pipeline CI, Kubernetes hidup di server produksi.

    Posisi containerd setelah dockershim dihapus

    Kamu mungkin pernah baca judul berita “Kubernetes membuang Docker”. Yang sebenarnya terjadi lebih teknis dari itu. Kubernetes berkomunikasi dengan container runtime lewat standar bernama CRI (Container Runtime Interface). Docker Engine tidak berbicara CRI secara native, jadi dulu Kubernetes memelihara kode perantara bernama dockershim. Sejak Kubernetes 1.24 (2022), dockershim dihapus dan Kubernetes memakai runtime yang mendukung CRI langsung, paling umum containerd.

    Bagian menariknya: containerd justru komponen di dalam Docker itu sendiri. Saat kamu menjalankan docker run, yang benar-benar mengeksekusi container adalah containerd. Kubernetes hanya memotong jalur, memanggil containerd langsung tanpa lewat Docker Engine. Image hasil docker build tetap jalan normal di Kubernetes karena keduanya mengikuti standar OCI (Open Container Initiative). Jadi keahlian Docker kamu tidak ada yang terbuang.

    Praktik: Membuktikan Self-Healing di Minikube

    Sekarang bagian paling penting. Nyalakan cluster dulu:

    minikube start

    Di bagian 1 kita menjalankan nginx sebagai pod tunggal. Kali ini kita jalankan nginx lewat Deployment, yaitu objek Kubernetes yang menyimpan deklarasi “berapa salinan yang harus selalu hidup”. Detail Deployment dibahas di bagian 7, sekarang cukup pakai dulu:

    kubectl create deployment web-demo --image=nginx:1.27

    Output yang diharapkan:

    deployment.apps/web-demo created

    Cek pod yang dibuat:

    kubectl get pods
    NAME                        READY   STATUS    RESTARTS   AGE
    web-demo-7c9b8d6f4d-x2k9p   1/1     Running   0          20s

    Nama pod kamu pasti berbeda di bagian akhirnya, karena akhiran itu acak. Sekarang kita berperan jadi bencana. Hapus pod itu secara paksa (ganti nama pod sesuai output di terminalmu):

    kubectl delete pod web-demo-7c9b8d6f4d-x2k9p
    pod "web-demo-7c9b8d6f4d-x2k9p" deleted

    Kalau ini Docker biasa, cerita selesai, container hilang. Sekarang cek lagi:

    kubectl get pods
    NAME                        READY   STATUS    RESTARTS   AGE
    web-demo-7c9b8d6f4d-m5j7q   1/1     Running   0          8s

    Pod baru dengan nama akhiran berbeda muncul dalam hitungan detik, tanpa kamu melakukan apa pun. Kubernetes melihat jumlah pod hidup (0) tidak sesuai deklarasi (1), lalu memperbaikinya sendiri. Ini self-healing yang tadi kita bahas.

    Coba juga scaling. Satu perintah, tiga salinan:

    kubectl scale deployment web-demo --replicas=3
    kubectl get pods
    NAME                        READY   STATUS    RESTARTS   AGE
    web-demo-7c9b8d6f4d-m5j7q   1/1     Running   0          2m
    web-demo-7c9b8d6f4d-b8w4r   1/1     Running   0          10s
    web-demo-7c9b8d6f4d-t6n2c   1/1     Running   0          10s

    Selesai bereksperimen, bersihkan:

    kubectl delete deployment web-demo

    Kapan Kamu Tidak Perlu Kubernetes

    Bagian ini jarang ditulis di tutorial, tapi penting. Kubernetes punya biaya: kurva belajar panjang, komponen yang harus dirawat, dan kebutuhan resource server yang lebih besar. Untuk banyak kasus, docker compose di satu VPS adalah pilihan yang lebih waras.

    Cukup docker compose kalau: aplikasimu jalan nyaman di satu server, downtime beberapa menit saat deploy masih bisa diterima, dan timnya kecil. Di proyek sistem aplikasi yang tim Arrazy kerjakan untuk klien, sebagian besar backend Go dan Laravel memang kami jalankan dengan docker compose di satu VPS, karena skalanya belum menuntut lebih. Pindah ke Kubernetes baru masuk akal ketika kamu butuh lebih dari satu server, zero-downtime deploy jadi keharusan, atau frekuensi deploy sudah tinggi.

    Aturan praktisnya: mulai dari compose, pindah ke Kubernetes saat rasa sakitnya nyata, bukan karena ikut tren.

    Salah Paham yang Sering Muncul

    “Kubernetes adalah pengganti Docker”

    Salah. Keduanya beroperasi di lapisan berbeda dan justru saling melengkapi. Kamu tetap butuh Docker (atau alat sejenis seperti Podman dan Buildah) untuk membangun image. Yang benar-benar digantikan Kubernetes hanyalah Docker Engine sebagai runtime di node cluster, itu pun digantikan oleh containerd yang notabene bagian dari Docker sendiri.

    “Docker Swarm sama dengan Kubernetes”

    Keduanya memang sama-sama orkestrator, tapi bukan barang yang sama. Docker Swarm adalah orkestrator bawaan Docker, lebih sederhana dipelajari, dengan fitur yang jauh lebih terbatas. Kubernetes menang di ekosistem: autoscaling, ekstensi, tooling, dukungan semua cloud provider besar, dan lowongan kerja. Swarm masih hidup dan dipakai, tapi arah industri sudah jelas ke Kubernetes. Untuk belajar yang nilainya jangka panjang, Kubernetes pilihan yang lebih aman.

    Troubleshooting: Error yang Sering Dialami Pemula

    The connection to the server … was refused

    Muncul saat menjalankan perintah kubectl apa pun. Penyebab paling umum: cluster Minikube belum jalan. Solusinya jalankan minikube status, dan kalau statusnya Stopped, nyalakan dengan minikube start. Kalau masih gagal, cek apakah Docker Desktop atau service Docker di mesinmu sudah hidup, karena Minikube dengan driver docker menumpang di situ.

    Pod dihapus tapi tidak hidup lagi

    Kamu praktik self-healing tapi pod tidak muncul kembali. Hampir pasti pod itu dibuat dengan kubectl run (pod tunggal tanpa pengawas), bukan lewat Deployment. Pod tunggal tidak punya siapa pun yang mengawasi jumlahnya. Solusinya buat lewat kubectl create deployment seperti contoh di atas, lalu ulangi eksperimennya.

    Status pod ImagePullBackOff

    Kubernetes gagal menarik image dari registry. Cek dua hal: nama dan tag image salah ketik (misalnya ngnix atau tag yang tidak ada), atau koneksi internet dari dalam Minikube bermasalah. Jalankan kubectl describe pod nama-pod dan baca bagian Events di paling bawah, di situ tertulis alasan persisnya.

    Image hasil docker build tidak ditemukan di Minikube

    Kamu build image lokal, lalu deploy ke Minikube dan kena ErrImagePull. Penyebabnya Docker di laptopmu dan runtime di dalam Minikube punya penyimpanan image terpisah. Solusi paling mudah: minikube image load nama-image:tag untuk menyalin image ke dalam cluster, atau build langsung di dalam Minikube dengan minikube image build -t nama-image:tag .

    Lanjut ke Bagian Berikutnya

    Kamu sekarang paham pembagian kerjanya: Docker membangun dan mengemas, Kubernetes menjalankan dan menjaga. Kamu juga sudah melihat self-healing bekerja dengan mata sendiri. Pertanyaan berikutnya, siapa sebenarnya yang mengawasi pod tadi dan memutuskan pod baru harus dibuat? Jawabannya ada di komponen-komponen cluster, dan itu bahasan “Belajar Kubernetes #3: Arsitektur Kubernetes Cluster” yang terbit menyusul. Pantau daftar terbarunya di halaman Belajar Kubernetes dari Nol.

    Referensi