Episode ini membahas deployment otomatis ke server Linux mulai dari menyiapkan private key SSH di secrets, mengeksekusi perintah remote dengan ssh-action, sinkronisasi file dengan rsync, hingga menjalankan playbook Ansible langsung dari pipeline.

Di episode 12 kita berhasil membangun dan mengirim image Docker ke registry. Tetapi image yang menganggur di registry tidak berarti apa-apa — hasil masakan yang bagus harus sampai ke meja. Episode 13 ini mengantarkannya: Continuous Deployment (CD) — deploy otomatis dari pipeline ke server produksi.
Fokusnya server Linux/VPS klasik, yang sampai hari ini masih menjadi tulang punggung banyak aplikasi. Kita mulai dari menyiapkan private key SSH, eksekusi perintah remote dengan appleboy/ssh-action, sinkronisasi file dengan Rsync/SCP, hingga manajemen konfigurasi yang idempoten dengan Ansible.
Deploy via SSH butuh kredensial: sepasang kunci ed25519. Kunci publik diletakkan di file authorized_keys di server, sedangkan kunci privat disimpan sebagai secret repository — tidak pernah di-commit.
ssh-keygen -t ed25519 -C "github-actions-deploy@devvnull"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@server.example.comssh-copy-id menyalin kunci publik ke server dan menguji loginnya. Kemudian isi ~/.ssh/id_ed25519 disimpan ke Settings > Secrets sebagai SERVER_SSH_KEY.
Warning
Kunci privat adalah master key ke server produksi — jangan pernah menaruhnya di repository, di Dockerfile, atau di log. Batasi risikonya: buat akun deploy khusus dengan izin seminimal mungkin, tambahkan batasan di authorized_keys (misalnya from= untuk membatasi IP sumber), dan batasi port SSH dengan firewall.
Cara paling langsung: workflow mengeksekusi perintah di server melalui SSH. Action appleboy/ssh-action membungkus koneksi SSH dan menjalankan skrip remote:
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: 22
script: |
cd /srv/app
git pull origin main
npm ci --omit=dev
systemctl restart appSkrip di script dijalankan di server: menarik kode terbaru, menginstal dependensi produksi, lalu me-restart service systemd. Karena skrip ini ditulis statis di workflow (bukan hasil interpolasi data user), ia aman dari script injection yang kita bahas di episode 9. Nilai-nilai dari secrets ditangani action ini langsung, tidak pernah muncul di log.
Terkadang yang perlu dikirim bukan perintah, melainkan file — misalnya hasil build frontend dari job sebelumnya. Pilihan pertama: appleboy/scp-action, yang mengunggah file lewat koneksi yang sama:
- uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: "dist/*"
target: "/srv/app/dist"
strip_components: 1source dan target menentukan file yang dikirim dan tujuan di server; strip_components menghilangkan direktori induk agar isi dist/ langsung jatuh ke dist/ server.
Untuk proyek dengan banyak file, rsync lebih efisien karena hanya mentransfer perbedaan. Ia bisa dipanggil langsung dari step bash memakai OpenSSH:
rsync -az --delete \
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
dist/ deploy@server.example.com:/srv/app/dist/-a mempertahankan atribut file, -z mengompresi transfer, dan --delete menghapus file di server yang tidak ada lagi di sumber — sehingga server selalu menjadi cermin persis hasil build. Kunci privat perlu disiapkan dulu di runner, misalnya dari secret.
Untuk satu server, skrip SSH sederhana cukup. Tetapi begitu aplikasi berjalan di beberapa server dengan konfigurasi yang harus konsisten, skrip one-off mulai rapuh. Di sinilah Ansible berperan: tool manajemen konfigurasi yang idempoten — menjalankan playbook berulang kali menghasilkan kondisi yang sama, tanpa efek samping ganda.
Ansible berjalan dari mesin kontrol (runner), terhubung ke server via SSH, dan tidak butuh agent terpasang di server. Dari workflow, kita siapkan Python dan Ansible lalu jalankan playbook:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ansible
- name: Siapkan kunci SSH
env:
SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
- name: Jalankan playbook
run: ansible-playbook -i inventory/prod.yml playbooks/deploy.yml
env:
ANSIBLE_HOST_KEY_CHECKING: "False"Playbook dan inventory disimpan di repository — konfigurasi server pun terversi seperti kode. Contoh playbook sederhana yang memperbarui kode, memasang dependensi, dan me-restart service:
- name: Deploy aplikasi
hosts: web
become: true
tasks:
- name: Perbarui kode dari git
ansible.builtin.git:
repo: https://github.com/devvnull/app.git
dest: /srv/app
version: main
- name: Pasang dependensi
ansible.builtin.command: npm ci --omit=dev
args:
chdir: /srv/app
- name: Restart service
ansible.builtin.systemd:
name: app
state: restartedKeunggulan idempotensi terasa saat deploy gagal di tengah jalan: kalian cukup me-restart workflow, dan Ansible hanya menyelesaikan bagian yang belum selesai — tidak melakukan ulang semuanya secara membabi buta.
| Kesalahan | Gejala | Solusi |
|---|---|---|
| Private key di-commit ke repository | Akses server bisa direbut siapa pun | Putar kunci, simpan hanya sebagai secret |
| Host key verification error | Koneksi SSH ditolak | Setel ANSIBLE_HOST_KEY_CHECKING untuk Ansible |
Path source SCP salah | File tidak terunggah atau tercecer | Periksa path relatif dan strip_components |
| Lupa menginstal Ansible | Perintah ansible tidak ditemukan | Tambahkan setup-python + pip install ansible |
| Deploy ke server tanpa rollback | Error langsung melanda produksi | Simpan versi rilis dan siapkan rollback |
Deployment ke server kini menjadi bagian dari pipeline, bukan ritual manual:
setup-python + pip install ansible.Di episode 14 berikutnya, kita naik kelas: Continuous Deployment ke Cloud & Kubernetes — deploy ke Cloud Run, Vercel, dan AWS Lambda, lalu ke cluster Kubernetes dengan kubectl dan Helm. Dari mengelola satu server, menjadi mengelola seluruh platform!