Cutting Deployment Time by 80% with GitHub Actions and Docker

English Version: For English version of this article, please visit: dev.to
Photo by Bruno Kelzer on Unsplash
Beberapa waktu lalu saya sedang mengerjakan project untuk seorang client. Sehari-hari saya adalah backend engineer, tapi di project ini ternyata scope-nya meluas. Saya harus masuk ke ranah server juga. Client ingin memigrasikan aplikasi Laravel mereka dari AWS Elastic Beanstalk ke AWS Lightsail, dengan alasan ingin menekan biaya infrastruktur bulanan dan punya kontrol lebih penuh atas server.
Sebagai orang yang suka belajar, ini justru momen yang menarik. Selama ini saya nyaman di zona backend, sekarang kesempatan buat pegang hal-hal infrastruktur dan DevOps secara langsung.
Beanstalk sebelumnya sangat nyaman, semua proses deployment otomatis. Push kode, selesai. Tapi begitu pindah ke Lightsail, semuanya berubah. Tiba-tiba saya harus setup server dari nol, konfigurasi Docker, dan yang paling menyebalkan: proses deployment yang serba manual.
Masalah: Deployment Manual yang Makan Waktu
Di setup awal Lightsail, setiap kali saya ingin deploy perubahan ke server, inilah ritual yang harus saya jalani:
- Push commit ke GitHub
- Buat tag release (
git tag -a v1.2.3 -m "release note" && git push origin v1.2.3) - SSH ke server Lightsail
git fetch --tagsdangit checkouttag yang baru- Jalankan
docker compose up -d --build - Kalau ada perubahan database, masuk ke container dengan
docker execlalu jalankanphp artisan migrate --forcesecara manual
Semua langkah di atas memakan waktu sekitar 2-3 menit. Belum lagi kalau lagi buru-buru karena ada bug kritis di server yang butuh cepat di beresin, setiap detiknya terasa lambat.
Nah, yang paling mengganggu sebenarnya bukan durasinya, tapi banyaknya langkah manual yang rawan human error. Pernah suatu kali saya lupa menjalankan migrasi setelah deploy, dan langsung mendapatkan error karena kolom baru di database belum ada. Cukup memalukan akibat terburu-buru dan step yang terlalu banyak.
Jadi muncul pertanyaan: kenapa tidak saya otomatisasi saja semua ini? Sewaktu masih bekerja di startup dulu, tim infra sudah pernah menyiapkan pipeline seperti ini. Gimana kalau saya coba cari tahu dan bangun sendiri?
Solusi: GitHub Actions Workflow
Karena kode sudah ada di GitHub dan server sudah pakai Docker, jawabannya cukup jelas: GitHub Actions. Dengan workflow_dispatch, kita bisa membuat workflow yang dipicu secara manual dari tab Actions di GitHub — lengkap dengan input parameter.
Mungkin kamu bertanya, kenapa tidak otomatis saja ketika ada commit atau merge ke branch main? Bukannya itu lebih praktis?
Nah, di sinilah pertimbangannya. Dengan trigger manual, ada beberapa keuntungan:
- Tidak ada perubahan mendadak di production. Setiap deployment harus disengaja, bukan efek samping dari merge
- Semua orang di tim jadi aware karena sebelum deploy, orang yang bersangkutan harus masuk ke tab Actions dan menjalankannya secara sadar
- Versioning wajib — dengan input tag, kita diharuskan membuat versi untuk setiap deployment. Kalau ke depannya terjadi error atau hal yang tidak diinginkan, kita bisa langsung rollback ke tag sebelumnya
Oke, jadi konsepnya sederhana:
- Kita tetap push commit dan buat tag seperti biasa
- Buka tab Actions di repository GitHub
- Pilih workflow Deploy Production
- Masukkan tag yang ingin di-deploy
- Centang checkbox jika ingin menjalankan migrasi
- Klik Run workflow, dan… tinggal tunggu
Tidak perlu SSH, tidak perlu docker exec, tidak perlu takut lupa migrasi. Deployment yang tadinya makan 2-3 menit sekarang selesai dalam waktu rata-rata 40 detik (itupun mostly nunggu build Docker).

Mari kita lihat bagaimana workflow ini dibangun.
Membangun Workflow
Workflow kita simpan di .github/workflows/deploy.yml. Berikut struktur dasarnya:
name: Deploy Production
on:
workflow_dispatch:
inputs:
tag:
description: "Git tag to deploy (example: v1.2.3)"
required: true
type: string
run_migration:
description: "Run database migration?"
required: false
type: boolean
default: false
Dengan konfigurasi workflow_dispatch di atas, GitHub akan menampilkan form sederhana setiap kali kita ingin menjalankan workflow. Ada dua input:
- tag, yaitu tag Git yang ingin di-deploy, wajib diisi
- run_migration, yaitu checkbox untuk menjalankan migrasi database, opsional
Cukup jelas ya. Sekarang kita masuk ke bagian jobs.
SSH ke Server
Langkah pertama adalah menyiapkan koneksi SSH ke server Lightsail menggunakan SSH key yang sudah kita simpan sebagai GitHub Secret:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
Di sini kita menyimpan private key ke file id_ed25519 dengan permission 600 (hanya bisa dibaca oleh owner). Lalu kita tambahkan host key server ke known_hosts agar koneksi SSH tidak ditolak.
Perlu diingat, semua informasi sensitif seperti SSH_PRIVATE_KEY, SSH_HOST, SSH_USER, dan APP_PATH disimpan sebagai GitHub Secrets, jangan pernah di-hardcode di workflow file.
Checkout Tag dan Deploy
Setelah koneksi SSH siap, kita kirim perintah ke server untuk fetch tag, checkout, dan build Docker:
- name: Deploy to Server
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "export GITHUB_PAT='${{ secrets.CREDENTIAL_GITHUB_PAT }}' && bash -s" << 'EOF'
set -e
set +o history
cd ${{ secrets.APP_PATH }}
echo "Temporarily set Git remote with token"
ORIGINAL_URL=$(git remote get-url origin)
REPO_PATH=${ORIGINAL_URL#https://github.com/}
git config --local credential.helper '!f() { echo "username=x-access-token"; echo "password=${GITHUB_PAT}"; }; f'
echo "Fetch tags"
git fetch --tags
echo "Checkout tag: ${{ github.event.inputs.tag }}"
git checkout -f ${{ github.event.inputs.tag }}
echo "Remove credential helper"
git config --local --unset credential.helper
echo "Deploy with Docker Compose"
export USER_ID=$(id -u)
export GROUP_ID=$(id -g)
docker compose up -d --build
set -o history
echo "Deployment completed successfully!"
EOF
Ada beberapa hal menarik di sini. Mari kita bahas satu per satu.
Pertama, kita menggunakan heredoc (<< 'EOF') untuk mengirim blok perintah shell ke server melalui SSH. Ini lebih rapi dibanding menulis semua perintah inline dalam satu baris panjang.
Kedua, kita menggunakan Git credential helper alih-alih menyimpan token di remote URL. Ini lebih aman karena setelah git checkout selesai, credential helper langsung kita hapus. Token hanya hidup selama eksekusi perintah tersebut.
Ketiga, perhatikan set +o history di awal. Ini mematikan bash history sementara agar token GitHub tidak tercatat di history shell server. Keamanan kecil yang sering terlewat.
Keempat, export USER_ID dan GROUP_ID diperlukan agar container Docker berjalan dengan user ID yang sama dengan host, sehingga permission file tetap konsisten.
Menjalankan Migrasi
Bagian terakhir adalah opsi untuk menjalankan migrasi database:
# Run migration if requested
if [ "${{ github.event.inputs.run_migration }}" == "true" ]; then
echo "Running database migration..."
docker exec container_app php artisan migrate --force
echo "Migration completed!"
else
echo "Skipping migration"
fi
Cukup straightforward. Cek apakah input run_migration bernilai true. Jika ya, jalankan php artisan migrate --force di dalam container container_app. Flag --force diperlukan karena artisan biasanya meminta konfirmasi di environment production.
Bagian ini yang dulu paling menyebalkan. Sering kali setelah docker compose up -d selesai, saya harus buru-buru docker exec untuk menjalankan migrasi sebelum ada request masuk yang menyentuh tabel baru. Sekarang tinggal centang kotak, beres.
Hasil Akhir
Yay 🎉, workflow sudah siap. Sekarang setiap kali ingin deploy, prosesnya sesederhana:
- Push commit dan buat tag (
git tag v1.2.3 && git push origin v1.2.3) - Buka repository di GitHub, tab Actions → pilih Deploy Production → klik Run workflow
- Isi tag yang ingin di-deploy
- Centang
Run database migration?jika ada perubahan database - Klik Run workflow dan tunggu notifikasi sukses
Berikut perbandingan sebelum dan sesudah:
| Langkah | Sebelum (Manual) | Sesudah (GitHub Actions) |
|---|---|---|
| Checkout kode di server | SSH → git fetch → git checkout | Otomatis via workflow |
| Build & deploy | docker compose up -d --build | Otomatis via workflow |
| Migrasi database | SSH → docker exec → php artisan migrate | Centang checkbox |
| Total waktu | 2–3 menit | ~40 detik (input parameter) |
| Risiko human error | Tinggi (lupa langkah) | Rendah (terstandarisasi) |
Dari 2-3 menit ke ~40 detik, itu artinya waktu deployment berkurang sekitar 80%. Tapi yang lebih penting dari pengurangan waktu adalah: deployment sekarang konsisten dan bebas dari human error.
Kesimpulan
Migrasi dari Beanstalk ke Lightsail memang memaksa saya keluar dari zona nyaman backend engineer dan menyentuh hal-hal infrastruktur yang sebelumnya sudah ditangani otomatis — termasuk deployment. Tapi dari situlah kita belajar dan berkembang.
Dengan GitHub Actions workflow_dispatch, kita bisa membangun CI/CD pipeline sederhana yang tidak kalah nyaman dengan managed service seperti Beanstalk. Modal utamanya cuma:
- Repository di GitHub
- Server yang bisa diakses via SSH
- Aplikasi yang sudah di-containerize dengan Docker
- Satu file workflow YAML
Oke, mungkin pipeline ini belum secanggih Jenkins atau GitLab CI dengan fitur rollback otomatis dan approval gate. Tapi untuk tim kecil atau proyek personal, ini sudah lebih dari cukup.
Jika Anda memiliki tambahan atau koreksi terhadap pembahasan di atas, mari kita diskusikan di kolom komentar. Semoga membantu 👋.