ESC
Type to search...
S
Soli Docs

Deployment

Deploy your Soli applications to remote servers with a single command. Blue-green deployments via soli-proxy, or bundle your app into a single binary for zero-dependency deployment.

Parallel Deployment

Deploy to multiple servers simultaneously with automatic git pull and blue-green switching.

Quick Start

Create a deploy.toml file in your project root:

git_url = "https://github.com/your-org/your-project.git"
git_branch = "main"
git_folder = "www/"

[[servers]]
name = "prod-1"
username = "deploy"
ip = "192.168.1.100"
folder = "/var/www/myapp"
api_key = "your-api-key"
proxy_url = "https://proxy.example.com"

[[servers]]
name = "prod-2"
username = "deploy"
ip = "192.168.1.101"
folder = "/var/www/myapp"
api_key = "your-api-key"
proxy_url = "https://proxy.example.com"

Then run:

soli deploy

Configuration

Global Settings

  • git_url — Git repository URL (required)
  • git_branch — Branch to deploy (default: main)
  • git_folder — Subfolder to deploy, e.g. www/ (default: /)

Server Settings

  • name — Server identifier for logging
  • username — SSH username
  • ip — Server IP address
  • folder — Deployment path on server
  • api_key — soli-proxy API key
  • proxy_url — soli-proxy base URL

Deployment Flow

Deployment happens in two phases:

Phase 1: Sync Code (parallel)

All servers sync code simultaneously via SSH:

  • SSH connect to username@ip
  • Git clone (first deploy) or git pull (subsequent)

Phase 2: Migrations (first server only)

Database migrations run only on the first server to avoid conflicts:

  • SSH to first server
  • Run soli db:migrate up

Phase 3: Deploy (parallel)

All servers trigger blue-green deployment simultaneously:

  • POST to soli-proxy deploy API
  • Automatic health checks and traffic switch

Asset cache during deploys

In production mode the server snapshots every .css and .js file under public/ into memory at boot. If new asset bytes land on disk before the binary restarts, the running process keeps serving its frozen snapshot — preventing the classic mismatch where in-flight HTML references one asset version while the disk has another. The next binary start reloads from disk. See Production Mode for details.

Requirements

  • SSH key-based authentication (keys must be added to ssh-agent)
  • The soli binary must be in your PATH
  • soli-proxy running on each target server
  • Valid API keys configured in soli-proxy

Usage

# Deploy from current directory
soli deploy

# Deploy from specific folder
soli deploy --folder /path/to/project

# Short form
soli deploy -f /path/to/project

Output Example

Deploying from /path/to/project...
Phase 1: Syncing code to all servers...

[prod-1] Connecting to deploy@192.168.1.100...
[prod-1] Folder exists, pulling latest changes...
[prod-1] Code synced ✓

[prod-2] Connecting to deploy@192.168.1.101...
[prod-2] Folder exists, pulling latest changes...
[prod-2] Code synced ✓

[prod-1] Running database migrations...
[prod-1] soli db:migrate up
[prod-1] Migrations completed ✓

Phase 2: Triggering blue-green deploy on all servers...

[prod-1] Triggering blue-green deploy...
[prod-1] Deploy started on slot green ✓

[prod-2] Triggering blue-green deploy...
[prod-2] Deploy started on slot blue ✓

✓ 2/2 servers deployed successfully

Bundle Deployment

For simpler, dependency-free deployment, bundle your application into a single .soli file. No source files needed on the server — only the soli binary and the bundle.

# Build the bundle
soli build my_app

# Deploy just the .soli file
scp my_app.soli deploy@server:/opt/my_app/

# Run from the bundle
ssh deploy@server "soli serve /opt/my_app/my_app.soli"

Bundle mode in deploy.toml

The soli deploy command supports mode = "bundle" to automatically SCP the bundle to all servers:

mode = "bundle"
bundle_source = "./my_app.soli"

[[servers]]
name = "prod-1"
username = "deploy"
ip = "192.168.1.100"
folder = "/opt/myapp"
proxy_url = "https://proxy.example.com"

How it works

soli build collects all .sl, .slv, .yml, .css, .js files into a single .soli bundle. When you run soli serve app.soli, the bundle is extracted to /tmp/soli_PID and the server boots normally. On the proxy side, set start_script = "soli serve /opt/myapp/my_app.soli --port $PORT --workers $WORKERS" in the app's app.infos. Migrations should be run locally before bundling — auto-migration is not supported in bundle mode.

Secrets stay out of the bundle. Dotfiles are never bundled, so ship your .env separately and drop it next to the .soli filesoli serve app.soli loads .env (and .env.{APP_ENV}) from the bundle's directory before boot. Variables already set in the process environment take precedence. Note that binary assets (images, fonts) in public/ are not bundled either — deploy those alongside or serve them from a CDN.

Encrypted & Protected Bundles

When you deploy to servers you don't fully control, encrypt the bundle so the source can't be copied off disk, and fetch the decryption key from a key server you own — revoking it there is a remote kill-switch.

FlagWhat it does
--encryptWraps the whole bundle in AES-256-GCM. The .soli file on disk is ciphertext; the key is needed to boot.
--protectImplies --encrypt, and additionally replaces every .sl source with its compiled binary AST — so even after decryption there is no readable source (comments and formatting are gone; identifiers and string literals remain, as in any bytecode).
# The key is read from the environment — NOT passed as an argument.
export SOLI_BUNDLE_KEY="a-long-random-secret"     # or point at your key server (below)

soli build my_app --protect
#   Building bundle from my_app...
#   Encrypting bundle (key from SOLI_BUNDLE_KEY)
#   ✓ Bundle written to my_app.soli (1.9 KB) (protected: binary AST, encrypted)

# Then serve it — the key is resolved again the same way:
export SOLI_BUNDLE_KEY="a-long-random-secret"     # (or a .env beside the bundle, below)
soli serve my_app.soli --port 8080

Command form. --encrypt and --protect are on/off flags that take no value, and --protect already implies --encrypt (you never pass both). The folder and the flags may appear in any order (soli build my_app --protect or soli build --protect my_app). There is no --key argument — a key on the command line would leak into shell history and the process list, so it is always taken from the environment. soli build --protect --encrypt abcd my_app does not work; use SOLI_BUNDLE_KEY=abcd soli build --protect my_app.

Where the key comes from

At both build and serve time the key is resolved in this order:

  1. SOLI_BUNDLE_KEY — the key material itself (handy for local testing).
  2. SOLI_BUNDLE_AUTH_URL — a URL your server exposes. Soli issues a GET, sending SOLI_BUNDLE_API_KEY (if set) as an x-api-key header; the response body is the key material (any encoding — it's hashed to a 256-bit key). This is the revocable path.

These can live in the .env next to the .soli file (loaded before decryption), so the deployed server only carries its API key, never the decryption key:

SOLI_BUNDLE_AUTH_URL=https://keys.example.com/my_app
SOLI_BUNDLE_API_KEY=srv-7f3c...           # this host's identity; revoke it to lock the app out

Delete or disable that entry on your key server and the next boot fails with a clear error — a decommissioned or stolen host stops working. A wrong or rotated key fails the same way.

What this protects — and what it doesn't

Encryption protects your source against casual copying: a hosting provider's backup, a leaked .soli, or a co-tenant browsing the disk sees only ciphertext, and decrypted files live in a private RAM-backed directory (/dev/shm, mode 0700), removed on shutdown — never on persistent disk. --protect raises the cost of reconstructing the source (like shipping .pyc/.class instead of source). Because the key is fetched at boot, revoking it is a kill-switch. It does not protect against an attacker with root on the running server — they can read the key from the process environment or the decrypted files from RAM. If your threat model includes a hostile host, don't deploy the source there at all.

Operational notes

  • Decrypted bundles extract to /dev/shm. On a system without it (e.g. macOS) the boot is refused rather than silently writing plaintext to disk; set SOLI_BUNDLE_ALLOW_DISK=1 to override (temp dir, still 0700).
  • A --protect bundle is locked to the exact Soli version that built it — the binary AST has no cross-version format guarantee. Serving it with a different soli fails with a "rebuild the bundle" error.
  • --protect does not yet support apps with an engines/ directory.

Standalone Executables

--standalone goes one step further than a .soli bundle: it embeds the entire soli runtime into the artifact, producing a single native executable that boots your app directly — the target machine needs no soli install at all. It composes with --encrypt and --protect; key resolution and the key-server kill-switch work exactly as above.

# Build a self-contained executable (encrypted, no readable source)
soli build my_app --standalone --protect
#   ✓ Standalone executable written to my_app (41.2 MB, 1.9 KB app bundle, protected: binary AST, encrypted)
#     Platform: linux-x86_64 — run it with: ./my_app --port 8080

# Ship ONE file; run it with no soli install on the server
scp my_app deploy@server:/opt/my_app/
ssh deploy@server "/opt/my_app/my_app --port 8080 --workers 4"

Cross-platform builds

--target selects which platform's runtime to embed — build on your workstation, deploy anywhere. The matching official release runtime (same version as your soli) is downloaded, sha256-verified against the published checksum, cached under ~/.cache/soli/runtimes/, and embedded:

soli build my_app --standalone --protect --target linux-arm64
#   ✓ Standalone executable written to my_app-linux-arm64 ...

# Supported targets (the published release artifacts):
#   linux-amd64   linux-arm64   darwin-arm64

Air-gapped or mirrored environments can point SOLI_RELEASE_BASE_URL at their own artifact server (same layout: {base}/v{version}/soli-{target}.tar.gz + .sha256). Without --target the running soli binary itself is embedded — no network needed.

Running a standalone app

The executable accepts app-oriented flags — --port, --host, --workers, --dev, --version, --help — and reads its .env from the directory containing the executable, the same convention as a .soli bundle. For encrypted apps, provide SOLI_BUNDLE_KEY or SOLI_BUNDLE_AUTH_URL there.

Operational notes

  • The runtime adds a ~40 MB baseline to the artifact regardless of app size.
  • The protected-bundle version lock is satisfied by construction — runtime and bundle ship as a matched pair.
  • Encrypted standalones inherit the RAM-only extraction contract: /dev/shm or SOLI_BUNDLE_ALLOW_DISK=1.
  • Do not post-process the artifactstrip, objcopy, upx or re-signing-tools that rewrite the file destroy the embedded bundle trailer.
  • macOS: building on a Mac ad-hoc re-signs the artifact automatically. A darwin-arm64 artifact cross-built on Linux must be re-signed before Apple Silicon will run it: codesign --force -s - my_app-darwin-arm64 on a Mac. Use codesign specifically — the payload rides inside __LINKEDIT, and a signer that regenerates that segment (such as rcodesign) discards it. No Windows target is published.
  • soli update does not apply to standalone apps — a runtime fix means rebuilding and redeploying the artifact.

Next Steps