Deployment
Deploy your Aegis Stack project to a remote server with a single command. The deployment system handles file syncing, Docker builds, and service orchestration over SSH.
Prerequisites
- A VPS or server with SSH access (Ubuntu, Debian, or Fedora)
- SSH key authentication configured for your server
- rsync installed locally (included on macOS and most Linux distributions)
- A domain (optional, required for TLS/HTTPS)
Quick Start
# 1. Initialize deployment config
aegis deploy-init --host 192.168.1.100
# 2. Provision the server (installs Docker, configures firewall)
aegis deploy-setup
# 3. Deploy your application
aegis deploy
Your application is now running at http://your-server-ip, or at the name aegis deploy-provision gave it. The Overseer at /dashboard/ admits only private networks until you add your address to ADMIN_IP_ALLOWLIST (see Ingress) and redeploy.
No server yet? Provision one
Experimental Feature
Run end to end against a live Hetzner account with an sslip.io name; the --domain path through Cloudflare is tested only against a mocked API.
export HCLOUD_TOKEN=... # Hetzner Cloud API token, read & write
aegis deploy-provision --email you@example.com
This replaces steps 1 and 2 above. It creates a server in your own Hetzner account (showing the monthly price first), points a name at it (<ip>.sslip.io, or yours with --domain and its DNS provider's keys, Porkbun's or Cloudflare's), turns on HTTPS for that name with Let's Encrypt, and runs aegis deploy. The server, the bill and the data stay yours. aegis deploy-destroy removes the server and its DNS record. All options: aegis deploy-provision.
From the Overseer
With the htmx frontend and Redis, Overseer > Deployments on your dev build
does the same from the page: a new server (a size and region picked from
what the provider sells, its price typed back to confirm), deploy, back up,
list backups, roll back, destroy. Until there is a server it walks you there
in steps (the agent, where: a provider's token or a server of your own, then
the server), each read from what is already true; after, it shows the
server and what can be done to it. Each action is audited, and its
output streams under Deployments > Jobs. The agent that runs them,
aegis deploy-agent, is the dev stack's deploy-agent service: make serve
starts and stops it with the rest, compose restarts it if it dies, and
docker compose logs deploy-agent shows what it did. It is never on the
server.
The service is built on the aegis this project pins, so aegis update brings
a new one. It drives your machine's Docker, so it sees the checkout and a
temp directory of its own at their own paths, and it signs with your machine's ssh-agent,
never a key file: make serve adds your key to the agent when it holds none
(ssh-add, once per login; on macOS the passphrase is kept in the Keychain).
Off Docker Desktop, set DEPLOY_AGENT_SSH_SOCK to your agent's socket.
AEGIS_STACK_SOURCE names a checkout of aegis to run instead: the service
runs it editable, each command on the code as it is, and the agent restarts
itself on it once idle. A stack generated from a checkout (aegis init --dev)
gets that checkout without asking, as the pinned release may not have the
agent its templates expect. Both are read by compose, not the app, so set them
in the shell (DEPLOY_AGENT_SSH_SOCK=... make serve), never in .env: the
app refuses to start on a key it does not know. With the stack started another way,
run aegis deploy-agent in the project directory yourself.
The cloud token is a secret like the app's other credentials: set
DIGITALOCEAN_TOKEN (or HCLOUD_TOKEN) on Overseer > Deployments >
Settings (beside where the provider makes one, and the scopes it needs), with the
secrets component, or in .env. The Overseer
offers a new server on each provider it has a token for, and sends the token
with the one request that needs it (the provider's sizes and regions, a new
server, a destroy), beside
its arguments rather than in them, so it is never audited or shown. The agent
hands it to the command in its environment, masks it in the output, and keeps
it nowhere. An exported variable where the agent runs works too (for CI, say),
and the Overseer offers that provider as well; a token sent with a request
wins over it.
The sizes and regions come from the provider, through the agent, and are
kept in Redis for an hour, so the form offers only what can be created and
where, at its price. The agent prices the server again before creating it,
and refuses if the price has changed since. Beside the price, "Will it fit?"
puts the stack against the size picked: its production services' memory (in
use now, and their limits), CPU limits and disk (images and data), measured
by the agent on your machine, each bar toned and saying why. Beside it,
what the account already runs: its servers with what each costs a month
(aegis's own marked), the total with the new one, and on DigitalOcean this
month's spend. Listing needs the token's droplet:read scope, and the
spend billing:read; without one, the page says which to add.
Why a separate runner: every step needs what the app's container does not
have and should not hold (the aegis CLI, the working tree it ships, your SSH
key, the host's Docker). So the Overseer only asks, through the project's
Redis, and aegis deploy-agent runs the same commands you would type, with
the key staying in your machine's ssh-agent. A deployed server's Overseer offers no deploy
actions: redeploy from the dev Overseer or from CI (aegis deploy-cd-setup).
The Flet dashboard shows the commands only.
Configuration
Deployment settings are stored in .aegis/deploy.yml, created by deploy-init:
# .aegis/deploy.yml
server:
host: 192.168.1.100
user: root
path: /opt/my-project
docker:
context: my-project-remote
# Optional: backup settings (defaults shown)
backup:
keep_count: 5 # Number of backups to retain
include_database: true # pg_dump before deploy
# Optional: health check after deploy (defaults shown)
health_check:
retries: 3 # Number of retry attempts
auto_rollback: true # Rollback on failed health check
| Field | Description |
|---|---|
server.host |
Server IP address or hostname |
server.user |
SSH user (default: root) |
server.path |
Application directory on server (default: /opt/{project-name}) |
docker.context |
Docker context name (used by generated Makefile deploy-* targets) |
backup.keep_count |
Number of backups to keep on server (default: 5) |
backup.include_database |
Include the database (SQLite snapshot or PostgreSQL dump) in backup (default: true) |
health_check.retries |
Number of health check attempts (default: 3) |
health_check.auto_rollback |
Auto-rollback on failed health check (default: true) |
All backup and health_check fields are optional, missing fields use defaults. Existing deploy.yml files work without changes.
Git-ignore deploy config
Add .aegis/ to your .gitignore to keep deployment config out of version control. deploy-init will remind you if it's missing.
Command Reference
aegis deploy-init
Initialize deployment configuration for a project.
Usage:
Options:
--host, -h TEXT, Server IP address or hostname (prompted if not provided)--user, -u TEXT, SSH user for deployment (default:root)--path, -p TEXT, Deployment path on server (default:/opt/{project-name})--project-path TEXT, Path to the project (default: current directory)
Examples:
# Interactive (prompts for host)
aegis deploy-init
# Specify host directly
aegis deploy-init --host 192.168.1.100
# Custom user and path
aegis deploy-init --host myserver.com --user deploy --path /srv/myapp
aegis deploy-setup
Provision a remote server for deployment. Run this once on a fresh server before your first deploy.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)--public-key PATH, Public key to install in the deploy user'sauthorized_keys(idempotent). Lets you skip the manualssh-copy-idstep.
What it does:
- Verifies SSH connectivity (adds host key if needed)
- Copies
scripts/server-setup.shto the server - Runs the setup script, which:
- Installs Docker and Docker Compose plugin
- Installs
uv(Python package manager) - Installs build tools (
build-essential/gcc) - Configures firewall (ports 22, 80, 443)
- Sets up Docker daemon with log rotation
- Creates application directory with proper permissions
- Applies system optimizations (file limits, TCP BBR)
- Under 4 GB of RAM, adds a swapfile (RAM-sized, capped at 2 GB) in
/etc/fstabwithvm.swappiness=10, unless the box already has swap
- If
--public-keywas passed, appends that key to the deploy user's~/.ssh/authorized_keys(idempotent — re-running with the same key is a no-op).
Examples:
Requires ingress component
The server-setup.sh script is generated when the ingress component is included. If you see "Server setup script not found", add ingress first: aegis add ingress.
aegis deploy-cd-setup
Wire up GitHub Actions continuous deployment in one command. Generates a dedicated SSH deploy key, installs the public key on your server, pushes the private key + host + user as repo secrets, and scaffolds .github/workflows/deploy.yml.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)--repo OWNER/NAME, GitHub repo (default: auto-detect fromgit remote get-url origin)--on-tag, Also trigger the deploy workflow onv*tag pushes (default:workflow_dispatchonly)--force, Overwrite existing secrets anddeploy.yml(use to rotate the deploy key)--dry-run, Print the plan without making any changes--keep-key PATH, Save a copy of the generated private key locally (default: no local copy — the key only lives in GitHub secrets)
Prerequisites:
aegis deploy-inithas been run (this command reads.aegis/deploy.ymlfor host/user)- The GitHub CLI (
gh) is installed and authenticated (gh auth login) - You can already SSH into the deploy server (the command uses your existing access to install the new CI key)
What it does:
- Detects the GitHub repo from
git remote get-url origin(or honors--repo) - Generates a fresh ed25519 keypair in a temp dir, no passphrase, comment
github-actions-deploy@OWNER/REPO - Appends the public key to the deploy user's
authorized_keyson the server (idempotent) - Pushes three secrets to the repo:
DEPLOY_SSH_KEY(private key),DEPLOY_HOST,DEPLOY_USER - Writes
.github/workflows/deploy.yml(workflow_dispatch by default;--on-tagadds the tag trigger) - Records the key fingerprint in
.aegis/deploy.ymlunderci.githubso re-runs detect "already configured" - Securely deletes the temp private key (overwrites then unlinks)
Examples:
aegis deploy-cd-setup # one-shot setup with workflow_dispatch trigger
aegis deploy-cd-setup --on-tag # also auto-deploy on v* tag pushes
aegis deploy-cd-setup --dry-run # see the plan without touching anything
aegis deploy-cd-setup --force # rotate the existing deploy key
Why a dedicated deploy key
Don't reuse your personal SSH key for CI. A dedicated key has a smaller blast radius (only deploys this one project), can be rotated without touching your other servers, and shows up as a distinct identity in auth.log. This command generates one for you.
Backing up the deploy key
GitHub Actions secrets are write-only — once the private key is uploaded, you can't read it back from the API or UI. By default, deploy-cd-setup uploads the key and securely deletes the local copy, so the only place it exists is inside GitHub.
If you want a local backup (for a password manager, disaster recovery, or to use the same key from another CI provider), pass --keep-key PATH at setup time:
If you already ran setup without --keep-key and want a copy now, rotate the key — there's no way to extract the existing one. Re-run with --force --keep-key PATH:
aegis deploy
Deploy (or redeploy) the application to the configured server.
Usage:
Options:
--build / --no-build, Build Docker images before deploying (default:--build)--backup / --no-backup, Create backup before deploying (default:--backup)--health-check / --no-health-check, Run health check after deploying (default:--health-check)--project-path TEXT, Path to the project (default: current directory)
What it does:
- Creates a backup of the current deployment (files, database, object storage; see
aegis deploy-backup) - Syncs files to the server via
rsync --delete, so a file removed from the project is removed from the server too. Only files git tracks are sent: untracked files (.env.backup-*, notes, local dumps) stay on your machine, and one an earlier deploy left on the server is removed. Outside git, or before anything is tracked, the directory is sent as it is - Copies
.envfile separately (excluded from rsync for safety) - Stops existing services with
docker compose down - Builds and starts services with production compose overrides
- Restarts Traefik if the ingress component is present (ensures container re-discovery)
- Runs health check against
/health/endpoint (through HTTPS for the app's name when TLS is on) - Auto-rollback if health check fails, restores the backup from step 1
- Records the deploy in the app's deploy history when the project has the deploy component and a database: who deployed, from where, the health result, the backup and any rollback
Excluded from sync:
Files and directories excluded from the rsync transfer. The sync deletes, and rsync never deletes an excluded path, so this list also protects server-side state from removal:
.git, Git history__pycache__, Python cache.venv, Virtual environment*.pyc, Compiled Python files.pytest_cache, Test cache.ruff_cache, Linter cachedata/, Local database files.envand.env.deploy, Environment files (copied separately).aegis/, Deploy configurationbackups/, Server-side backupsnode_modules/, Node dependenciestraefik/acme/, Let's Encrypt certificate store (deleting it forces re-issuance and can hit rate limits)
Examples:
# Full deploy with build (backup + health check enabled by default)
aegis deploy
# Skip image rebuild (faster, uses cached images)
aegis deploy --no-build
# Deploy without backup or health check (original behavior)
aegis deploy --no-backup --no-health-check
aegis deploy-backup
Create a backup of the currently deployed application on the remote server.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)
What it does:
- Creates a timestamped backup directory on the server,
backups/<timestamp>/ - Snapshots a SQLite database into
data/through SQLite's online backup API, run in the webserver container: a consistent copy while the app keeps writing. If the snapshot fails, the live file is copied with the rest instead - Copies all deployed files into
files/via server-sidersync(the code and the server's.env) - Dumps the PostgreSQL database to
db_backup.sqlif a postgres container is running - Archives the object store (the
storage-datavolume) tostorage.tar.gz - Prunes old backups beyond the retention count (default: 5)
These backups stay on the server, for rollbacks.
Off the server: backups on the server die with it. A project with the deploy component, a database and the scheduler sends its nightly backup elsewhere instead: at 02:00 the app backs up its database (and its stored files, on the filesystem store), and at 02:30 its off-site job sends that backup with restic to S3-compatible storage (DigitalOcean Spaces, Backblaze B2, AWS S3). restic encrypts it before it leaves, and keeps 7 daily, 4 weekly and 6 monthly copies.
It does nothing until these four are set, in .env.deploy or in
Overseer > Secrets:
OFFSITE_BACKUP_REPOSITORY=s3:https://nyc3.digitaloceanspaces.com/<bucket>/<project>
OFFSITE_BACKUP_PASSWORD=<long random password>
OFFSITE_BACKUP_ACCESS_KEY=<Spaces key id>
OFFSITE_BACKUP_SECRET_KEY=<Spaces key secret>
Keep the password and the app's ENCRYPTION_KEY somewhere else too (a
password manager): without them the copies cannot be read, nor the app's
encrypted fields. Overseer > Deployments > Backups shows the last copy,
and History lists each one.
To check that a copy restores, run aegis deploy-restore-drill (or
Rehearse a restore in the Overseer). It restores the newest copy on
this machine and runs the app's own checks on it: the database is whole,
the app starts on it, every stored secret decrypts, every stored file is
there. Nothing on the server changes; the result is recorded there.
To restore by hand, anywhere with restic and the same four values (as
RESTIC_REPOSITORY, RESTIC_PASSWORD, AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY): restic snapshots, then
restic restore latest --target <dir>.
Examples:
aegis deploy-backups
List available deployment backups on the remote server.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)
Example output:
Backups on 143.198.20.208 (3 total):
Timestamp Size Database
──────────────────────── ──────────── ──────────
2026-03-11_183045 42M yes
2026-03-10_120000 41M no
2026-03-09_090000 40M yes
Rollback with: aegis deploy-rollback --backup <timestamp>
aegis deploy-rollback
Rollback to a previous deployment backup.
Usage:
Options:
--backup, -b TEXT, Backup timestamp to rollback to (default: latest)--project-path TEXT, Path to the project (default: current directory)
What it does:
- Stops all running services
- Restores files from the backup snapshot (skipped for a backup without
files/, such as one restored from off-box) - Restores the SQLite snapshot and the object store if the backup has them
- Restores the PostgreSQL database if a dump exists in the backup
- Rebuilds and starts all services
- Restarts Traefik if present
Examples:
# Rollback to the most recent backup
aegis deploy-rollback
# Rollback to a specific backup
aegis deploy-rollback --backup 2026-03-11_183045
aegis deploy-logs
View logs from the deployed application.
Usage:
Options:
--follow / --no-follow, -f, Follow log output in real-time (default:--follow)--service, -s TEXT, Show logs for a specific service--project-path TEXT, Path to the project (default: current directory)
Examples:
# Follow all service logs
aegis deploy-logs
# View logs without following
aegis deploy-logs --no-follow
# View logs for a specific service
aegis deploy-logs --service webserver
aegis deploy-logs -s traefik
aegis deploy-logs -s redis
aegis deploy-status
Check the status of all deployed services.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)
Examples:
Shows the output of docker compose ps on the remote server, including container status, ports, and health.
aegis deploy-stop
Stop all deployed services.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)
Examples:
Runs docker compose down on the remote server.
aegis deploy-restart
Restart all deployed services.
Usage:
Options:
--project-path TEXT, Path to the project (default: current directory)
Examples:
Runs docker compose restart on the remote server.
aegis deploy-shell
Open an interactive shell in a deployed container.
Usage:
Options:
--service, -s TEXT, Service to connect to (default:webserver)--project-path TEXT, Path to the project (default: current directory)
Examples:
# Shell into the webserver (default)
aegis deploy-shell
# Shell into Redis
aegis deploy-shell --service redis
# Shell into Traefik
aegis deploy-shell -s traefik
Opens a /bin/bash session inside the container via SSH.
TLS/HTTPS with Let's Encrypt
Enable automatic HTTPS on a deployed project using aegis ingress-enable.
aegis ingress-enable
Configure TLS (HTTPS) with automatic Let's Encrypt certificates.
Usage:
Options:
--domain, -d TEXT, Domain name for the TLS certificate (e.g.,example.com)--email, -e TEXT, Email for Let's Encrypt certificate notifications--project-path, -p TEXT, Path to the project (default: current directory)--yes, -y, Skip confirmation prompts
What it does:
- Adds the ingress component if not already present
- Configures
traefik/traefik.ymlwith HTTPS entrypoint and ACME resolver - Updates Docker Compose labels for TLS routing
- Adds
traefik-letsencryptDocker volume for certificate storage - Configures HTTP → HTTPS redirect
Examples:
# Interactive (prompts for domain and email)
aegis ingress-enable
# Fully specified
aegis ingress-enable --domain example.com --email admin@example.com
# Non-interactive
aegis ingress-enable -d example.com -e admin@example.com -y
# On a different project
aegis ingress-enable -p ../my-project -d example.com
Full TLS Workflow
# 1. Enable TLS on your project
aegis ingress-enable --domain example.com --email admin@example.com
# 2. Deploy to your server
aegis deploy
# 3. Ensure DNS is configured
# Point your domain's A record to your server IP
# 4. Certificates auto-provision on first request
# Traefik handles Let's Encrypt automatically
After deployment:
http://example.comredirects tohttps://example.com- Certificates renew automatically before expiration
- Certificate data persists in the
traefik-letsencryptDocker volume
DNS must be configured first
Let's Encrypt validates domain ownership via HTTP challenge. Your domain's A record must point to the server IP before requesting certificates.
Deployment Workflow
First Deployment
# 1. Create project with ingress
aegis init my-app --components ingress
cd my-app
# 2. Configure and test locally
uv sync && cp .env.example .env
make serve # Verify everything works
# 3. Set up deployment
aegis deploy-init --host your-server-ip
aegis deploy-setup
# 4. Deploy
aegis deploy
Subsequent Deployments
# Make changes, then redeploy (auto-backup + health check)
aegis deploy
# Or skip rebuild for config-only changes
aegis deploy --no-build
Backup and Rollback
Every aegis deploy automatically creates a backup before making changes. If the health check fails after deploy, the previous version is restored automatically.
# Create a manual backup
aegis deploy-backup
# List available backups
aegis deploy-backups
# Rollback to the most recent backup
aegis deploy-rollback
# Rollback to a specific backup
aegis deploy-rollback --backup 2026-03-11_183045
How backups work:
- Backups are stored on the server at
{deploy_path}/backups/{timestamp}/ - Each backup contains a full snapshot of deployed files
- PostgreSQL databases are dumped via
pg_dump(SQLite files are included automatically) - Old backups are pruned automatically (default: keep last 5)
- Backups are never synced or overwritten during deploys
Auto-rollback flow:
aegis deploy
→ backup current state
→ sync new code
→ docker compose up
→ health check (GET /health/)
→ pass: done
→ fail: restore backup automatically
Monitoring
# Check service health
aegis deploy-status
# Follow logs in real-time
aegis deploy-logs
# Check a specific service
aegis deploy-logs --service webserver
# Debug inside a container
aegis deploy-shell
Next Steps
- Ingress Component, Traefik configuration, routing, and admin protection
- CLI Reference, Complete command reference
- Evolving Your Stack, Adding components and deploying over time