Sign up

Docs

Everything you need to go from code to live URL.

Install

Works on macOS, Linux and Windows (Intel and ARM). Download and inspect the installer before executing it; the installer then verifies the independently signed release record and archive digest.

macOS / Linux

$ installer="$(mktemp)"
$ curl --fail --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
    https://innstance.impossibuild.ai/install --output "$installer"
$ less "$installer"
$ sh "$installer"
$ rm -f "$installer"

The binary installs to ~/.local/bin/innstance, with ifhost beside it as a link to the same file. Make sure that directory is in your PATH.

# Add to ~/.zshrc or ~/.bashrc if needed
export PATH="$HOME/.local/bin:$PATH"

Windows (PowerShell)

PS> $installer = Join-Path ([IO.Path]::GetTempPath()) "innstance-install-$([guid]::NewGuid()).ps1"
PS> Invoke-WebRequest -Uri https://innstance.impossibuild.ai/install.ps1 -MaximumRedirection 0 -TimeoutSec 60 -OutFile $installer
PS> Get-Content $installer
PS> & $installer
PS> Remove-Item $installer

The binary installs to %LOCALAPPDATA%\innstance\innstance.exe, and the installer adds that folder to your user PATH itself — open a new terminal for it to take effect. For the previous name, a new install puts two small shims beside it, ifhost.cmd (cmd and PowerShell) and ifhost (Git Bash); an install that already has an ifhost.exe keeps it, updated to the same build. In scripts and agent configurations, call innstance: ifhost.cmd runs through cmd.exe, which reads characters such as &, | and % in arguments a second time. Signature verification uses ssh-keygen from the Windows OpenSSH Client; if the installer reports it missing, run Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 in an elevated PowerShell, then retry.

Login / Logout / Switch Account

For normal production use, run innstance login below. Only when using staging or a self-hosted environment, set its API URL in your current terminal first and use an account and token belonging to that environment.

# macOS / Linux
export IMPOSSIBLE_API_URL=https://innstance.impossibuild.ai
# Windows PowerShell
$env:IMPOSSIBLE_API_URL = "https://innstance.impossibuild.ai"

The current CLI also reads this setting as INNSTANCE_API_URL, the product's new name. A CLI that has not updated yet ignores that name without a warning and would talk to production with its saved login, so use IMPOSSIBLE_API_URL, which every build reads.

Sign in (interactive)

$ innstance login
  Opening your browser to sign in...
  (pick your account, approve)

  Logged in as you@email.com

Login via token (CI / agents)

$ printf '%s' "$TOKEN_FROM_SECRET_STORE" | innstance login --token -
# Or read a mounted/local secret file:
$ innstance login --from-file /run/secrets/innstance-token

On Windows the same two forms are $env:TOKEN_FROM_SECRET_STORE | innstance login --token - and innstance login --from-file C:\path\to\token.txt.

In CI, inject IMPOSSIBLE_API_TOKEN with the CI provider's secret store; every CLI build reads it, and the current one also reads it as INNSTANCE_TOKEN. If you also set the API URL, use the matching name (IMPOSSIBLE_API_URL with IMPOSSIBLE_API_TOKEN): a token and URL set under different names are refused. Do not put a token literal in a command, repository file, or workflow.

Multiple accounts

You can log in to multiple accounts. Each login is saved as a profile.

# Log in to a second account
$ innstance login
Logged in as work@company.com

# Switch between accounts
$ innstance login --switch
Accounts:
  * [1] you@email.com
    [2] work@company.com

Enter number: 2
Switched to: work@company.com

Logout

$ innstance logout
Logged out: work@company.com
Switched to: you@email.com

Removes the active account. If other profiles remain, switches to the next one.

Tip: Credentials are saved at ~/.impossible/credentials.json (%USERPROFILE%\.impossible\credentials.json on Windows), or at ~/.innstance/credentials.json once that file exists. Supports multiple profiles with one active. The IMPOSSIBLE_API_TOKEN env var (the current CLI also reads INNSTANCE_TOKEN) overrides the file (useful for CI).

Publish a website

A front-end that builds to static files (Vite, React, Vue, Svelte, Astro, Next.js with output: 'export', Hugo, Jekyll) needs no machine. Build it, then publish the folder the build writes:

$ npm run build
$ innstance publish --name my-app ./dist
Published! Your site is live at: https://my-app.<tenant domain>

# The same site from a .zip
$ innstance publish --name my-app site.zip

The folder needs index.html at its top. /about serves about.html or about/index.html, and a top-level 404.html answers addresses with nothing behind them. A single-page app (one index.html and no other page) gets index.html for its client-side routes; --spa=on or --spa=off decides that yourself.

A _redirects and a _headers file at the top of the folder are read the way Netlify writes them, for a safe subset: redirects and rewrites within the site, redirects to other websites, but no proxying to them, and only headers that cannot weaken the platform's own. Anything left out is listed after the publish. Names starting with a dot are skipped, and only web file types publish.

Versions and rollback

$ innstance sites versions my-app
$ innstance sites rollback my-app
$ innstance sites rollback my-app --to <version>

Every publish is kept as a version, and the most recent few stay stored. A rollback switches which one is live, at once, without uploading anything; rolling forward is the same command with --to.

In the dashboard, Update files on a site takes the website as one .zip of its folder (lock a new name first), and Versions lists what is kept, makes one live again and deletes one.

One HTML page, PDFs and images publish the same way: innstance publish --name my-page page.html, or innstance publish --name menu a.pdf b.pdf.

Deploy a runner

innstance deploy provisions or reuses a shell VM. Uploading code, installing dependencies and starting the application are explicit steps. A built website, a single HTML page, PDFs and images can use innstance publish instead.

Choose resources and boot the machine

$ innstance init --app my-app --port 3000 --memory 512 --autostop=false --min-machines 1
$ innstance deploy --yes

Read the project's README to choose its port, dependencies and start command. Keep persistent application data under a mounted volume such as /data; use --storage local when initializing an app that needs it.

Upload and install

$ innstance machines push --app my-app . --to /app
$ innstance machines install --app my-app <apt-package> --max-wait 6m

Install the runtime packages your project requires, then run its package-manager setup and migrations with innstance machines exec. An installation success confirms installed packages, not application readiness.

Start and verify the application

$ innstance machines exec --app my-app -- sh -c 'cd /app && setsid nohup <start-command> </dev/null >/tmp/app.log 2>&1 &'
$ innstance status

Replace <start-command> with the project's actual command. Bind the server to 0.0.0.0 on the configured port, inspect its logs, and check the app URL returned by the API before reporting it live. Use innstance machines console for interactive setup.

Replacing a runner machine does not recreate its installed tools or application process. Preserve needed data on the volume and retain reproducible setup commands.

Account Overview

Example output below includes existing legacy multi-machine apps. New runner apps use one machine. Addresses are placeholders: the real one is built from the app name, and the CLI prints it.

$ innstance status
Logged in as: you@email.com
Plan:         pro

Apps (2):

  my-app
    URL:      https://<my-app address>
    Status:   deployed   Region: iad
    Running:  d8967e0f
    Standby:  a1b2c3d4

  api-backend
    URL:      https://<api-backend address>
    Status:   deployed   Region: sin
    Running:  e3f4a5b6

Shows your login, plan, and every app grouped with its URL, status, region, and current machine IDs. No --app needed.

For agents: innstance status --json is the fastest way to discover machine IDs for machines exec --machine <id> or machines console start. Pipe to jq.

Machines

All app-specific commands live under innstance machines.

List machines

$ innstance machines --app my-app
Machines for 'my-app' (2): 1 running, 1 stopped

Running:
  d8967e0f  sweet-surf-1234  started  iad  shared/1  256MB  created 2d ago

Standby:
  a1b2c3d4  proud-moon-5678  stopped  iad  shared/1  256MB  created 2d ago

Grouped by state (Running vs Standby), with machine name and age so you can tell replicas apart. Use any listed ID with --machine <id> on exec, or target all at once with start / stop / restart.

Start / Stop / Restart

$ innstance machines stop --app my-app      # Stop all machines
$ innstance machines start --app my-app     # Start them back up
$ innstance machines restart --app my-app   # Restart with fresh env/secrets

Environment Variables

Non-sensitive config - injected into your container's environment at runtime.

# Stage one or more; pass --restart to apply immediately
$ innstance machines env set NODE_ENV=production PORT=3000 --app my-app
Set 2 environment variable(s)

# List all
$ innstance machines env list --app my-app
NODE_ENV=production
PORT=3000

# Inline during deploy (repeatable)
$ innstance deploy --app my-app --env NODE_ENV=production --env PORT=3000

Secrets

For sensitive values (API keys, database URLs, tokens). Values are read from environment variables, files, or stdin—never literal command arguments—and are never returned by the API. Changes are staged until you restart, unless you pass --restart.

# Set secrets from protected sources
$ innstance machines secrets set DATABASE_URL=@env:DATABASE_URL API_KEY=@file:/secure/api-key --app my-app
Set 2 secret(s)

# List (keys only - values never shown)
$ innstance machines secrets list --app my-app
API_KEY
DATABASE_URL

# Read one secret from stdin during deploy
$ printf %s "$STRIPE_KEY" | innstance deploy --app my-app --secret STRIPE_KEY=@stdin
Never put literal secrets in: impossible.toml, Dockerfile ENV, or CLI arguments. Use @env:NAME, @file:PATH, or @stdin. A tracked [secrets] config is refused and the config file is excluded from uploads.

Custom Domains

Run the add command and follow the DNS records it returns. If ownership is not verified yet, add the requested TXT record, keep it in DNS, and run the command again. Use the returned routing target; do not derive it from the app name or its public URL.

$ innstance machines domains add myapp.example.com --app my-app

# After adding the returned DNS records, retry
$ innstance machines domains add myapp.example.com --app my-app

# List domains and recorded TLS status
$ innstance machines domains list --app my-app

# Remove
$ innstance machines domains rm myapp.example.com --app my-app

TLS certificates are provisioned automatically via Let's Encrypt after you add the CNAME record.

Content checks and free plan limits

Every page you publish is checked before it goes live. If it looks like phishing, a crypto wallet drainer, malware, gambling promotion or adult content, it is kept but held offline for a person to review. The publish command then fails with the reason and an appeal link, and the site can't be changed or deleted until the review is done. Phishing, malware and child-safety holds are reviewed within 1 hour where possible, and always within 24 hours; everything else within 72 hours.

On the free plan

  • Forms can only submit to, and scripts can only fetch from, the site's own address. Free sites and apps are served with Content-Security-Policy: form-action 'self'; connect-src 'self' in addition to any policy you set.
  • Pages can't include password, card-number or card security-code fields, or send visitors to another website with a redirect (including a _redirects rule). A site that does is held for review. Every page of a website is checked, not only its front page.
  • Only web file types are published: .html .htm .css .js .mjs .json .map .txt .md .xml .csv .svg .png .jpg .jpeg .gif .webp .avif .ico .bmp .woff .woff2 .ttf .otf .eot .mp3 .wav .ogg .mp4 .webm .pdf .webmanifest .wasm .glb .gltf .yml .yaml .toml. Anything else is refused and nothing is published.

Any paid plan lifts these limits. A held or taken-down site shows an Under review badge with the reason and an appeal link in your dashboard.

Runner capacity

Runner apps use one machine. Manual replica scaling and autoscale commands are not available in the current CLI. A persistent volume attaches to that one machine.

To change CPU or memory, edit [resources] in impossible.toml, then run innstance apply from that project directory. The account plan and available pool limit the requested resources.

$ innstance apply
$ innstance machines --app my-app

Keep [service] autostop = false and min_machines = 1 for a manually started application unless it is prepared to recover through the stop/start lifecycle. A started machine alone does not prove that the application is serving requests.

Persistent Volumes (advanced)

A persistent volume stores data for one machine. Each machine can attach one volume. Store data that must survive replacement under its mount path.

Volumes provide a local disk that survives redeploys, attached to one machine:

$ innstance machines volumes create data --mount /data --size 5 --app my-app
Created volume 'data' (5GB) mounted at /data

$ innstance machines volumes list --app my-app
  data  5GB  /data  iad

$ innstance machines volumes rm data --app my-app

Volume limitations

  • 1 volume = 1 machine. A runner supports one primary volume on its one machine; it cannot share that volume across machines.
  • Fixed size. You choose the size at creation (charged even if empty). Can grow but not shrink.
  • Region-locked. Volume lives in one region; machine must be in the same region.

When to use a volume

Use caseRecommended
SQLite database (single machine)Volume
Local config / state for one machineVolume
User uploads, images, filesExternal S3 (Tigris, Cloudflare R2, AWS S3) - bring your own bucket and credentials
Postgres / MySQLManaged DB (Supabase, Neon)
Cache / RedisManaged Redis (Upstash)

Logs

Default: live stream (like tail -f), runs until Ctrl+C. Add --since or --lines to fetch historical logs and exit immediately. Filters work in both modes.

# Live tail - runs forever
$ innstance machines logs --app my-app

# Last hour, then exit
$ innstance machines logs --app my-app --since 1h

# Last 50 lines, then exit
$ innstance machines logs --app my-app --lines 50

# Filter by substring (streaming or historical)
$ innstance machines logs --app my-app --grep "ERROR"

# Filter by level (error = error/fatal/panic lines)
$ innstance machines logs --app my-app --level error

# Combined filters + historical window
$ innstance machines logs --app my-app --grep "GET" --lines 50

# Structured JSON, one object per line (for jq)
$ innstance machines logs --app my-app --json

Exec (run commands inside your app)

Run any command inside your running container. Useful for debugging, inspecting files, running migrations, or checking configuration.

$ innstance machines exec --app my-app -- ls /data
$ innstance machines exec --app my-app -- env
$ innstance machines exec --app my-app -- cat /etc/nginx/nginx.conf
$ innstance machines exec --app my-app -- python manage.py migrate
$ innstance machines exec --app my-app -- sh -c "du -sh /data/*"

# Multi-machine apps: target a specific machine by ID
$ innstance machines --app my-app                     # list machines with their IDs
$ innstance machines exec --app my-app --machine 32d41... -- ps -ef
For agents: Use exec to inspect the runner, run setup commands, or diagnose the application. Available tools depend on what has been installed. Use --machine to select an owned machine explicitly.
Limitations:
  • Use a console session for long-running or interactive commands
  • No interactive terminal (no vim, htop, tmux). Use for one-off commands only.
  • Machine must be running - start it first with innstance machines start
  • Install missing tools before invoking them through exec

Destroy

$ innstance machines destroy --app my-app --yes-irreversible  # Permanently delete app and all resources

Apply saved configuration

Environment and secret saves update stored configuration. innstance apply synchronizes resource edits from the current project's impossible.toml and applies CPU, memory, environment and secrets to the machine without a new deployment build. The machine restarts, so verify the application afterward.

$ innstance machines env set LOG_LEVEL=debug --app my-app
$ innstance apply --app my-app

Inspect the result for failed machine IDs. JSON output includes applied and failed_machines; the CLI's partial-result exit status alone does not establish that every machine succeeded.

Config File

innstance deploy requires impossible.toml (or innstance.toml, the same file under its current name; not both) in the current project directory. Run innstance init to create it, then edit the settings for your app. Supported command flags override the corresponding file settings for that invocation.

app = "my-app"
region = "iad"

[service]
  internal_port = 3000
  autostop = false
  min_machines = 1

[resources]
  cpu_kind = "shared"     # "shared" or "performance" (performance on Pro and Studio)
  cpus = 1                # 1, 2, 4, 8 (your plan caps the max)
  memory_mb = 256         # 256, 512, 1024, 2048, ...

[env]
  NODE_ENV = "production" # Non-sensitive only!

With this file in your repo, deploys are just:

$ innstance deploy

Update & Version

Check version

$ innstance version
innstance build: 20260416-024938
Up to date.

Compares your local build with the latest on the server. If outdated:

$ innstance version
innstance build: 20260101-000000
Update available: 20260101-000000 -> 20260416-024938
Run 'innstance update' to install.

Update

$ innstance update
Checking for updates...
Updated: /Users/you/.local/bin/innstance

Downloads the latest binary for your OS/arch and replaces the current one in-place (%LOCALAPPDATA%\innstance\innstance.exe on Windows).

Tip: innstance status also shows your CLI version and a hint if an update is available - no need to check manually.

Upgrading and Defender

A new CLI build ships only when the CLI itself changes; its build ID follows the CLI's own source, not every server release. Each new build is still a new file to Windows, and Microsoft Defender or SmartScreen may hold back a file it has not seen before. The Windows build is not code-signed yet.

Before you allow a blocked or quarantined innstance.exe, check it against the expected digest. The digest of every release archive is in the release record, /dl/release.txt, and /dl/release.txt.sshsig is its signature. A digest fetched from the same server as the file only shows the download is intact, so verify the signature first, with the same key the installer uses, as below. (The installer does this before it installs anything; innstance update instead verifies the signed manifest at /cli/version, which carries the same digests.) Take the digest from the verified record each time: never from a page, a chat or an email, and this page lists none on purpose. ssh-keygen comes with the Windows OpenSSH Client, as for the installer.

PS> $dl = 'https://innstance.impossibuild.ai/dl'
PS> Invoke-WebRequest "$dl/release.txt" -OutFile release.txt
PS> Invoke-WebRequest "$dl/release.txt.sshsig" -OutFile release.txt.sshsig
PS> [IO.File]::WriteAllText("$PWD\allowed_signers", "ifhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEv+FR+Wibo0JJPEmmJfqQz2wsoBkrCLatDZ8XwZq2zJ`n")
PS> cmd /d /c 'ssh-keygen -Y verify -f allowed_signers -I ifhost -n ifhost-release -s release.txt.sshsig < release.txt'
Good "ifhost-release" signature for ifhost with ED25519 key SHA256:UTpOS476R6nx7lTC1abytzI4ZxFowCAefeJFVaR/2s0
PS> if ($LASTEXITCODE -ne 0) { throw 'release.txt is not signed by the release key; stop here' }
PS> $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }
PS> $zip = "innstance_windows_$arch.zip"
PS> $expected = ((Get-Content release.txt | Where-Object { $_ -like "artifact.$zip=*" }) -replace '^[^=]*=', '').Trim()
PS> Invoke-WebRequest "$dl/$zip" -OutFile $zip
PS> (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower() -eq $expected
True
PS> Expand-Archive $zip -DestinationPath innstance-check
PS> Get-FileHash innstance-check\innstance.exe, "$env:LOCALAPPDATA\innstance\innstance.exe"

If the signature is good, the archive matches the record and the two innstance.exe hashes match each other, the file is the release; report the detection to Microsoft as a false positive at microsoft.com/wdsi/filesubmission. If anything differs, do not run it, and tell us through the contact in /.well-known/security.txt.

If Windows will not start a build that an update just installed, the CLI puts the previous build back by itself and says so. If Defender blocks the new build later, put the previous build back by hand; it stays beside the new one as innstance.exe.old until the new build has run once:

PS> $dir = Join-Path $env:LOCALAPPDATA 'innstance'
PS> Remove-Item (Join-Path $dir 'innstance.exe') -ErrorAction SilentlyContinue
PS> Rename-Item (Join-Path $dir 'innstance.exe.old') 'innstance.exe'

Without an innstance.exe.old, check the blocked file as above, or run the installer again.

For AI Agents

Innstance is designed to be used by AI coding agents. Key features:

  • --json flag on every command - structured output to stdout, logs to stderr
  • --yes skips ordinary confirmation prompts. Destructive actions and egress consent still require their explicit flags, such as --yes-irreversible and --yes-egress.
  • IMPOSSIBLE_API_TOKEN env var (the current CLI also reads INNSTANCE_TOKEN) - no interactive login needed
  • Clear exit codes - 0 success, non-zero failure
  • Error messages include what to do next
# Agent workflow: deploy and get URL as JSON
$ innstance deploy --app my-app --json
{"id":"01ABC...","status":"live","url":"https://<my-app address>","machines":1}

# Check account status
$ innstance status --json
{"email":"you@email.com","plan":"free","apps":[{"name":"my-app","status":"deployed",...}]}

For the complete API reference (all endpoints, request/response shapes), see /llms.txt.

Remote MCP

Assistants that speak MCP can use Innstance directly, with no CLI on the machine: https://innstance.impossibuild.ai/mcp (Streamable HTTP). Create a token for it. It gets every scope the tools use and expires in 30 days, and the command prints the settings below with the token filled in. Narrow it with --scope, --app or --site.

$ innstance tokens create --mcp
$ innstance tokens create --mcp --scope read,publish --site my-page

Claude Code

$ claude mcp add --transport http innstance https://innstance.impossibuild.ai/mcp \
    --header "Authorization: Bearer $INNSTANCE_MCP_TOKEN"

Codex (~/.codex/config.toml, with INNSTANCE_MCP_TOKEN exported)

[mcp_servers.innstance]
url = "https://innstance.impossibuild.ai/mcp"
bearer_token_env_var = "INNSTANCE_MCP_TOKEN"

Cursor (~/.cursor/mcp.json)

{ "mcpServers": { "innstance": {
    "url": "https://innstance.impossibuild.ai/mcp",
    "headers": { "Authorization": "Bearer ${env:INNSTANCE_MCP_TOKEN}" } } } }

Tools: whoami, sites_list, publish_html, publish_files and publish_files_finalize, sites_delete, domains_add, domains_check, apps_list, app_status, app_logs, app_exec, app_deploy, billing_status, topup_link. A token only sees the tools its scopes allow.

To publish files from a chat, publish_files returns one upload URL per file. PUT each file to its URL within 15 minutes; it needs no token and takes one upload of at most the size you declared. Then call publish_files_finalize. Everything goes through the same API, plan rate limits and content checks as the CLI.

Scopes are read, publish, deploy, destroy, domains and billing:read. destroy deletes apps, their machines and their volumes; no tool uses it, so an assistant's token leaves it out. A token with none of them, such as one from before scopes existed, keeps full access. A scoped token can never create tokens, change the plan, or manage hosted assistants or wallets. innstance tokens list and the dashboard show what each token may do; revoke one there when the assistant no longer needs it.