Aku deploy website ini lewat SFTP. Manual. Buka FileZilla, connect ke server Rumahweb, drag file dari folder lokal ke public_html, tunggu upload selesai, cek di browser. Kalau ada yang salah, edit, upload lagi. Repeat.

Prosesnya sekitar 15 menit per deploy. Ga lama, tapi annoying. Terutama kalau kamu deploy 3-4 kali sehari karena lagi iterasi cepat. 15 menit dikali 4 itu sejam. Sejam cuma buat mindahin file.

Jadi aku bikin script Python pakai Paramiko. Sekarang deploy cuma satu perintah di terminal. Selesai dalam 30 detik. Ga perlu buka GUI. Ga perlu ingat folder mana yang udah berubah.

Ini bukan tutorial untuk developer senior. Ini catatan dari praktisi yang hosting-nya shared hosting Rumahweb, ga punya SSH full access, dan deployment tool-nya ya SFTP. Titik.

Key concept: Automasi deployment bukan soal teknologi canggih. Ini soal menghilangkan friction antara "selesai edit" dan "live di production." Semakin kecil friction-nya, semakin sering kamu deploy. Semakin sering deploy, semakin cepat iterasi. Semakin cepat iterasi, semakin bagus hasilnya.

Kenapa SFTP, bukan Git deploy atau CI/CD?

Pertanyaan yang wajar. Jawabannya sederhana: shared hosting.

Rumahweb, Niagahoster, Hostinger, dan mayoritas hosting Indonesia itu shared hosting. Kamu ga dapet SSH shell access (atau kalau dapet, sangat terbatas). Ga bisa install Git di server. Ga bisa jalankan webhook. Ga bisa pasang runner untuk CI/CD pipeline.

Yang kamu punya: cPanel, File Manager, dan SFTP access. Itu doang.

Kalo kamu punya VPS atau cloud server, ya silakan pakai Git hooks atau GitHub Actions. Bagus. Tapi itu bukan realita kebanyakan website Indonesia. Realitanya adalah shared hosting dengan cPanel, dan cara paling reliable buat transfer file ke sana adalah SFTP.

SFTP (SSH File Transfer Protocol) itu beda sama FTP biasa. FTP itu plain text, password kamu dikirim tanpa enkripsi. SFTP itu encrypted, lewat SSH tunnel. Jadi meskipun kamu di shared hosting, setidaknya transfer file-nya aman.

Rumahweb support SFTP. Kebanyakan hosting modern juga. Kalau hosting kamu cuma support FTP, serius, pindah.


Kenapa Python dan Paramiko?

Paramiko itu library Python yang implement SSHv2 protocol. Dia bisa bikin SSH connection, jalankan command remote (kalau servernya izinin), dan yang paling penting buat kita: transfer file lewat SFTP.

Kenapa ga pakai bash script aja? Atau PowerShell? Bisa. Tapi Python punya beberapa keunggulan:

  • Cross-platform. Script yang sama jalan di Windows, Mac, dan Linux. Aku pakai Windows (ThinkPad T450), tapi scriptnya ga perlu diubah kalau suatu hari pindah ke Mac.
  • Readable. Python itu pseudo-code yang kebetulan bisa di-execute. Orang yang ga ngerti programming pun bisa baca dan paham flow-nya.
  • Extensible. Mau nambahin logging? Gampang. Mau filter file tertentu? Tinggal tambahin kondisi. Mau notifikasi ke Telegram setelah deploy? Beberapa baris doang.
  • Paramiko itu battle-tested. Library ini udah ada sejak 2003. Dipakai di production oleh ribuan perusahaan. Bukan project random di GitHub dengan 12 stars.

Install-nya satu baris:

pip install paramiko

Itu aja. Ga ada dependency hell. Ga ada konfigurasi tambahan.


Script deploy lengkap

Ini script yang aku pakai. Bukan proof of concept. Ini yang beneran jalan di laptop aku setiap kali deploy ke hibranwar.com.

Script ini:

  1. Connect ke server via SFTP
  2. Scan folder lokal, cari file yang berubah sejak deploy terakhir
  3. Upload hanya file yang berubah (bukan seluruh folder)
  4. Bikin folder baru di server kalau belum ada
  5. Log semua aktivitas
  6. Tutup koneksi dengan bener
"""
deploy.py
SFTP deploy script untuk shared hosting.
Pakai: python deploy.py
"""

import os
import sys
import json
import time
import paramiko
from pathlib import Path
from datetime import datetime

# ============================================================
# KONFIGURASI - sesuaikan dengan hosting kamu
# ============================================================
CONFIG = {
    "host": "namaserver.rumahweb.com",
    "port": 22,
    "username": "username_cpanel_kamu",
    "password": None,  # None = pakai key file
    "key_file": os.path.expanduser("~/.ssh/id_rsa"),

    "local_dir": "./public_html",
    "remote_dir": "/home/username/public_html",

    "exclude": [
        ".git",
        ".DS_Store",
        "Thumbs.db",
        "__pycache__",
        "node_modules",
        ".env",
        "*.pyc",
    ],

    "state_file": ".deploy-state.json",
}

# ============================================================
# FUNGSI UTAMA
# ============================================================

def load_state(state_file):
    """Load timestamp deploy terakhir."""
    if os.path.exists(state_file):
        with open(state_file, "r") as f:
            return json.load(f)
    return {"last_deploy": 0, "files_deployed": []}


def save_state(state_file, state):
    """Simpan state setelah deploy."""
    with open(state_file, "w") as f:
        json.dump(state, f, indent=2)


def should_exclude(filepath, excludes):
    """Cek apakah file harus di-skip."""
    for pattern in excludes:
        if pattern.startswith("*"):
            if filepath.endswith(pattern[1:]):
                return True
        elif pattern in filepath:
            return True
    return False


def get_changed_files(local_dir, last_deploy, excludes):
    """Ambil daftar file yang berubah sejak deploy terakhir."""
    changed = []
    local_path = Path(local_dir)

    for filepath in local_path.rglob("*"):
        if filepath.is_dir():
            continue

        relative = str(filepath.relative_to(local_path))
        relative = relative.replace("\\", "/")

        if should_exclude(relative, excludes):
            continue

        mtime = filepath.stat().st_mtime
        if mtime > last_deploy:
            changed.append({
                "local": str(filepath),
                "relative": relative,
                "size": filepath.stat().st_size,
                "mtime": mtime,
            })

    return changed


def ensure_remote_dir(sftp, remote_path):
    """Bikin folder di server kalau belum ada. Rekursif."""
    dirs_to_create = []
    current = remote_path

    while current and current != "/":
        try:
            sftp.stat(current)
            break
        except FileNotFoundError:
            dirs_to_create.append(current)
            current = str(Path(current).parent).replace("\\", "/")

    for d in reversed(dirs_to_create):
        try:
            sftp.mkdir(d)
            print(f"  [DIR]  {d}")
        except IOError:
            pass  # mungkin race condition, folder udah ada


def deploy():
    """Fungsi utama deploy."""
    start_time = time.time()

    # Load state
    state = load_state(CONFIG["state_file"])
    last_deploy = state.get("last_deploy", 0)

    # Cari file yang berubah
    print(f"\nScanning {CONFIG['local_dir']}...")
    changed = get_changed_files(
        CONFIG["local_dir"],
        last_deploy,
        CONFIG["exclude"]
    )

    if not changed:
        print("Ga ada file yang berubah. Done.")
        return

    print(f"Found {len(changed)} file(s) to deploy:\n")
    total_size = 0
    for f in changed:
        size_kb = f["size"] / 1024
        total_size += f["size"]
        print(f"  {f['relative']} ({size_kb:.1f} KB)")

    print(f"\nTotal: {total_size / 1024:.1f} KB")
    print()

    # Connect via SFTP
    print(f"Connecting to {CONFIG['host']}...")
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    connect_args = {
        "hostname": CONFIG["host"],
        "port": CONFIG["port"],
        "username": CONFIG["username"],
    }

    if CONFIG["password"]:
        connect_args["password"] = CONFIG["password"]
    elif CONFIG["key_file"] and os.path.exists(CONFIG["key_file"]):
        connect_args["key_filename"] = CONFIG["key_file"]
    else:
        print("ERROR: Ga ada password atau key file.")
        sys.exit(1)

    try:
        ssh.connect(**connect_args)
    except paramiko.AuthenticationException:
        print("ERROR: Authentication gagal. Cek username/password.")
        sys.exit(1)
    except Exception as e:
        print(f"ERROR: Ga bisa connect. {e}")
        sys.exit(1)

    sftp = ssh.open_sftp()
    print("Connected.\n")

    # Upload file yang berubah
    deployed = []
    errors = []

    for f in changed:
        remote_path = CONFIG["remote_dir"] + "/" + f["relative"]
        remote_dir = str(Path(remote_path).parent).replace("\\", "/")

        try:
            ensure_remote_dir(sftp, remote_dir)
            sftp.put(f["local"], remote_path)
            deployed.append(f["relative"])
            print(f"  [OK]   {f['relative']}")
        except Exception as e:
            errors.append({"file": f["relative"], "error": str(e)})
            print(f"  [FAIL] {f['relative']}: {e}")

    # Tutup koneksi
    sftp.close()
    ssh.close()

    # Simpan state
    elapsed = time.time() - start_time
    state = {
        "last_deploy": time.time(),
        "files_deployed": deployed,
        "deploy_time": datetime.now().isoformat(),
        "duration_seconds": round(elapsed, 2),
    }
    save_state(CONFIG["state_file"], state)

    # Summary
    print(f"\n{'='*50}")
    print(f"Deploy selesai.")
    print(f"  Files: {len(deployed)} uploaded, {len(errors)} errors")
    print(f"  Time:  {elapsed:.1f} seconds")
    print(f"  State: saved to {CONFIG['state_file']}")

    if errors:
        print(f"\nErrors:")
        for e in errors:
            print(f"  - {e['file']}: {e['error']}")

    print()


if __name__ == "__main__":
    deploy()

Copy script ini, simpan sebagai deploy.py di root project kamu. Edit bagian CONFIG sesuai hosting. Jalankan:

python deploy.py

Deploy pertama akan upload semua file (karena belum ada state). Deploy kedua dan seterusnya cuma upload file yang berubah. Cepat.

Penjelasan bagian penting

State tracking. Script ini simpan timestamp deploy terakhir di file .deploy-state.json. Setiap kali jalan, dia bandingin modification time file lokal sama timestamp terakhir. Cuma file yang lebih baru yang di-upload. Ini alasannya deploy kedua jauh lebih cepat dari deploy pertama.

Exclude patterns. Folder .git, node_modules, file .env, dan file temporary lainnya otomatis di-skip. Kamu ga mau upload .env yang isinya password ke server. Serius.

Recursive directory creation. Kalau kamu bikin folder baru di lokal, script otomatis bikin folder yang sama di server. Ga perlu manual bikin folder lewat cPanel.

AutoAddPolicy. Baris ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) itu auto-accept host key baru. Untuk production server yang kamu kontrol sendiri, ini fine. Tapi kalau kamu paranoid (bagus), bisa ganti dengan RejectPolicy dan manage known_hosts manual.


Setup SSH key (opsional tapi recommended)

Script di atas support dua cara autentikasi: password dan SSH key. Password lebih gampang, tapi SSH key lebih aman dan ga perlu ketik password setiap deploy.

Generate key (kalau belum punya):

ssh-keygen -t rsa -b 4096 -C "email@kamu.com"

Tekan Enter untuk semua prompt (default location, no passphrase untuk convenience). Ini bikin dua file:

  • ~/.ssh/id_rsa (private key, JANGAN share)
  • ~/.ssh/id_rsa.pub (public key, ini yang dikasih ke server)

Di Rumahweb, buka cPanel, cari "SSH Access" atau "Manage SSH Keys". Import public key kamu. Authorize. Done.

Kalau hosting kamu ga support SSH key import via cPanel, kamu bisa tetap pakai password. Ganti "password": None jadi "password": "password_cpanel_kamu" di CONFIG. Tapi jangan commit password ke Git. Pakai environment variable:

import os
CONFIG = {
    "password": os.environ.get("DEPLOY_PASSWORD"),
    # ...
}

Terus set variable sebelum jalankan:

# Linux/Mac
export DEPLOY_PASSWORD="passwordku"
python deploy.py

# Windows (PowerShell)
$env:DEPLOY_PASSWORD="passwordku"
python deploy.py

Perbandingan metode deployment

Biar adil, ini perbandingan lengkap semua opsi yang realistis untuk website statis di Indonesia:

Metode Waktu Deploy Setup Awal Shared Hosting? Automasi Keamanan
Manual FTP (FileZilla) 10-15 menit 5 menit Ya Ga ada Plain text
Manual SFTP (FileZilla) 10-15 menit 5 menit Ya Ga ada Encrypted
Python + Paramiko 30 detik 30 menit Ya Full Encrypted
rsync over SSH 15 detik 15 menit Terbatas Full Encrypted
Git hooks (push to deploy) 10 detik 1-2 jam Ga (perlu SSH) Full Encrypted
GitHub Actions + SFTP 1-2 menit 1-2 jam Ya Full Encrypted
CI/CD (Netlify, Vercel) 30-60 detik 15 menit Ga (hosting sendiri) Full Encrypted

Perhatiin kolom "Shared Hosting?" dan "Setup Awal." Python + Paramiko itu sweet spot: automasi penuh, jalan di shared hosting, setup ga lama. Rsync lebih cepat, tapi butuh SSH shell access yang kebanyakan shared hosting ga kasih.

Netlify dan Vercel gratis dan gampang. Tapi kamu ga kontrol servernya. Untuk personal blog, fine. Untuk website bisnis yang butuh kontrol penuh, shared hosting dengan deploy script sendiri itu lebih solid.


Tips dari pengalaman

1. Selalu test di staging dulu

Bikin subfolder di server, misalnya public_html/staging/. Deploy ke sana dulu. Cek di browser. Baru deploy ke production. Di script, tinggal ganti remote_dir.

2. Jangan upload .env atau credentials

Tambahin .env, .deploy-state.json, dan file sensitif lainnya ke exclude list. Kalau kamu pakai Git, tambahin juga ke .gitignore. Dobel pengaman.

3. Log setiap deploy

Script di atas print ke terminal. Kamu bisa redirect ke file log:

python deploy.py >> deploy.log 2>&1

Atau tambahin Python logging module kalau mau lebih proper. Tapi buat ukuran personal site, redirect stdout udah cukup.

4. Hati-hati dengan file permissions

SFTP upload biasanya pakai permission default server. Di shared hosting, ini biasanya 644 untuk file dan 755 untuk folder. Udah bener. Tapi kalau kamu upload script PHP yang butuh permission khusus, cek manual setelah deploy pertama.

5. Connection timeout

Shared hosting kadang lambat response SSH. Tambahin timeout di connect:

ssh.connect(
    hostname=CONFIG["host"],
    port=CONFIG["port"],
    username=CONFIG["username"],
    password=CONFIG["password"],
    timeout=30,  # 30 detik timeout
    banner_timeout=30,
)

Ini mencegah script hang kalau server lagi sibuk.


Kapan script ini ga cukup?

Jujur: ada batasnya.

Kalau website kamu udah ratusan halaman dan deploy perlu build step (compile SASS, minify JS, optimize gambar), kamu butuh lebih dari script SFTP. Kamu butuh build pipeline. Entah itu Makefile, npm script, atau CI/CD beneran.

Tapi kalau website kamu kayak static site yang di-generate sekali terus deploy hasilnya, script Paramiko ini lebih dari cukup. Aku pakai ini buat hibranwar.com yang sekarang udah 50+ halaman, dan deploy time tetap di bawah 1 menit.

Yang bikin script ini berguna bukan kecanggihan kodenya. Tapi fakta bahwa setiap kali kamu selesai edit, kamu cuma perlu satu perintah. Ga perlu buka browser, login cPanel, navigasi folder. Satu perintah. Selesai.

Itu yang namanya mengurangi friction. Dan friction yang hilang itu compound. Hari ini kamu hemat 15 menit. Besok 15 menit lagi. Dalam setahun, kamu hemat puluhan jam. Jam yang bisa dipakai buat nulis konten, bukan drag-and-drop file di FileZilla.


Alternatif: GitHub Actions + SFTP

Kalau kamu udah pakai GitHub buat version control, ada opsi lain: GitHub Actions yang deploy otomatis setiap kali kamu push ke branch main.

# .github/workflows/deploy.yml
name: Deploy to shared hosting

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SFTP
        uses: wlixcc/SFTP-Deploy-Action@v1.2.4
        with:
          server: ${{ secrets.SFTP_HOST }}
          username: ${{ secrets.SFTP_USER }}
          password: ${{ secrets.SFTP_PASSWORD }}
          port: 22
          local_path: "./public_html/"
          remote_path: "/home/username/public_html/"
          sftp_only: true

Simpan credentials di GitHub Secrets (Settings > Secrets > Actions). Setiap push ke main, website otomatis ke-deploy.

Kelebihannya: ga perlu jalankan script manual. Push aja, selesai.

Kekurangannya: tergantung GitHub infra (kalau GitHub down, deploy kamu juga down), dan ada sedikit delay (pipeline butuh 1-2 menit). Untuk personal site, delay ini ga masalah. Tapi kalau kamu lagi hotfix urgent, script lokal lebih cepat.

Aku sendiri pakai dua-duanya. Script Paramiko buat deploy cepat dari laptop. GitHub Actions sebagai backup kalau aku deploy dari device lain.


Workflow aku sehari-hari

Biar konkret, ini workflow deploy aku untuk hibranwar.com:

  1. Edit file PHP di VS Code. Save.
  2. Buka terminal (udah terbuka di VS Code, shortcut Ctrl+`).
  3. Ketik python deploy.py. Enter.
  4. Tunggu 20-30 detik. Script nge-scan, upload file yang berubah, print summary.
  5. Buka browser. Refresh. Cek hasilnya.
  6. Kalau oke, lanjut kerja. Kalau ada yang salah, edit, deploy lagi.

Total waktu dari "selesai edit" sampai "live di production": kurang dari 1 menit. Bandingkan sama workflow lama yang 15 menit per deploy.

Ini bukan improvement kecil. Ini ngubah cara kerja. Kalau deploy cepat, kamu ga takut deploy. Kalau ga takut deploy, kamu lebih berani eksperimen. Kalau lebih berani eksperimen, website kamu berkembang lebih cepat.

Ya kan?


Frequently Asked Questions

Apakah Paramiko aman untuk deploy ke production?

Ya. Paramiko implement SSH v2 protocol, yang berarti semua transfer data terenkripsi. Ini jauh lebih aman daripada FTP biasa yang kirim password dalam plain text. Paramiko udah dipakai sejak 2003 dan jadi standar de facto untuk SSH di Python. Library ini di-maintain aktif dan punya track record security yang solid.

Hosting aku ga support SSH key. Bisa pakai password aja?

Bisa. Di CONFIG, ganti "password": None jadi password cPanel kamu. Tapi jangan hardcode password di file. Pakai environment variable (os.environ.get("DEPLOY_PASSWORD")) supaya password ga tersimpan di source code. Dan jangan pernah commit file yang berisi password ke Git.

Apa bedanya SFTP sama FTP?

FTP (File Transfer Protocol) transfer data tanpa enkripsi. Password dan file kamu dikirim as-is lewat jaringan. Siapa pun yang bisa sniff traffic bisa baca semuanya. SFTP (SSH File Transfer Protocol) jalankan semua transfer lewat encrypted SSH tunnel. Beda fundamental. Kalau hosting kamu cuma support FTP tanpa SFTP, pertimbangkan pindah hosting. Di 2026, ga ada alasan pakai FTP plain.

Script ini bisa deploy folder tertentu aja?

Bisa. Ganti local_dir di CONFIG ke subfolder yang mau di-deploy. Misalnya kalau kamu cuma mau deploy folder CSS: "local_dir": "./public_html/assets/css" dan "remote_dir": "/home/username/public_html/assets/css". Atau bikin beberapa CONFIG dan pilih lewat argument: python deploy.py --target css.

Kalau deploy gagal di tengah jalan, file yang udah ke-upload gimana?

File yang udah ke-upload tetap di server. Script ini ga atomic (ga pakai temporary folder + rename). Untuk personal site dan kebanyakan website bisnis, ini ga masalah karena file-file yang udah berhasil upload itu valid. Kalau kamu butuh atomic deploy (semua atau ga sama sekali), kamu perlu strategi yang lebih rumit: upload ke folder temp, lalu rename. Tapi itu overkill untuk kebanyakan kasus.

References

  1. Paramiko contributors. "Paramiko: A Python implementation of SSHv2." Paramiko Documentation, 2024. Link
  2. Nezhar. "Auto deploy static generated websites on shared hosting." nezhar.com, 2023. Link
  3. SFTPCloud. "Paramiko SFTP: A Guide with Examples." SFTPCloud Learn, 2024. Link
  4. wlixcc. "SFTP-Deploy-Action: GitHub Action for deploying via SFTP." GitHub Marketplace, 2024. Link
  5. Linode. "How to Use Paramiko and Python to SSH into a Server." Linode Docs, 2024. Link

Related notes

2026-03-25

A site survey teaches you more than a spec sheet.