<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.haidhi.my.id/wiki/index.php?action=history&amp;feed=atom&amp;title=Deploy_app_bare-metal</id>
	<title>Deploy app bare-metal - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.haidhi.my.id/wiki/index.php?action=history&amp;feed=atom&amp;title=Deploy_app_bare-metal"/>
	<link rel="alternate" type="text/html" href="https://wiki.haidhi.my.id/wiki/index.php?title=Deploy_app_bare-metal&amp;action=history"/>
	<updated>2026-11-01T21:53:28Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.0</generator>
	<entry>
		<id>https://wiki.haidhi.my.id/wiki/index.php?title=Deploy_app_bare-metal&amp;diff=873&amp;oldid=prev</id>
		<title>SanzSkuy: Created page with &quot;Catatan tentang cara deploy aplikasi (Python FastAPI, Rust Actix, Node frontend) langsung di server Ubuntu (contoh: &lt;code&gt;marshall-3&lt;/code&gt;) tanpa Docker, memakai &#039;&#039;&#039;uv&#039;&#039;&#039;, &#039;&#039;&#039;systemd&#039;&#039;&#039;, dan &#039;&#039;&#039;Ansible&#039;&#039;&#039;. Fokus utama catatan ini adalah sisi &#039;&#039;&#039;sysadmin&#039;&#039;&#039;: layout direktori, model user dan permission, hardening service, dan otomasi.  == Gambaran besar ==  Docker memberi tiga hal: &#039;&#039;&#039;isolasi&#039;&#039;&#039;, &#039;&#039;&#039;reproducibility&#039;&#039;&#039;, dan &#039;&#039;&#039;packaging&#039;&#039;&#039;. Di bare metal, ketiganya tetap b...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.haidhi.my.id/wiki/index.php?title=Deploy_app_bare-metal&amp;diff=873&amp;oldid=prev"/>
		<updated>2026-10-20T06:42:08Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;Catatan tentang cara deploy aplikasi (Python FastAPI, Rust Actix, Node frontend) langsung di server Ubuntu (contoh: &amp;lt;code&amp;gt;marshall-3&amp;lt;/code&amp;gt;) tanpa Docker, memakai &amp;#039;&amp;#039;&amp;#039;uv&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;systemd&amp;#039;&amp;#039;&amp;#039;, dan &amp;#039;&amp;#039;&amp;#039;Ansible&amp;#039;&amp;#039;&amp;#039;. Fokus utama catatan ini adalah sisi &amp;#039;&amp;#039;&amp;#039;sysadmin&amp;#039;&amp;#039;&amp;#039;: layout direktori, model user dan permission, hardening service, dan otomasi.  == Gambaran besar ==  Docker memberi tiga hal: &amp;#039;&amp;#039;&amp;#039;isolasi&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;reproducibility&amp;#039;&amp;#039;&amp;#039;, dan &amp;#039;&amp;#039;&amp;#039;packaging&amp;#039;&amp;#039;&amp;#039;. Di bare metal, ketiganya tetap b...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Catatan tentang cara deploy aplikasi (Python FastAPI, Rust Actix, Node frontend) langsung di server Ubuntu (contoh: &amp;lt;code&amp;gt;marshall-3&amp;lt;/code&amp;gt;) tanpa Docker, memakai &amp;#039;&amp;#039;&amp;#039;uv&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;systemd&amp;#039;&amp;#039;&amp;#039;, dan &amp;#039;&amp;#039;&amp;#039;Ansible&amp;#039;&amp;#039;&amp;#039;. Fokus utama catatan ini adalah sisi &amp;#039;&amp;#039;&amp;#039;sysadmin&amp;#039;&amp;#039;&amp;#039;: layout direktori, model user dan permission, hardening service, dan otomasi.&lt;br /&gt;
&lt;br /&gt;
== Gambaran besar ==&lt;br /&gt;
&lt;br /&gt;
Docker memberi tiga hal: &amp;#039;&amp;#039;&amp;#039;isolasi&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;reproducibility&amp;#039;&amp;#039;&amp;#039;, dan &amp;#039;&amp;#039;&amp;#039;packaging&amp;#039;&amp;#039;&amp;#039;. Di bare metal, ketiganya tetap bisa didapat dengan alat bawaan Linux:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Kebutuhan !! Di Docker !! Di bare metal&lt;br /&gt;
|-&lt;br /&gt;
| Isolasi proses || namespace, cgroup || user terpisah per app + hardening systemd (juga pakai namespace &amp;amp; cgroup)&lt;br /&gt;
|-&lt;br /&gt;
| Dependency terkunci || image || lock file (&amp;lt;code&amp;gt;uv.lock&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Cargo.lock&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt;) + venv per release&lt;br /&gt;
|-&lt;br /&gt;
| Restart otomatis || &amp;lt;code&amp;gt;restart: unless-stopped&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;Restart=on-failure&amp;lt;/code&amp;gt; di unit systemd&lt;br /&gt;
|-&lt;br /&gt;
| Log || &amp;lt;code&amp;gt;docker logs&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;journalctl -u nama-app&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Rollback || tag image lama || folder release lama + symlink&lt;br /&gt;
|-&lt;br /&gt;
| Deploy berulang || compose / CI || Ansible&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Kapan bare metal masuk akal:&amp;#039;&amp;#039;&amp;#039; sedikit server, workload GPU, tim kecil, atau binary tunggal (Rust/Go).&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Kapan Docker lebih unggul:&amp;#039;&amp;#039;&amp;#039; banyak server, scaling horizontal, atau dependency sistem yang bentrok antar app (misalnya versi CUDA berbeda per app, karena di bare metal semua app berbagi satu driver NVIDIA).&lt;br /&gt;
&lt;br /&gt;
== Layout direktori (FHS) ==&lt;br /&gt;
&lt;br /&gt;
Setiap app memakai lokasi standar &amp;#039;&amp;#039;Filesystem Hierarchy Standard&amp;#039;&amp;#039;, supaya jelas mana kode, mana config, mana data:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Path !! Isi !! Kenapa di sini&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/&amp;amp;lt;app&amp;amp;gt;/releases/&amp;amp;lt;versi&amp;amp;gt;/&amp;lt;/code&amp;gt; || kode + &amp;lt;code&amp;gt;.venv&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;target&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;dist&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/opt&amp;lt;/code&amp;gt; untuk software tambahan yang bukan dari paket OS&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/&amp;amp;lt;app&amp;amp;gt;/current&amp;lt;/code&amp;gt; || symlink ke release aktif || service selalu menunjuk ke sini; ganti versi = ganti symlink&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/etc/&amp;amp;lt;app&amp;amp;gt;/&amp;amp;lt;app&amp;amp;gt;.env&amp;lt;/code&amp;gt; || environment variable &amp;amp; secret || &amp;lt;code&amp;gt;/etc&amp;lt;/code&amp;gt; untuk konfigurasi milik host, terpisah dari kode&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;/&amp;lt;/code&amp;gt; || data, cache model, upload, cache uv/cargo/npm || &amp;lt;code&amp;gt;/var/lib&amp;lt;/code&amp;gt; untuk state yang ditulis aplikasi saat jalan&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/uv/python/&amp;lt;/code&amp;gt; || interpreter Python bersama || dipakai semua app, bukan milik satu user&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/etc/systemd/system/&amp;amp;lt;app&amp;amp;gt;.service&amp;lt;/code&amp;gt; || unit service || lokasi unit buatan admin (bukan dari paket)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Pemisahan ini penting untuk permission: &amp;#039;&amp;#039;&amp;#039;kode&amp;#039;&amp;#039;&amp;#039; boleh read-only saat runtime, &amp;#039;&amp;#039;&amp;#039;config&amp;#039;&amp;#039;&amp;#039; hanya root yang boleh ubah, dan &amp;#039;&amp;#039;&amp;#039;hanya folder data&amp;#039;&amp;#039;&amp;#039; yang boleh ditulis aplikasi.&lt;br /&gt;
&lt;br /&gt;
== Model user &amp;amp; permission ==&lt;br /&gt;
&lt;br /&gt;
=== Satu app, satu system user ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo useradd --system --home-dir /var/lib/transcribe-api \&lt;br /&gt;
  --shell /usr/sbin/nologin transcribe&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;--system&amp;lt;/code&amp;gt;: UID diambil dari rentang system (di Ubuntu di bawah 1000), tidak muncul di layar login, dan tidak dibuatkan mailbox atau aging password.&lt;br /&gt;
* &amp;lt;code&amp;gt;--shell /usr/sbin/nologin&amp;lt;/code&amp;gt;: user tidak bisa login interaktif. Kalau proses app dibobol, penyerang tidak otomatis dapat shell login.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Kenapa per app?&amp;#039;&amp;#039;&amp;#039; Kalau satu app bocor, app lain dan file-nya tidak ikut terbaca. Ini prinsip &amp;#039;&amp;#039;least privilege&amp;#039;&amp;#039;.&lt;br /&gt;
&lt;br /&gt;
Perintah berguna:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
id transcribe                 # UID, GID, dan group tambahan&lt;br /&gt;
getent passwd transcribe      # home &amp;amp; shell&lt;br /&gt;
getent group | grep transcribe&lt;br /&gt;
sudo -u transcribe -H bash -c &amp;#039;whoami; echo $HOME&amp;#039;   # tes sebagai user app&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Salah nama user?&amp;#039;&amp;#039;&amp;#039; Rename tanpa bikin ulang:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -l transcribe -d /var/lib/transcribe-api appuser   # ganti login &amp;amp; home&lt;br /&gt;
sudo groupmod -n transcribe appuser                            # ganti nama group&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Karena UID/GID tidak berubah, ownership file otomatis ikut. &amp;lt;code&amp;gt;usermod -d&amp;lt;/code&amp;gt; akan gagal kalau user sedang dipakai proses yang jalan, jadi stop service-nya dulu.&lt;br /&gt;
&lt;br /&gt;
Error &amp;lt;code&amp;gt;chown: invalid group: &amp;#039;root:appuser&amp;#039;&amp;lt;/code&amp;gt; artinya group tersebut sudah tidak ada (sudah di-rename atau dihapus). Cek dengan &amp;lt;code&amp;gt;getent group&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Tabel ownership &amp;amp; mode ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Path !! Owner:Group !! Mode !! Artinya&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; || root:root || &amp;lt;code&amp;gt;0755&amp;lt;/code&amp;gt; || hanya root yang bisa membuat/mengganti symlink &amp;lt;code&amp;gt;current&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/&amp;amp;lt;app&amp;amp;gt;/releases&amp;lt;/code&amp;gt; || app:app || &amp;lt;code&amp;gt;0755&amp;lt;/code&amp;gt; || user app meng-clone dan build release di sini&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/etc/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; || root:app || &amp;lt;code&amp;gt;0750&amp;lt;/code&amp;gt; || app boleh masuk &amp;amp; baca, user lain tidak bisa melihat isinya&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/etc/&amp;amp;lt;app&amp;amp;gt;/&amp;amp;lt;app&amp;amp;gt;.env&amp;lt;/code&amp;gt; || root:app || &amp;lt;code&amp;gt;0640&amp;lt;/code&amp;gt; || root tulis, app baca, lainnya tidak bisa apa-apa&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; || app:app || &amp;lt;code&amp;gt;0750&amp;lt;/code&amp;gt; || satu-satunya tempat app boleh menulis&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;/.ssh&amp;lt;/code&amp;gt; || app:app || &amp;lt;code&amp;gt;0700&amp;lt;/code&amp;gt; || deploy key; SSH menolak key yang permission-nya terlalu terbuka&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;/opt/uv/python&amp;lt;/code&amp;gt; || root:root || &amp;lt;code&amp;gt;a+rX&amp;lt;/code&amp;gt; || semua user boleh baca &amp;amp; eksekusi interpreter, tidak boleh ubah&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Membaca mode oktal ===&lt;br /&gt;
&lt;br /&gt;
Tiap digit = &amp;#039;&amp;#039;&amp;#039;owner&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;group&amp;#039;&amp;#039;&amp;#039;, &amp;#039;&amp;#039;&amp;#039;others&amp;#039;&amp;#039;&amp;#039;. Nilainya jumlah dari &amp;lt;code&amp;gt;r=4&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;w=2&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;x=1&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Mode !! Owner !! Group !! Others !! Dipakai untuk&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;0755&amp;lt;/code&amp;gt; || rwx || r-x || r-x || folder publik, binary&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;0750&amp;lt;/code&amp;gt; || rwx || r-x || --- || folder privat yang boleh dibaca group&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;0700&amp;lt;/code&amp;gt; || rwx || --- || --- || folder sangat privat (&amp;lt;code&amp;gt;.ssh&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;0640&amp;lt;/code&amp;gt; || rw- || r-- || --- || file secret yang dibaca group&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;0600&amp;lt;/code&amp;gt; || rw- || --- || --- || private key&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Untuk &amp;#039;&amp;#039;&amp;#039;folder&amp;#039;&amp;#039;&amp;#039;, bit &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt; artinya &amp;quot;boleh masuk (traverse)&amp;quot;, bukan &amp;quot;eksekusi&amp;quot;. Tanpa &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt; di folder induk, file di dalamnya tidak bisa diakses walaupun file-nya sendiri &amp;lt;code&amp;gt;r&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;chmod -R a+rX&amp;lt;/code&amp;gt; (huruf X besar) memberi &amp;lt;code&amp;gt;x&amp;lt;/code&amp;gt; hanya ke folder dan file yang sudah executable. Ini cara aman membuka akses baca satu pohon direktori tanpa membuat semua file teks jadi executable.&lt;br /&gt;
&lt;br /&gt;
=== Kenapa env file &amp;lt;code&amp;gt;root:app 0640&amp;lt;/code&amp;gt;? ===&lt;br /&gt;
&lt;br /&gt;
* App perlu &amp;#039;&amp;#039;&amp;#039;membaca&amp;#039;&amp;#039;&amp;#039; secret (DATABASE_URL, SECRET_KEY), tapi tidak perlu &amp;#039;&amp;#039;&amp;#039;mengubahnya&amp;#039;&amp;#039;&amp;#039;.&lt;br /&gt;
* Kalau app dibobol, penyerang tidak bisa mengganti config agar persisten.&lt;br /&gt;
* User lain di server tidak bisa membaca secret sama sekali.&lt;br /&gt;
* Jangan simpan secret di &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; dalam folder kode: gampang ikut ter-commit, dan ikut terbaca siapa pun yang bisa membaca kode.&lt;br /&gt;
&lt;br /&gt;
=== Toolchain bersama &amp;amp; masalah home directory ===&lt;br /&gt;
&lt;br /&gt;
uv bisa meng-install Python sendiri. Defaultnya ke &amp;lt;code&amp;gt;~/.local/share/uv/python&amp;lt;/code&amp;gt;. Masalahnya:&lt;br /&gt;
&lt;br /&gt;
* venv &amp;#039;&amp;#039;&amp;#039;hanya symlink&amp;#039;&amp;#039;&amp;#039; ke interpreter. Kalau interpreter ada di home user A, user B tidak bisa memakainya.&lt;br /&gt;
* Unit systemd dengan &amp;lt;code&amp;gt;ProtectHome=true&amp;lt;/code&amp;gt; membuat &amp;lt;code&amp;gt;/home&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/root&amp;lt;/code&amp;gt;, dan &amp;lt;code&amp;gt;/run/user&amp;lt;/code&amp;gt; tidak terlihat. Interpreter di home = service gagal start.&lt;br /&gt;
&lt;br /&gt;
Solusinya taruh di lokasi bersama dan buka akses baca:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
export UV_PYTHON_INSTALL_DIR=/opt/uv/python&lt;br /&gt;
sudo -E uv python install 3.13&lt;br /&gt;
sudo chmod -R a+rX /opt/uv/python&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Pola yang sama untuk Rust: &amp;lt;code&amp;gt;RUSTUP_HOME=/opt/rust/rustup&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;CARGO_HOME=/opt/rust/cargo&amp;lt;/code&amp;gt;, lalu &amp;lt;code&amp;gt;a+rX&amp;lt;/code&amp;gt;. Cache per app (registry cargo, cache uv, cache npm) ditaruh di &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; yang memang writable.&lt;br /&gt;
&lt;br /&gt;
=== Akses GPU ===&lt;br /&gt;
&lt;br /&gt;
Device NVIDIA ada di &amp;lt;code&amp;gt;/dev/nvidia*&amp;lt;/code&amp;gt;:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls -l /dev/nvidia*&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Kalau mode-nya &amp;lt;code&amp;gt;crw-rw-rw-&amp;lt;/code&amp;gt;, semua user bisa memakai GPU. Kalau hanya group &amp;lt;code&amp;gt;video&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;render&amp;lt;/code&amp;gt;, tambahkan user app ke group tersebut (&amp;lt;code&amp;gt;usermod -aG video transcribe&amp;lt;/code&amp;gt;) dan tulis &amp;lt;code&amp;gt;SupplementaryGroups=video&amp;lt;/code&amp;gt; di unit. Opsi &amp;lt;code&amp;gt;-a&amp;lt;/code&amp;gt; wajib; tanpa itu, group tambahan lain akan terhapus.&lt;br /&gt;
&lt;br /&gt;
=== sudo yang sempit ===&lt;br /&gt;
&lt;br /&gt;
Kalau deploy dijalankan sebagai user app dan perlu restart service, beri izin &amp;#039;&amp;#039;&amp;#039;hanya&amp;#039;&amp;#039;&amp;#039; untuk perintah itu:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
echo &amp;#039;transcribe ALL=(root) NOPASSWD: /usr/bin/systemctl restart transcribe-api, /usr/bin/systemctl restart transcribe-api-worker&amp;#039; \&lt;br /&gt;
  | sudo tee /etc/sudoers.d/transcribe-api&lt;br /&gt;
sudo chmod 0440 /etc/sudoers.d/transcribe-api&lt;br /&gt;
sudo visudo -cf /etc/sudoers.d/transcribe-api     # validasi sintaks!&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Selalu validasi dengan &amp;lt;code&amp;gt;visudo -c&amp;lt;/code&amp;gt;. File sudoers yang rusak bisa mengunci &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; untuk semua orang.&lt;br /&gt;
* Path harus absolut dan persis sama. Di Ubuntu modern &amp;lt;code&amp;gt;/bin&amp;lt;/code&amp;gt; adalah symlink ke &amp;lt;code&amp;gt;/usr/bin&amp;lt;/code&amp;gt;, tapi sudo membandingkan string path, jadi cek dengan &amp;lt;code&amp;gt;command -v systemctl&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Jangan beri &amp;lt;code&amp;gt;NOPASSWD: ALL&amp;lt;/code&amp;gt; ke user app. Itu sama saja memberi root.&lt;br /&gt;
&lt;br /&gt;
Kalau deploy dijalankan lewat Ansible dengan user admin + &amp;lt;code&amp;gt;become&amp;lt;/code&amp;gt;, aturan sudoers untuk user app &amp;#039;&amp;#039;&amp;#039;tidak diperlukan sama sekali&amp;#039;&amp;#039;&amp;#039;.&lt;br /&gt;
&lt;br /&gt;
=== Siapa yang boleh menulis kode? ===&lt;br /&gt;
&lt;br /&gt;
Di setup ini user app yang meng-clone dan build release, jadi kode bisa ditulis oleh user app. Ini sederhana tapi berarti app yang dibobol bisa mengubah kodenya sendiri. Alternatif yang lebih ketat:&lt;br /&gt;
&lt;br /&gt;
* Buat user &amp;lt;code&amp;gt;deployer&amp;lt;/code&amp;gt; terpisah yang meng-clone &amp;amp; build semua release.&lt;br /&gt;
* User app hanya punya akses baca + eksekusi ke &amp;lt;code&amp;gt;/opt/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Bonus: cache uv bisa dipakai bersama antar app, sehingga hardlink menghemat disk (berguna untuk paket besar seperti torch).&lt;br /&gt;
&lt;br /&gt;
Pada level ini, &amp;lt;code&amp;gt;ProtectSystem=strict&amp;lt;/code&amp;gt; sudah menahan sebagian besar risiko: saat runtime seluruh filesystem read-only bagi proses app, kecuali &amp;lt;code&amp;gt;ReadWritePaths&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== systemd ==&lt;br /&gt;
&lt;br /&gt;
=== Anatomi unit ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot;&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=Transcribe API&lt;br /&gt;
After=network-online.target redis-server.service&lt;br /&gt;
Wants=network-online.target redis-server.service&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
User=transcribe&lt;br /&gt;
Group=transcribe&lt;br /&gt;
SupplementaryGroups=video&lt;br /&gt;
WorkingDirectory=/opt/transcribe-api/current&lt;br /&gt;
Environment=HOME=/var/lib/transcribe-api&lt;br /&gt;
Environment=PORT=6969&lt;br /&gt;
EnvironmentFile=-/etc/transcribe-api/transcribe-api.env&lt;br /&gt;
ExecStart=/opt/transcribe-api/current/.venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 6969&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=5&lt;br /&gt;
&lt;br /&gt;
NoNewPrivileges=true&lt;br /&gt;
ProtectSystem=strict&lt;br /&gt;
ProtectHome=true&lt;br /&gt;
PrivateTmp=true&lt;br /&gt;
ReadWritePaths=/var/lib/transcribe-api&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Catatan per baris:&lt;br /&gt;
* &amp;lt;code&amp;gt;After=&amp;lt;/code&amp;gt; mengatur &amp;#039;&amp;#039;&amp;#039;urutan&amp;#039;&amp;#039;&amp;#039;, &amp;lt;code&amp;gt;Wants=&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Requires=&amp;lt;/code&amp;gt; mengatur &amp;#039;&amp;#039;&amp;#039;ketergantungan&amp;#039;&amp;#039;&amp;#039;. Hanya &amp;lt;code&amp;gt;After=&amp;lt;/code&amp;gt; tidak membuat Redis ikut dijalankan.&lt;br /&gt;
* &amp;lt;code&amp;gt;EnvironmentFile=-...&amp;lt;/code&amp;gt;: tanda &amp;lt;code&amp;gt;-&amp;lt;/code&amp;gt; artinya &amp;quot;jangan gagal kalau file tidak ada&amp;quot;. Variabel dari file &amp;#039;&amp;#039;&amp;#039;meng-override&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;Environment=&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Format env file systemd: &amp;lt;code&amp;gt;KEY=value&amp;lt;/code&amp;gt; per baris, &amp;#039;&amp;#039;&amp;#039;tanpa&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;export&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;ExecStart&amp;lt;/code&amp;gt; harus path absolut. Panggil binary venv langsung, bukan &amp;lt;code&amp;gt;uv run&amp;lt;/code&amp;gt;, karena &amp;lt;code&amp;gt;uv run&amp;lt;/code&amp;gt; bisa mencoba sync dependency saat start.&lt;br /&gt;
* Worker sebaiknya &amp;lt;code&amp;gt;Restart=always&amp;lt;/code&amp;gt; dan &amp;lt;code&amp;gt;TimeoutStopSec=120&amp;lt;/code&amp;gt; supaya job yang sedang jalan sempat selesai saat restart (systemd mengirim SIGTERM dulu, baru SIGKILL setelah timeout).&lt;br /&gt;
&lt;br /&gt;
=== Directive hardening ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Directive !! Efek&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;NoNewPrivileges=true&amp;lt;/code&amp;gt; || proses dan turunannya tidak bisa naik hak akses (setuid, file capabilities tidak berlaku)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ProtectSystem=strict&amp;lt;/code&amp;gt; || seluruh filesystem read-only bagi service&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReadWritePaths=&amp;lt;/code&amp;gt; || pengecualian: path yang tetap boleh ditulis&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ProtectHome=true&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/home&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/root&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/run/user&amp;lt;/code&amp;gt; tidak terlihat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PrivateTmp=true&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;/tmp&amp;lt;/code&amp;gt; privat per service, tidak berbagi dengan proses lain&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Ukur tingkat keamanan unit:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemd-analyze security transcribe-api&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Output-nya memberi skor dan daftar directive lain yang bisa ditambahkan (misalnya &amp;lt;code&amp;gt;ProtectKernelTunables&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;RestrictAddressFamilies&amp;lt;/code&amp;gt;). Tambahkan bertahap dan tes, karena hardening yang terlalu ketat bisa mematahkan app (misalnya akses GPU butuh device yang tidak diblokir &amp;lt;code&amp;gt;PrivateDevices&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Operasional harian ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo systemctl daemon-reload                     # wajib setelah edit file unit&lt;br /&gt;
sudo systemctl enable --now transcribe-api       # aktif saat boot + jalan sekarang&lt;br /&gt;
systemctl status transcribe-api transcribe-api-worker&lt;br /&gt;
journalctl -u transcribe-api -u transcribe-api-worker -f    # follow log&lt;br /&gt;
journalctl -u transcribe-api --since &amp;quot;1 hour ago&amp;quot; -p err    # hanya error&lt;br /&gt;
systemctl cat transcribe-api                      # lihat unit final yang dipakai&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Exit code systemd yang sering muncul ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Status !! Penyebab umum&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;status=203/EXEC&amp;lt;/code&amp;gt; || path di &amp;lt;code&amp;gt;ExecStart&amp;lt;/code&amp;gt; salah, file tidak executable, atau venv rusak (interpreter yang di-symlink tidak bisa diakses)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;status=217/USER&amp;lt;/code&amp;gt; || user di &amp;lt;code&amp;gt;User=&amp;lt;/code&amp;gt; tidak ada&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;status=200/CHDIR&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;WorkingDirectory&amp;lt;/code&amp;gt; tidak ada atau tidak bisa dimasuki user app&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;status=226/NAMESPACE&amp;lt;/code&amp;gt; || path di &amp;lt;code&amp;gt;ReadWritePaths&amp;lt;/code&amp;gt; belum dibuat&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Permission denied&amp;lt;/code&amp;gt; di log app || app menulis di luar &amp;lt;code&amp;gt;ReadWritePaths&amp;lt;/code&amp;gt; (cache model, file upload); arahkan ke &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Pola release + symlink ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
/opt/transcribe-api/&lt;br /&gt;
├── releases/&lt;br /&gt;
│   ├── 3f2a9c1d7e0b/&lt;br /&gt;
│   └── 9be41c0a22f1/&lt;br /&gt;
└── current -&amp;gt; releases/3f2a9c1d7e0b&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alur deploy:&lt;br /&gt;
# Clone versi baru ke &amp;lt;code&amp;gt;releases/&amp;amp;lt;sha&amp;amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Build (&amp;lt;code&amp;gt;uv sync --locked --no-dev&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;cargo build --release --locked&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;npm ci &amp;amp;&amp;amp; npm run build&amp;lt;/code&amp;gt;).&lt;br /&gt;
# Jalankan migrasi database kalau ada.&lt;br /&gt;
# Arahkan &amp;lt;code&amp;gt;current&amp;lt;/code&amp;gt; ke release baru, restart service.&lt;br /&gt;
# Health check. Gagal → arahkan kembali ke release lama.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Soal atomik:&amp;#039;&amp;#039;&amp;#039; &amp;lt;code&amp;gt;ln -sfn&amp;lt;/code&amp;gt; sebenarnya menghapus symlink lalu membuat ulang, sehingga ada jeda sangat singkat tanpa &amp;lt;code&amp;gt;current&amp;lt;/code&amp;gt;. Cara yang benar-benar atomik:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ln -s /opt/app/releases/NEW /opt/app/current.tmp&lt;br /&gt;
mv -T /opt/app/current.tmp /opt/app/current     # rename(2) atomik&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Venv tidak bisa dipindah.&amp;#039;&amp;#039;&amp;#039; Path absolut tertanam di script dalam &amp;lt;code&amp;gt;.venv/bin&amp;lt;/code&amp;gt;, jadi selalu build venv langsung di folder release, jangan build di tempat lain lalu di-copy.&lt;br /&gt;
&lt;br /&gt;
== Python dengan uv ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;uv sync --locked&amp;lt;/code&amp;gt; gagal kalau &amp;lt;code&amp;gt;uv.lock&amp;lt;/code&amp;gt; tidak sinkron dengan &amp;lt;code&amp;gt;pyproject.toml&amp;lt;/code&amp;gt;. Ini yang diinginkan di produksi: server tidak pernah mengubah lock file.&lt;br /&gt;
* &amp;lt;code&amp;gt;--no-dev&amp;lt;/code&amp;gt; melewati dependency development/test.&lt;br /&gt;
* &amp;lt;code&amp;gt;UV_PYTHON_DOWNLOADS=never&amp;lt;/code&amp;gt; mencegah uv diam-diam mengunduh Python saat build.&lt;br /&gt;
* Update dependency dilakukan di laptop: &amp;lt;code&amp;gt;uv lock --upgrade&amp;lt;/code&amp;gt;, commit, lalu deploy.&lt;br /&gt;
* Cache uv memakai hardlink ke venv selama berada di filesystem yang sama, sehingga deploy berikutnya cepat dan hemat disk.&lt;br /&gt;
&lt;br /&gt;
== Redis lokal ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y redis-server&lt;br /&gt;
sudo sed -i &amp;#039;s/^appendonly no/appendonly yes/&amp;#039; /etc/redis/redis.conf   # persistence&lt;br /&gt;
grep -E &amp;#039;^(bind|appendonly)&amp;#039; /etc/redis/redis.conf                     # bind 127.0.0.1 -::1&lt;br /&gt;
sudo systemctl enable --now redis-server&lt;br /&gt;
redis-cli ping&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Redis tanpa password &amp;#039;&amp;#039;&amp;#039;wajib&amp;#039;&amp;#039;&amp;#039; hanya listen di localhost. Di Docker, hostname-nya &amp;lt;code&amp;gt;redis&amp;lt;/code&amp;gt;; di bare metal jadi &amp;lt;code&amp;gt;redis://127.0.0.1:6379/0&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Jaringan ==&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;--host 127.0.0.1&amp;lt;/code&amp;gt;: app hanya bisa diakses dari server itu sendiri (cocok kalau nginx ada di server yang sama).&lt;br /&gt;
* &amp;lt;code&amp;gt;--host 0.0.0.0&amp;lt;/code&amp;gt;: app bisa diakses dari luar. Wajib dibarengi firewall.&lt;br /&gt;
* Kalau nginx publik ada di VM proxy terpisah, buka port app &amp;#039;&amp;#039;&amp;#039;hanya untuk IP VM proxy&amp;#039;&amp;#039;&amp;#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo ufw allow OpenSSH&lt;br /&gt;
sudo ufw allow from 192.168.88.10 to any port 6969 proto tcp comment &amp;#039;transcribe-api&amp;#039;&lt;br /&gt;
sudo ufw enable&lt;br /&gt;
sudo ufw status numbered&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ubuntu memakai &amp;#039;&amp;#039;&amp;#039;AppArmor&amp;#039;&amp;#039;&amp;#039;, bukan SELinux seperti AlmaLinux/RHEL. Untuk app di &amp;lt;code&amp;gt;/opt&amp;lt;/code&amp;gt; biasanya tidak perlu konfigurasi tambahan.&lt;br /&gt;
&lt;br /&gt;
== Otomasi dengan Ansible ==&lt;br /&gt;
&lt;br /&gt;
=== Kenapa Ansible ===&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Agentless&amp;#039;&amp;#039;&amp;#039;: cukup SSH + Python di server.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Idempotent&amp;#039;&amp;#039;&amp;#039;: dijalankan berkali-kali hasilnya sama; hanya yang berubah yang diubah.&lt;br /&gt;
* Semua langkah manual di atas terdokumentasi sebagai kode, sehingga server bisa dibangun ulang kapan saja.&lt;br /&gt;
&lt;br /&gt;
=== Struktur project ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ansible-apps/&lt;br /&gt;
├── inventory.ini            daftar server&lt;br /&gt;
├── apps.yml                 daftar app (repo, port, worker)&lt;br /&gt;
├── deploy.yml               playbook utama&lt;br /&gt;
├── group_vars/all/main.yml  setting global&lt;br /&gt;
├── group_vars/all/vault.yml secret terenkripsi&lt;br /&gt;
├── scripts/new-app.sh       tambah app interaktif&lt;br /&gt;
└── roles/&lt;br /&gt;
    ├── base/                paket dasar&lt;br /&gt;
    ├── redis/&lt;br /&gt;
    ├── python_toolchain/    uv + Python di /opt/uv/python&lt;br /&gt;
    ├── rust_toolchain/      rustup di /opt/rust&lt;br /&gt;
    ├── node_toolchain/      NodeSource&lt;br /&gt;
    └── app/                 clone, deteksi tipe, build, systemd/nginx, health check, rollback&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Entry app ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
apps:&lt;br /&gt;
  - name: transcribe-api&lt;br /&gt;
    repo: git@github.com:org/transcribe-api.git&lt;br /&gt;
    version: main&lt;br /&gt;
    port: 6969&lt;br /&gt;
    user: transcribe&lt;br /&gt;
    worker: true          # bikin service transcribe-api-worker&lt;br /&gt;
    redis: true&lt;br /&gt;
    apt_packages: [ffmpeg]&lt;br /&gt;
    extra_groups: [video]&lt;br /&gt;
    health_path: /health&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Tipe app dideteksi dari isi repo: &amp;lt;code&amp;gt;pyproject.toml&amp;lt;/code&amp;gt; → Python, &amp;lt;code&amp;gt;Cargo.toml&amp;lt;/code&amp;gt; → Rust, &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; → Node (Vite/React jadi static di-serve nginx, Next/Nuxt jadi service).&lt;br /&gt;
&lt;br /&gt;
=== Perintah ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ansible-playbook deploy.yml                                   # semua app&lt;br /&gt;
ansible-playbook deploy.yml -e only=transcribe-api            # satu app&lt;br /&gt;
ansible-playbook deploy.yml -e only=core-api -e rev=v1.2.0    # versi tertentu / rollback&lt;br /&gt;
ansible-playbook deploy.yml --syntax-check&lt;br /&gt;
ansible-vault encrypt group_vars/all/vault.yml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Konsep Ansible yang terkait permission ===&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;become: true&amp;lt;/code&amp;gt;: task dijalankan sebagai root lewat sudo.&lt;br /&gt;
* &amp;lt;code&amp;gt;become_user: transcribe&amp;lt;/code&amp;gt;: task dijalankan sebagai user app (clone, build), sehingga file yang dihasilkan langsung dimiliki user yang benar tanpa &amp;lt;code&amp;gt;chown&amp;lt;/code&amp;gt; belakangan.&lt;br /&gt;
* Paket &amp;lt;code&amp;gt;acl&amp;lt;/code&amp;gt; wajib terpasang. Saat &amp;lt;code&amp;gt;become_user&amp;lt;/code&amp;gt; ke user non-root, Ansible menulis file modul sementara lalu memakai ACL (&amp;lt;code&amp;gt;setfacl&amp;lt;/code&amp;gt;) agar user tujuan bisa membacanya.&lt;br /&gt;
* Modul &amp;lt;code&amp;gt;file&amp;lt;/code&amp;gt; dengan &amp;lt;code&amp;gt;owner&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;group&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;mode&amp;lt;/code&amp;gt; menggantikan rangkaian &amp;lt;code&amp;gt;mkdir&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;chown&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;chmod&amp;lt;/code&amp;gt; dan idempotent.&lt;br /&gt;
* Secret di &amp;#039;&amp;#039;&amp;#039;ansible-vault&amp;#039;&amp;#039;&amp;#039;, dirender ke env file lewat modul &amp;lt;code&amp;gt;template&amp;lt;/code&amp;gt; dengan mode &amp;lt;code&amp;gt;0640&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Deploy key untuk repo private ===&lt;br /&gt;
&lt;br /&gt;
* Satu key per app, disimpan di &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;/.ssh/id_ed25519&amp;lt;/code&amp;gt; (mode &amp;lt;code&amp;gt;0600&amp;lt;/code&amp;gt;, folder &amp;lt;code&amp;gt;0700&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Didaftarkan sebagai &amp;#039;&amp;#039;&amp;#039;Deploy Key read-only&amp;#039;&amp;#039;&amp;#039; di repo. Kalau server bocor, yang terekspos hanya akses baca ke satu repo, bukan akun GitHub pribadi.&lt;br /&gt;
* &amp;lt;code&amp;gt;StrictHostKeyChecking=accept-new&amp;lt;/code&amp;gt;: host key disimpan saat pertama kali, dan koneksi ditolak kalau host key berubah (proteksi MITM).&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Gejala !! Penyebab !! Solusi&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;chown: invalid group&amp;lt;/code&amp;gt; || group sudah di-rename/dihapus || &amp;lt;code&amp;gt;getent group&amp;lt;/code&amp;gt;, pakai nama baru&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;useradd&amp;lt;/code&amp;gt; menghasilkan shell aneh || sisa &amp;lt;code&amp;gt;\&amp;lt;/code&amp;gt; line-continuation ikut diketik dalam satu baris || cek &amp;lt;code&amp;gt;getent passwd&amp;lt;/code&amp;gt;, bikin ulang user&lt;br /&gt;
|-&lt;br /&gt;
| Service &amp;lt;code&amp;gt;203/EXEC&amp;lt;/code&amp;gt; padahal file ada || venv menunjuk ke Python di home user lain || install Python di &amp;lt;code&amp;gt;/opt/uv/python&amp;lt;/code&amp;gt;, buat ulang venv&lt;br /&gt;
|-&lt;br /&gt;
| Model Whisper gagal diunduh || cache di luar &amp;lt;code&amp;gt;ReadWritePaths&amp;lt;/code&amp;gt; || set &amp;lt;code&amp;gt;WHISPER_CACHE_DIR&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;HF_HOME&amp;lt;/code&amp;gt; ke &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;libcudnn&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;libcublas&amp;lt;/code&amp;gt; not found || di Docker library datang dari image || install paket pip &amp;lt;code&amp;gt;nvidia-cudnn-cu12&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;nvidia-cublas-cu12&amp;lt;/code&amp;gt; atau CUDA toolkit di host&lt;br /&gt;
|-&lt;br /&gt;
| Worker restart terus || error di job pertama atau Redis belum jalan || &amp;lt;code&amp;gt;journalctl -u app-worker -n 100&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redis-cli ping&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Ansible gagal &amp;lt;code&amp;gt;become_user&amp;lt;/code&amp;gt; || paket &amp;lt;code&amp;gt;acl&amp;lt;/code&amp;gt; belum terpasang || &amp;lt;code&amp;gt;apt install acl&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Checklist server baru ==&lt;br /&gt;
&lt;br /&gt;
* [ ] User admin pakai SSH key, login password dimatikan&lt;br /&gt;
* [ ] &amp;lt;code&amp;gt;ufw&amp;lt;/code&amp;gt; aktif, hanya SSH + port yang perlu&lt;br /&gt;
* [ ] Driver NVIDIA terpasang, &amp;lt;code&amp;gt;nvidia-smi&amp;lt;/code&amp;gt; jalan (kalau ada GPU)&lt;br /&gt;
* [ ] &amp;lt;code&amp;gt;unattended-upgrades&amp;lt;/code&amp;gt; aktif untuk patch keamanan&lt;br /&gt;
* [ ] Jalankan &amp;lt;code&amp;gt;ansible-playbook deploy.yml&amp;lt;/code&amp;gt;&lt;br /&gt;
* [ ] &amp;lt;code&amp;gt;systemd-analyze security &amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; untuk tiap service&lt;br /&gt;
* [ ] Backup data di &amp;lt;code&amp;gt;/var/lib/&amp;amp;lt;app&amp;amp;gt;&amp;lt;/code&amp;gt; dan database, &amp;#039;&amp;#039;&amp;#039;tes restore&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
== Lihat juga ==&lt;br /&gt;
* [https://docs.astral.sh/uv/ Dokumentasi uv]&lt;br /&gt;
* [https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html systemd.exec (directive hardening)]&lt;br /&gt;
* [https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html Filesystem Hierarchy Standard 3.0]&lt;br /&gt;
* [https://docs.ansible.com/ Dokumentasi Ansible]&lt;/div&gt;</summary>
		<author><name>SanzSkuy</name></author>
	</entry>
</feed>