INSTALL

Running in one command.

Free, self-hosted, no signup. The installer checks your machine, sets everything up and tells you where to open Talos, so you can try a working control plane within minutes.

terminal
curl -fsSL https://talosfleet.site/install.sh | sh

Any distro with sh. Installs Docker for you if it is missing, after asking first. It asks before it changes anything. Enter means no.

FOUR WAYS TO INSTALL

Pick the one that matches where Talos will run.

One command

A Linux server or VM, or a Mac.

curl -fsSL https://talosfleet.site/install.sh | shread the guide →

Windows and macOS

A desktop with Docker Desktop.

irm https://talosfleet.site/install.ps1 | iexread the guide →

A new cloud server

Paste a start-up script when you create the server. No SSH needed.

#!/bin/sh … (cloud-init user data)read the guide →

Try it on your computer

Only your computer can open it, and one command removes it all again.

curl -fsSL https://talosfleet.site/install.sh | sh -s -- --evalread the guide →

WHAT THE INSTALLER DOES

It asks before it changes anything.

One script for every system: on Windows and macOS it runs inside a small Docker container, so it is the same tested installer everywhere. Run it again at any time: it finds the existing install and updates it in place.

01

Checks Docker

On Linux it offers to install Docker with Docker’s own installer if it is missing, and to start it if it is stopped. On Windows and macOS it starts Docker Desktop and waits for it.

02

Finds an existing install

An earlier Talos on the machine is updated in place: same folder, same data, same secrets. It never starts a second, empty database next to data it can see.

03

Checks the machine

CPU, memory, disk and both ports. Below the minimum it warns and asks. A busy port gets a suggested free one, or one you type.

04

Shows the plan and asks

Nothing is changed before you answer. Enter means no. --yes answers yes to everything, for scripts.

05

Writes two files

docker-compose.yml and .env, with secrets generated on the spot. Into /opt/talos as root, otherwise ~/talos.

06

Starts Talos and tests it

Waits until the backend is healthy, then opens the links from this machine. It only says “running” once they answer.

07

Prints the link and the setup token

The address to open, and the one-time token that creates the first account.

a successful install
Talos 1.1.0 is installed and running.
 
Open: http://203.0.113.10:3000
Setup token: 3f9a07c2e1b4…
Needed once: choose "Create the first one" and enter it.
It is also saved as SETUP_TOKEN in /opt/talos/.env.
 
Back up /opt/talos/.env now.

WHY A SETUP TOKEN

Whoever creates the first account becomes the administrator. On a server with a public address, automated scanners can find unclaimed first-run pages within minutes, so creating that first account needs a token only someone with access to the machine can read. After that it is never asked for again.

PREREQUISITES

Docker

Linux: the installer installs Docker Engine for you if you agree. Windows and macOS: Docker Desktop, installed by you once.

2 GB RAM, 2 CPUs

The minimum; 4 GB and 4 CPUs recommended. Jobs run on your servers, not here, so it stays flat as the fleet grows.

Linux, Windows or macOS

It needs outbound SSH to the machines you intend to manage, and nothing inbound but your browser.

MINIMUMRECOMMENDED
CPU2 cores4 cores
Memory2 GB4 GB
Free disk10 GB20 GB

Below the minimum the installer warns and asks before going on. On Windows and macOS these are Docker Desktop’s own limits; raise them in Docker Desktop → Settings → Resources.

WHAT YOU ARE ABOUT TO RUN

Four public images. No build step.

Two of ours and two upstream. Nothing is compiled on your machine, there is no registry login, and you can inspect any of them before you start.

talosfleet/talos-backend
API, job queue and background workers
on Docker Hub ↗
talosfleet/talos-frontend
The console, and the /api proxy to the backend
on Docker Hub ↗

plus pgvector/pgvector:pg16 · redis:7-alpine (unmodified upstream)

PREFER TO DO IT BY HAND?

The whole deployment is two files.

The installer writes exactly these. Download them, fill in three secrets, and start it yourself. The script does nothing beyond this.

download the kit
mkdir talos && cd talos
curl -fsSLO https://talosfleet.site/install-kit/docker-compose.yml
curl -fsSL https://talosfleet.site/install-kit/env.example -o .env
docker-compose.yml (all 142 lines)
# Talos — self-hosted deployment
#
# This is the file we ship to customers. It runs PRE-BUILT images from a registry, so
# there is no source checkout, no build step, and no toolchain to install.
#
# 1. cp .env.example .env
# 2. follow the three instructions in .env
# 3. docker compose up -d
# 4. open http://localhost:3000
#
# Deliberately NOT the repository's root docker-compose.yml. That one builds from source
# and exposes every SaaS, billing and platform knob — handing it to a customer is handing
# them ninety lines of configuration to be confused by. This file has three variables.
#
# DEPLOYMENT_MODE is absent on purpose: it is baked into the image
# (TALOS_DISTRIBUTION=self-hosted), which also pins the mode and makes weak secrets fatal
# rather than a warning. See docs/deployment/MODES_DATABASES_AND_DISTRIBUTION.md.
 
services:
postgres:
# pgvector, not plain postgres:16 — the AI knowledge base needs the `vector`
# extension. Drop-in replacement, same data directory.
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_DB: talos
POSTGRES_USER: talos
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- talos_postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U talos -d talos']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
 
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- talos_redis:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 5s
retries: 5
 
backend:
image: ${TALOS_REGISTRY:-talosfleet}/talos-backend:${TALOS_VERSION:-latest}
restart: unless-stopped
ports:
- '${TALOS_BIND:+${TALOS_BIND:-}:}${BACKEND_PORT:-3001}:3001'
environment:
DATABASE_URL: postgresql://talos:${POSTGRES_PASSWORD}@postgres:5432/talos?schema=public
# `redis`, not `localhost` — inside the compose network the hostname is the service
# name. A stale `localhost` here makes the backend hang at boot and login fail with
# an empty response, which is not an obvious symptom.
REDIS_URL: redis://redis:6379
 
# The only two secrets. The backend REFUSES TO BOOT if either is missing or is the
# published default — see .env for why, and for how to generate them.
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?set ENCRYPTION_KEY in .env}
 
# Needed once, to create the FIRST account — so a fresh install on a public IP
# cannot be claimed by whoever reaches it first. The installer generates it and
# shows it with the link. Left empty, the backend derives one and prints it in
# `docker compose logs backend` until the first account exists.
SETUP_TOKEN: ${SETUP_TOKEN:-}
# Only `false` switches it off. The installer writes that ONLY for --eval, together
# with TALOS_BIND=127.0.0.1, so nobody else can reach the page the token guards.
SETUP_TOKEN_REQUIRED: ${SETUP_TOKEN_REQUIRED:-true}
 
# Public URL of the frontend, used in the links inside emails.
FRONTEND_URL: ${FRONTEND_URL:-http://localhost:3000}
 
# ── Optional, all defaulted ──
# Every one of these must be listed here, not just documented in .env: an unlisted
# variable is silently dropped by Compose, so a customer would set it and nothing
# would happen — a failure with no error message anywhere.
ALLOW_SELF_REGISTRATION: ${ALLOW_SELF_REGISTRATION:-false}
ALLOW_MULTI_ORG: ${ALLOW_MULTI_ORG:-false}
 
# Sign-in with Google / GitHub. The backend refuses to boot if a provider is
# enabled without credentials, rather than showing a button that cannot work.
OAUTH_CALLBACK_BASE_URL: ${OAUTH_CALLBACK_BASE_URL:-}
OAUTH_GOOGLE_ENABLED: ${OAUTH_GOOGLE_ENABLED:-false}
OAUTH_GOOGLE_CLIENT_ID: ${OAUTH_GOOGLE_CLIENT_ID:-}
OAUTH_GOOGLE_CLIENT_SECRET: ${OAUTH_GOOGLE_CLIENT_SECRET:-}
OAUTH_GITHUB_ENABLED: ${OAUTH_GITHUB_ENABLED:-false}
OAUTH_GITHUB_CLIENT_ID: ${OAUTH_GITHUB_CLIENT_ID:-}
OAUTH_GITHUB_CLIENT_SECRET: ${OAUTH_GITHUB_CLIENT_SECRET:-}
 
# Outbound email. Defaults to `log`, which writes invitation and reset links to
# `docker compose logs backend` — workable for one admin, awkward for a team.
MAIL_TRANSPORT: ${MAIL_TRANSPORT:-log}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
MAIL_FROM: ${MAIL_FROM:-}
volumes:
# Terraform workspaces and generated state. Persisted because losing a state file
# orphans real cloud resources that Terraform can then neither see nor destroy.
- talos_terraform:/app/infra/terraform/workspaces
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost:3001/api/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
 
frontend:
image: ${TALOS_REGISTRY:-talosfleet}/talos-frontend:${TALOS_VERSION:-latest}
restart: unless-stopped
ports:
- '${TALOS_BIND:+${TALOS_BIND:-}:}${FRONTEND_PORT:-3000}:3000'
environment:
# There is deliberately no API URL to set here.
#
# Next.js compiles NEXT_PUBLIC_* into the client bundle at BUILD time, so a prebuilt
# image cannot be told its API address at run time — this block used to pass
# NEXT_PUBLIC_API_URL and it silently did nothing, leaving every install that was not
# reached at localhost with a UI that could not talk to its own API.
#
# The browser now calls this container on a relative /api, and the Next server
# forwards to the backend below. So Talos works at whatever hostname you put in front
# of it, with nothing to configure and no CORS to get wrong.
BACKEND_INTERNAL_URL: http://backend:3001
depends_on:
- backend
 
volumes:
talos_postgres:
talos_redis:
talos_terraform:
.env
# Talos — self-hosted configuration
#
# cp .env.example .env
#
# Three values. Everything else has a working default.
 
 
# ─── 1. Database password ─────────────────────────────────────────────────────
# Used only inside the Docker network, never exposed.
#
# openssl rand -hex 24
#
# On Windows PowerShell, if you have no openssl:
# -join ((1..48) | % { '0123456789abcdef'[(Get-Random -Max 16)] })
#
# ⚠ USE hex, NOT base64. This value is substituted into a connection URL:
#
# postgresql://talos:<PASSWORD>@postgres:5432/talos
#
# base64 output contains "/" and "+", and a "/" ends the URL's authority
# section — so the backend crash-loops on a cryptic Prisma error that says
# nothing about your password:
#
# Error: P1013 ... invalid port number in database URL
#
# `openssl rand -base64 24` produces 32 characters, so roughly two of every
# three passwords generated that way contain a "/" or "+". This file used to
# recommend exactly that. hex is URL-safe by construction and 48 hex characters
# are stronger than 32 base64 ones.
#
# If you must reuse an existing password containing / + @ : ? # or %, you have to
# percent-encode it here (/ becomes %2F, + becomes %2B, @ becomes %40).
#
POSTGRES_PASSWORD=
 
 
# ─── 2 & 3. The two secrets ───────────────────────────────────────────────────
# Talos REFUSES TO START if either is missing or left at a default. That is
# deliberate: both fallbacks are published in a public repository, so an install using
# them has forgeable login tokens and every stored credential — SSH keys, cloud keys,
# database passwords — encrypted under a value anyone can look up.
#
# Generate each ONCE:
#
# openssl rand -hex 32 # JWT_SECRET
# openssl rand -hex 24 # ENCRYPTION_KEY
#
# On Windows PowerShell, if you have no openssl (64 and 48 hex characters):
# -join ((1..64) | % { '0123456789abcdef'[(Get-Random -Max 16)] })
# -join ((1..48) | % { '0123456789abcdef'[(Get-Random -Max 16)] })
#
# hex like the database password: one rule for every secret, and the installer generates
# exactly these.
#
JWT_SECRET=
ENCRYPTION_KEY=
 
# ⚠ BACK UP ENCRYPTION_KEY WITH YOUR DATABASE. ⚠
#
# It cannot be changed once data exists. Every SSH key, cloud credential and database
# password is encrypted with it, and there is no recovery path — a database backup
# without this key is unreadable. Losing it means re-entering every credential by hand.
# Losing it *and* the credentials means starting over.
 
 
# ─── Optional ─────────────────────────────────────────────────────────────────
 
# Setup token: needed once, to create the FIRST account. It stops anyone who reaches a
# fresh install before you do — on a server with a public IP, bots look for exactly that —
# from making themselves its administrator. After the first account exists it does nothing.
#
# openssl rand -hex 16
#
# Left empty, Talos derives one from ENCRYPTION_KEY and prints it in
# `docker compose logs backend` until the first account is created.
# SETUP_TOKEN=
 
# Where users reach Talos in a browser. Used for the links inside invitation and
# password-reset emails, so set it if you are not on localhost — otherwise you will send
# people a link to their own machine.
#
# There is no API URL to configure: the browser talks to the frontend on a relative /api
# and the frontend forwards to the backend inside the Docker network. Talos therefore works
# at whatever hostname you put in front of it. (An earlier version of this file had a
# PUBLIC_API_URL here; it never had any effect, because that value is compiled into the
# frontend at build time and cannot be supplied to a prebuilt image at run time.)
# FRONTEND_URL=http://localhost:3000
 
# Which address the ports listen on. Default 0.0.0.0: every network interface, so other
# computers can reach Talos. 127.0.0.1: only this computer (e.g. behind your own reverse
# proxy on the same machine).
# TALOS_BIND=0.0.0.0
 
# Host ports, if 3000/3001 are taken.
# FRONTEND_PORT=3000
# BACKEND_PORT=3001
 
# Pin a version instead of tracking `latest`. Recommended for production: an
# unattended `latest` means an upgrade whenever you happen to pull.
#
# NO LEADING "v". Releases and git tags are named like "v1.2.3", but the image tags carry
# no prefix — so a leading "v" fails with "manifest unknown". Copy the number, not the
# release title.
#
# TALOS_VERSION=1.2.3 ✅
# TALOS_VERSION=v1.2.3 ❌ no such tag
#
# (1.2.3 is an example. The current version is on the install page and the releases page.)
#
# TALOS_VERSION=latest
 
# Sign-in with Google or GitHub. Off unless configured; users sign in with a password.
# Register the app first and set the callback to
# <your API URL>/auth/oauth/<provider>/callback
# exactly — a mismatch is the most common cause of a flow that fails with no useful
# message. See docs/oauth-social-login/PLAN.md §8.
# OAUTH_CALLBACK_BASE_URL=http://localhost:3001
# OAUTH_GOOGLE_ENABLED=false
# OAUTH_GOOGLE_CLIENT_ID=
# OAUTH_GOOGLE_CLIENT_SECRET=
# OAUTH_GITHUB_ENABLED=false
# OAUTH_GITHUB_CLIENT_ID=
# OAUTH_GITHUB_CLIENT_SECRET=
 
# Outbound email, for invitations and password resets. Without it those links are
# written to the container log instead (`docker compose logs backend`), which works for
# a single-admin install but is awkward for a team.
# MAIL_TRANSPORT=smtp
# SMTP_HOST=
# SMTP_PORT=587
# SMTP_USER=
# SMTP_PASSWORD=
 
# By default one organization, and no self-service signup — you create accounts by
# invitation. Turn these on to run several teams on one install.
# ALLOW_SELF_REGISTRATION=false
# ALLOW_MULTI_ORG=false
generate the secrets (use hex: base64 breaks the database URL)
openssl rand -hex 24 # POSTGRES_PASSWORD
openssl rand -hex 32 # JWT_SECRET
openssl rand -hex 24 # ENCRYPTION_KEY
openssl rand -hex 16 # SETUP_TOKEN (optional)
 
# Windows PowerShell, without openssl (48 hex characters):
-join ((1..48) | % { '0123456789abcdef'[(Get-Random -Max 16)] })
start it
docker compose up -d

The first start takes a minute or two while the database schema is applied. Without a SETUP_TOKEN in .env, the one to use is printed in docker compose logs backend.

AFTER IT IS RUNNING

Production, backups, upgrades and uninstalling.

By hand
# the same two files the installer writes
mkdir talos && cd talos
curl -fsSLO https://talosfleet.site/install-kit/docker-compose.yml
curl -fsSL https://talosfleet.site/install-kit/env.example -o .env
 
# fill in the three secrets (hex, see below), then
docker compose up -d
 
✓ migrations applied
·Use hex for the secrets: base64 contains “/” and “+”, and POSTGRES_PASSWORD goes inside a connection URL.
·Without SETUP_TOKEN in .env, Talos prints one in `docker compose logs backend` until the first account exists.
·The folder name becomes the Compose project, and so the volume names. Keep it, or set COMPOSE_PROJECT_NAME.

YOUR FIRST FIVE MINUTES

From empty console to a streaming job.

01

Create the first account

Open the link the installer printed, choose “Create the first one” and enter the setup token. It is needed once: whoever creates the first account becomes the administrator, so a stranger who reaches the page first cannot.

→ http://localhost:3000
setup token: 3f9a…c21e (from the installer)
✓ organization created · you are the administrator
02

Add a server

Hostname, port, user. No agent to install: Talos connects over plain SSH.

host: 10.0.4.21 user: ops port: 22
→ testing reachability …
✓ reachable
03

Add an SSH key

Pasted once and encrypted at rest under your ENCRYPTION_KEY. It is decrypted in memory for the duration of a job and never written to a log line or a job payload.

key: talos_ed25519 (ed25519)
✓ encrypted at rest
✓ fingerprint SHA256:9tK…c1
04

Run discovery

Read-only inventory: OS, kernel, runtimes, running containers, database engines. It changes nothing on the host.

$ discover 10.0.4.21
→ ubuntu 24.04 · kernel 6.8.0
→ docker 27.3 · 6 containers
✓ discovery complete
05

Watch the first job stream

Every line arrives live. What you watch is exactly what lands in the audit ledger.

$ jobs follow 1
→ [00:00.2] connecting …
→ [00:03.1] gathering facts
✓ [00:08.4] job #1 succeeded

ENVIRONMENT VARIABLES

What is in .env.

VARIABLEREQUIRED?WHAT IT DOES
POSTGRES_PASSWORDrequiredHex. Used only inside the Docker network. Never change it after the first start, because the database keeps the original.
JWT_SECRETrequiredSigns session tokens. The built-in default is publicly known, so Talos refuses to start without your own.
ENCRYPTION_KEYrequiredEncrypts every stored credential. Cannot be changed once data exists; there is no recovery path.
SETUP_TOKENoptionalNeeded once, to create the first account. The installer generates it. Left empty, Talos makes one and prints it in the backend log until the first account exists.
FRONTEND_URLoptionalWhere users reach Talos in a browser. Used for links inside invitation and password-reset emails. Set it if you are not on localhost.
FRONTEND_PORT / BACKEND_PORToptionalHost ports, if 3000 and 3001 are already taken.
TALOS_BINDoptionalThe address the ports listen on. Unset: every network interface. 127.0.0.1: only this computer (e.g. behind your own reverse proxy).
TALOS_VERSIONoptionalThe image version. The installer pins it; pin it by hand too, rather than tracking latest.

THIS IS DELIBERATE

Talos refuses to start with missing or default secrets. There is no fallback value and no warning-and-continue path. The check runs before anything opens a network connection, so a refused boot has written nothing. An instance holding your root SSH keys should never come up with a publicly known default key.

BACK UP ENCRYPTION_KEY WITH YOUR DATABASE

It cannot be changed once data exists. Every SSH key, cloud credential and database password is encrypted with it, and there is no recovery path: a database backup without this key is unreadable.

Email, Google/GitHub sign-in and multiple organizations are in the configuration reference.

TROUBLESHOOTING

If Talos does not start.

Where is the setup token?
The installer prints it next to the link, and it is saved as SETUP_TOKEN in the .env file in the install folder (/opt/talos, ~/talos, or C:\Users\<you>\talos). Installed by hand without one? Run docker compose logs backend and look for “setup token”. After the first account exists it is never asked for again.
The installer says a port is in use.
It suggests the next free port. Press Enter to take it, or type another. With --yes it takes the suggestion. To choose up front: --frontend-port and --backend-port (-FrontendPort / -BackendPort on Windows).
“Stopping: existing Talos data belongs to a different Compose project.”
The installer found Talos data on this machine under another project name, which usually means a moved or renamed install. Starting here would create a second, EMPTY database while the real one sits unused, so it stops instead. The message says how to point it at the existing data, or to start a separate new install with --dir and a new folder.
The backend exits immediately with “Refusing to start”.
JWT_SECRET or ENCRYPTION_KEY is missing, or still set to the published default, or the database password contains “/”, “?” or “#”, which break the connection URL. The check runs before anything is written. The installer generates safe hex values; by hand, use the openssl commands above.
I lost ENCRYPTION_KEY. Can I reset it?
No. Every SSH key, cloud credential and database password is encrypted with it and there is no recovery path, for you or for us. You would re-enter every credential by hand against a fresh install. Back the key up somewhere separate from the database dump, before you have any data.
Windows/macOS: “Docker Desktop did not finish starting”.
The installer starts Docker Desktop and waits two minutes. On a first start Docker Desktop may be waiting for you to accept its terms. Open it, finish that, and run the installer again.
On an arm64 machine the installer stops about the kernel and Redis.
Some older arm64 kernels have a memory bug that Redis 7 refuses to run on. The installer checks this before starting anything. Update the kernel (fixed in 5.10, and in 5.9.12 / 5.4.81 / 4.19.161 / 4.14.210) and run it again.
Discovery fails with “permission denied (publickey)”.
The private key in Talos does not match a public key in the target host’s authorized_keys, or the SSH user is wrong. Test the same key from your own shell first: ssh -i ~/.ssh/talos_ed25519 ops@host. If that fails, Talos will too.
Logs stream for a few seconds then stop.
Almost always a reverse proxy buffering the response. In nginx, set proxy_buffering off and a long proxy_read_timeout for the streaming path. The job itself is still running; only the live stream stopped.

It's running. Now add your fleet.

Discovery is read-only, so connecting your first server changes nothing on it.

Read the security model