SFTP and FTP

On this page

The sftp and ftp adapters upload the build to a folder on a server: a VPS, shared hosting, or anything with an SSH or FTP login. Prefer sftp; use ftp when the host offers nothing else.

import { defineConfig } from "@kilim-dev/core";
import { sftp } from "@kilim-dev/core/adapters";

export default defineConfig({
  url: "https://www.example.com",
  deploy: sftp({
    host: "example.com",
    user: "deploy",
    path: "/var/www/site",
    hostKey: "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s",
  }),
});
  • path is the site's folder on the server, the one the web server serves (often public_html or /var/www/<site>). An absolute path starts with /; anything else is relative to the folder the user signs in to. Missing folders are created.
  • port defaults to 22 for SFTP and 21 for FTP.
  • Every deploy writes _kilim/deploy.json last, the list of the site's files and their SHA-256 hashes, and the next kilim deploy uploads only new and changed files. When the folder has none (the first deploy, or files copied there another way), every file already on the server counts as changed and is uploaded again.
  • Old files are kept by default, because the folder often holds files that are not the site's: .htaccess, .well-known/, another app. kilim deploy says how many it keeps. Set deleteRemoved: true to delete the files of earlier deploys that the build no longer has, and the folders they leave empty. Without a manifest that is every file in path that the build does not have, so look at the list from kilim deploy --dry-run first.
  • Pages are uploaded after stylesheets and images, so a visitor never gets a page whose assets are missing.
  • _headers and _redirects are not uploaded; Apache and nginx do not read them. Redirects from aliases and redirects.txt still work through the redirect pages Kilim writes. For the security headers and CSP, set them in the server's config (or .htaccess on Apache).
  • Point the server's 404 page at 404.html: ErrorDocument 404 /404.html on Apache, error_page 404 /404.html; on nginx.

SFTP sign-in and the server's key

The user name is user in the config. Everything secret comes from environment variables, never the config:

VariableMeaning
SFTP_PRIVATE_KEYThe private key itself (OpenSSH or PEM), e.g. from a CI secret. A key pasted on one line with \n for the line breaks works too.
SFTP_PRIVATE_KEY_PATHA key file instead, e.g. ~/.ssh/id_ed25519. Set this or SFTP_PRIVATE_KEY, not both.
SFTP_PASSPHRASEThe key's passphrase, if it has one.
SFTP_PASSWORDA password, for servers without keys.
SSH_AUTH_SOCKA running ssh-agent; usually set already. On Windows, set it to \\.\pipe\openssh-ssh-agent to use the OpenSSH agent.

Kilim always checks the server's key, as ssh does, so nobody can stand in for the server. The key has to be in ~/.ssh/known_hosts (connect once with ssh user@host and accept it), or its fingerprint in hostKey; a list of fingerprints is fine too. In CI there is usually no known_hosts, so set hostKey. Get the fingerprint from the host, or on the server with ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub. When the server's key is not known, kilim deploy stops and shows the fingerprint it got; check it before you copy it into the config.

FTPS

import { ftp } from "@kilim-dev/core/adapters";

deploy: ftp({ host: "ftp.example.com", user: "me", path: "public_html" }),
  • The password comes from FTP_PASSWORD.
  • FTP is encrypted with TLS (FTPS) by default: secure: true asks the server for AUTH TLS on port 21, secure: "implicit" uses TLS from the start on port 990. The certificate must be valid for host, so use the host name the certificate is for (often the server's own name, not your domain). For a server with its own (self-signed) certificate, check the certificate with the host, save it as a PEM file and point NODE_EXTRA_CA_CERTS at it, which Node.js reads at start.
  • secure: false is plain FTP, which sends the password and the files unencrypted. Kilim warns about it in kilim deploy and kilim doctor; use it only when the host offers nothing else.
  • One file is sent at a time, as FTP allows one transfer per connection.

kilim doctor shows the address it connects to, which credentials it found, whether the server's key is in known_hosts or hostKey, and warns about plain FTP. It does not connect.

# .github/workflows/sftp.yml
name: SFTP
on:
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx kilim deploy
        env:
          SFTP_PRIVATE_KEY: ${{ secrets.SFTP_PRIVATE_KEY }}