Static Files #

Menyajikan berkas statis adalah salah satu fungsi paling mendasar dan krusial dari sebuah server web. Baik kita membangun situs portofolio sederhana, mendistribusikan aset gambar, maupun men-deploy aplikasi halaman tunggal (Single Page Application atau SPA) modern yang kompleks, efisiensi server web dalam membaca file dari disk dan mengirimkannya melalui jaringan sangat menentukan performa dan pengalaman pengguna. Caddy melakukan tugas ini dengan sangat optimal menggunakan konfigurasi yang sangat ringkas, menggabungkan efisiensi penanganan berkas tingkat tinggi dengan otomatisasi HTTPS bawaan.

Artikel ini akan membahas secara tuntas cara menyajikan berkas statis secara profesional menggunakan Caddy. Kita akan mendalami konfigurasi dasar, mekanisme internal IO yang digunakan Caddy pada tingkat sistem operasi, pengelolaan hak akses direktori, teknik kustomisasi tipe MIME, strategi penanganan caching yang agresif namun aman, pengamanan berkas sensitif dari akses luar, hingga pola penyebaran (deployment) SPA modern seperti React, Vue, dan Angular di lingkungan produksi.


Konfigurasi Paling Sederhana #

Untuk menyajikan situs statis publik lengkap dengan HTTPS otomatis dan pengalihan (redirect) HTTP ke HTTPS, Caddyfile kita hanya memerlukan tiga baris instruksi berikut:

# Konfigurasi standar penyajian berkas statis
example.com {
    root * /var/www/html
    file_server
}

Di balik kesederhanaan tiga baris ini, Caddy secara otomatis melakukan hal-hal berikut untuk kita di latar belakang:

  • Manajemen TLS ACME: Menghubungi Let’s Encrypt atau ZeroSSL untuk mendapatkan sertifikat TLS dan mengelolanya.
  • MIME Mapping: Mendeteksi ekstensi berkas yang diminta dan mengirimkan tajuk Content-Type yang tepat ke browser.
  • HTTP/2 & HTTP/3: Mengaktifkan negosiasi protokol modern untuk mempercepat pemuatan aset secara paralel.
  • Redirect Otomatis: Mengalihkan semua lalu lintas port HTTP 80 ke port HTTPS 443.

Optimasi Kinerja IO pada Tingkat Kernel (Sendfile) #

Caddy ditulis menggunakan bahasa pemrograman Go, yang memiliki pustaka standar jaringan dan sistem berkas yang sangat efisien. Ketika menyajikan berkas statis yang besar melalui direktif file_server, Caddy memanfaatkan optimasi tingkat kernel yang disebut sendfile (pada Linux dan macOS).

flowchart TD
    subgraph NoSendfile ["Tanpa Sendfile (Penyalinan Memori Berulang)"]
        direction LR
        Disk1["Disk Server"] --> BufKer1["Buffer Kernel"]
        BufKer1 --> BufApp1["Buffer Aplikasi (Go)"]
        BufApp1 --> SockKer1["Socket Kernel"]
        SockKer1 --> Net1["Jaringan"]
    end

    subgraph WithSendfile ["Dengan Sendfile (Zero-Copy Transfer)"]
        direction LR
        Disk2["Disk Server"] --> BufKer2["Buffer Kernel"]
        BufKer2 -->|"Zero-Copy"| SockKer2["Socket Kernel"]
        SockKer2 --> Net2["Jaringan"]
    end

Dengan teknik zero-copy ini, Caddy meminimalkan perpindahan konteks CPU (context switches) dan penggunaan memori. Hal ini membuat Caddy mampu menyajikan ribuan berkas secara bersamaan dengan penggunaan memori RAM yang sangat kecil, menjadikannya pilihan ideal untuk server dengan spesifikasi perangkat keras terbatas.


Direktif root — Memahami Webroot #

Direktif root menetapkan direktori dasar (base directory) tempat berkas-berkas statis kita disimpan pada sistem penyimpanan server. Argumen pertamanya adalah matcher yang menentukan untuk jalur permintaan mana direktori dasar tersebut berlaku.

Sintaks penulisan root:

# root [matcher] [jalur_direktori]

Dalam praktik produksi, kita dapat menetapkan direktori dasar yang berbeda untuk jenis permintaan yang berbeda pula:

example.com {
    # Matcher '*' berarti berlaku untuk semua permintaan sebagai fallback default
    root * /var/www/html
    
    # Menetapkan root khusus untuk aset gambar
    root /images/* /storage/media/images
    
    # Menetapkan root khusus untuk dokumentasi
    root /docs/* /var/www/documentation/dist
    
    file_server
}

Implikasi Penggabungan Jalur (Path Resolution) #

Caddy menyelesaikan jalur berkas dengan cara menggabungkan direktori dasar yang dikonfigurasi dengan URI permintaan yang masuk. Kita harus berhati-hati dalam merancang struktur direktori kita agar sesuai dengan alur pencarian berikut:

Skenario A:
  Konfigurasi: root * /var/www/html
  Permintaan masuk: GET /images/logo.png
  Jalur yang dicari Caddy: /var/www/html/images/logo.png

Skenario B:
  Konfigurasi: root /images/* /storage/media/images
  Permintaan masuk: GET /images/logo.png
  Jalur yang dicari Caddy: /storage/media/images/images/logo.png
  (Perhatikan bahwa prefix "/images" tetap dipertahankan dan ditambahkan ke akhir root)

Jika kita ingin membuang prefix jalur saat mencari berkas pada direktori dasar, kita harus menggunakan direktif uri strip_prefix sebelum memanggil file_server (detail pembahasan ini akan dijelaskan pada artikel File Server).


Pengelolaan Hak Akses Berkas di Linux #

Salah satu penyebab paling sering munculnya kesalahan HTTP 403 Forbidden saat menyajikan berkas statis adalah konfigurasi hak akses berkas (file permissions) yang keliru pada sistem operasi Linux.

Akun pengguna yang menjalankan Caddy (biasanya bernama caddy pada instalasi paket Debian/Ubuntu resmi) harus memiliki hak akses membaca (read) untuk seluruh berkas statis, serta hak akses mengeksekusi (execute) untuk direktori yang menaungi berkas tersebut agar Caddy dapat menelusuri isi folder.

# 1. Memeriksa pengguna yang menjalankan proses Caddy
ps aux | grep caddy

# 2. Mengatur kepemilikan direktori webroot ke pengguna caddy secara rekursif
sudo chown -R caddy:caddy /var/www/html

# 3. Mengatur izin akses yang tepat
# Berikan akses baca-tulis untuk pemilik (caddy) dan baca untuk umum
find /var/www/html -type d -exec chmod 755 {} + # 755 memberikan izin eksekusi pada folder
find /var/www/html -type f -exec chmod 644 {} + # 644 memberikan izin baca pada berkas

# 4. Menguji apakah user caddy dapat membaca file secara manual
sudo -u caddy cat /var/www/html/index.html

Kustomisasi Tipe MIME (MIME Types) #

Secara default, Caddy mendeteksi tipe MIME (Multipurpose Internet Mail Extensions) suatu berkas berdasarkan ekstensi namanya menggunakan database internal Go dan berkas konfigurasi sistem operasi (seperti /etc/mime.types di Linux). Tajuk Content-Type yang tepat sangat krusial; jika browser menerima tipe MIME yang keliru untuk file JavaScript, browser akan memblokir eksekusi file tersebut demi keamanan (MIME-sniffing protection).

Jika kita menggunakan format berkas modern atau ekstensi kustom, kita dapat memaksa tajuk Content-Type yang tepat menggunakan pencocok (matcher) dan direktif header di Caddyfile:

example.com {
    root * /var/www/html
    
    # Menjamin berkas WebAssembly disajikan dengan tipe MIME yang benar
    @wasm path *.wasm
    header @wasm Content-Type "application/wasm"
    
    # Menjamin format gambar modern AVIF disajikan dengan benar
    @avif path *.avif
    header @avif Content-Type "image/avif"
    
    # Menyajikan berkas manifest aplikasi web progresif (PWA)
    @webmanifest path *.webmanifest
    header @webmanifest Content-Type "application/manifest+json"
    
    file_server
}

Strategi Manajemen Caching untuk Performa Maksimal #

Caching di sisi browser pelanggan adalah kunci utama untuk meningkatkan kecepatan pemuatan halaman berulang (page load speed) secara dramatis dan mengurangi beban transfer data (bandwidth) pada server kita. Strategi caching yang buruk dapat menyebabkan pengguna menerima halaman web lama yang usang, atau sebaliknya, server terpaksa mengirimkan kembali gambar statis yang sama yang sebenarnya tidak pernah berubah.

Berikut adalah konfigurasi strategi caching modern yang direkomendasikan untuk situs statis yang memanfaatkan skema cache-busting (seperti build React/Vite yang menambahkan hash unik pada nama berkas aset, misalnya main.a1b2c3d4.js):

example.com {
    root * /var/www/html
    
    # 1. Strategi Aset Ter-hash (Cache Selamanya - 1 Tahun)
    # Karena nama file akan berubah jika isinya berubah, file ini aman di-cache selamanya
    @hashedAssets {
        path_regexp \.[a-f0-9]{8,}\.(js|css|woff2?|ttf|eot)$
    }
    header @hashedAssets Cache-Control "public, max-age=31536000, immutable"
    
    # 2. Strategi Aset Gambar Umum (Cache Sedang - 30 Hari)
    @images path *.jpg *.jpeg *.png *.gif *.webp *.avif *.svg *.ico
    header @images Cache-Control "public, max-age=2592000"
    
    # 3. Strategi Berkas HTML Utama (Tanpa Cache Lokal - Wajib Validasi)
    # Browser harus selalu menanyakan ke server apakah file HTML telah berubah (menggunakan ETag)
    @html path *.html /
    header @html Cache-Control "no-cache"
    
    # 4. Berkas Konfigurasi Sensitif & Dinamis (Jangan Simpan Cache Sama Sekali)
    @dynamic path *.json *.xml
    header @dynamic Cache-Control "no-store"
    
    encode gzip zstd
    file_server
}

Memahami Perbedaan Direktif Cache-Control: #

  • max-age=31536000: Menginstruksikan browser untuk menyimpan berkas tersebut selama 1 tahun (31.536.000 detik) tanpa perlu menghubungi server lagi.
  • immutable: Memberitahu browser bahwa berkas tersebut tidak akan pernah berubah namanya. Browser tidak perlu mengirimkan kueri validasi conditional GET selama masa max-age berlaku, bahkan jika pengguna menekan tombol refresh browser.
  • no-cache: Browser menyimpan berkas, tetapi wajib melakukan validasi ke server (menggunakan kueri HTTP If-None-Match dengan ETag) sebelum menyajikannya ke pengguna. Jika server merespons dengan status 304 Not Modified, cache lokal akan digunakan.
  • no-store: Browser sama sekali tidak boleh menyimpan berkas ini ke dalam penyimpanan cache lokal disk. Berkas harus selalu diunduh penuh dari server setiap kali diakses.

Pengamanan Berkas Sensitif dan Tersembunyi #

Saat kita men-deploy sebuah proyek web, terkadang ada berkas sensitif yang ikut terunggah ke dalam direktori webroot, seperti konfigurasi environment (.env), repositori Git (.git), atau dependensi proyek (node_modules). Membiarkan berkas ini dapat diakses oleh publik adalah celah keamanan yang sangat berbahaya.

Kita dapat mengonfigurasi Caddy untuk memblokir akses ke berkas-berkas tersebut dan mengembalikan respons HTTP 404 Not Found agar keberadaan berkas sensitif tersebut tidak diketahui oleh pemindai otomatis (scanner):

example.com {
    root * /var/www/html
    
    # Mendefinisikan kelompok berkas sensitif menggunakan matcher path
    @sensitive {
        path /.env
        path /.git/*
        path /.gitignore
        path /wp-config.php
        path /composer.json
        path /package.json
        path /package-lock.json
        path /*.key
        path /*.pem
        path /*.sql
        path /*.db
    }
    
    # Mengembalikan 404 untuk semua pencocokan di atas
    respond @sensitive 404
    
    # Memblokir seluruh berkas tersembunyi yang diawali tanda titik (dotfiles)
    @dotfiles {
        path_regexp ^/\.
    }
    respond @dotfiles 404
    
    file_server
}

Deployment SPA (React, Vue, Angular) #

Aplikasi Halaman Tunggal (Single Page Application atau SPA) menggunakan perutean di sisi klien (client-side routing). Saat pengguna mengakses https://app.com/about, browser mencoba meminta berkas /about ke server Caddy. Karena berkas /about tidak ada di sistem penyimpanan server (hanya berupa rute virtual di React Router atau Vue Router), server Caddy secara default akan mengembalikan kesalahan 404 Not Found.

Agar perutean sisi klien berfungsi dengan benar, Caddy harus dikonfigurasi untuk menyajikan berkas indeks utama index.html sebagai fallback ketika berkas yang diminta tidak ditemukan di disk:

# Konfigurasi Caddyfile untuk Aplikasi SPA
app.example.com {
    root * /var/www/app/dist
    
    encode gzip zstd
    
    # Header keamanan untuk aplikasi web
    header {
        Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }
    
    # Cache agresif untuk aset hasil build bundler (Vite/Webpack)
    @hashedAssets {
        path_regexp assets/.*\.[a-f0-9]{8}\.(js|css)$
    }
    header @hashedAssets Cache-Control "public, max-age=31536000, immutable"
    
    # Atur cache no-cache untuk HTML utama agar selalu diperbarui saat deploy baru
    @html path *.html /
    header @html Cache-Control "no-cache"
    
    # KUNCI UNTUK SPA:
    # Jika berkas yang diminta TIDAK ADA di filesystem, dan jalur BUKAN merupakan
    # panggilan API (/api/*), tulis ulang jalur menjadi /index.html
    @notFound {
        not file
        not path /api/*
    }
    rewrite @notFound /index.html
    
    file_server
}

Struktur Keluaran Build Vite/React yang Khas: #

Berikut adalah visualisasi struktur direktori yang dihasilkan setelah kita menjalankan npm run build pada proyek React/Vite kita. Caddy akan menyajikan berkas-berkas ini berdasarkan konfigurasi di atas:

/var/www/app/dist/
  ├── index.html                 # Di-serve sebagai fallback untuk client-side routing
  ├── favicon.ico
  ├── assets/
  │   ├── index-a1b2c3d4.js      # Mengandung hash - di-cache 1 tahun (immutable)
  │   ├── index-e5f6g7h8.css     # Mengandung hash - di-cache 1 tahun (immutable)
  │   └── vendor-i9j0k1l2.js
  └── images/
      └── logo.png               # Di-cache 30 hari (caching gambar umum)

Optimasi Bandwidth Menggunakan Kompresi Dinamis dan Statis #

Mengompresi respons teks (seperti HTML, CSS, JavaScript, dan JSON) sebelum dikirimkan ke jaringan sangat penting untuk menghemat kuota data dan mempercepat waktu muat halaman, terutama bagi pengguna dengan koneksi internet seluler yang lambat.

Caddy mendukung dua metode kompresi:

1. Kompresi Dinamis (On-The-Fly) #

Caddy secara otomatis mengompresi data sebelum dikirimkan ke klien menggunakan algoritma Gzip atau Zstandard (zstd) berdasarkan isi tajuk Accept-Encoding yang dikirimkan oleh browser:

example.com {
    root * /var/www/html
    
    # Mengaktifkan kompresi dinamis
    encode {
        # Zstandard menawarkan rasio kompresi yang lebih baik dan lebih cepat dari Gzip
        zstd
        gzip 6
        
        # Hanya kompres berkas yang berukuran minimal 1 KB (1024 bytes)
        minimum_length 1024
    }
    
    file_server
}

2. Menyajikan Berkas Kompresi Awal (Precompressed Assets) #

Melakukan kompresi dinamis setiap kali ada permintaan masuk membutuhkan daya komputasi CPU server. Di lingkungan produksi dengan lalu lintas tinggi, hal ini dapat membebani CPU. Solusi terbaik adalah melakukan kompresi berkas statis terlebih dahulu saat proses build di mesin CI/CD kita, kemudian memerintahkan Caddy untuk langsung menyajikan berkas terkompresi tersebut:

example.com {
    root * /var/www/html
    
    file_server {
        # Jika klien mendukung Brotli ("br") dan berkas "style.css.br" ada di disk,
        # Caddy akan langsung mengirimkannya tanpa melakukan kompresi berulang.
        precompressed br gzip
    }
}

Naskah Otomatisasi Pembuatan Berkas Kompresi Awal (CI/CD): #

Kita dapat menyertakan naskah (script) bash sederhana berikut pada alur kerja integrasi berkelanjutan kita untuk menghasilkan berkas .gz dan .br secara otomatis sebelum diunggah ke server:

# Menavigasi ke direktori hasil build aplikasi
cd /var/www/html

# Membuat versi kompresi Gzip (.gz) dengan tingkat kompresi maksimal (-9)
find . -type f -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.svg" | while read -r f; do
    gzip -k -9 "$f"
done

# Membuat versi kompresi Brotli (.br) dengan kualitas maksimal (-q 11)
# (Memerlukan perkakas 'brotli' terinstal di sistem)
find . -type f -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.svg" | while read -r f; do
    brotli -k -q 11 "$f"
done

Ringkasan #

  • Konfigurasi Minimal — Gabungan direktif root * /path dan file_server sudah cukup untuk menyajikan situs statis lengkap dengan protokol HTTPS otomatis.
  • Efisiensi Sendfile — Caddy secara bawaan menggunakan sistem panggilan sendfile tingkat kernel untuk meminimalkan beban memori dan perpindahan konteks CPU saat menyajikan file.
  • Hak Akses Linux — Pengguna sistem caddy wajib memiliki izin membaca berkas statis (644) dan izin mengeksekusi (755) pada direktori untuk menelusuri folder.
  • Manajemen Cache — Gunakan tajuk Cache-Control yang agresif (immutable) untuk aset ter-hash, tajuk no-cache untuk berkas HTML utama, dan no-store untuk data dinamis.
  • Perutean SPA — Gunakan aturan pencocokan not file dan lakukan direktif rewrite ke /index.html agar perutean virtual sisi klien (React/Vue/Angular) tidak menyebabkan error 404.
  • Kompresi Aset — Aktifkan direktif encode untuk kompresi dinamis, atau gunakan opsi precompressed untuk langsung menyajikan berkas .gz/.br yang telah dikompresi sebelumnya guna menghemat daya CPU server.

← Sebelumnya: DNS Challenge   Berikutnya: Virtual Host →

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