Skip to content

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:

aegis deploy-init [OPTIONS]

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:

aegis deploy-setup [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)
  • --public-key PATH, Public key to install in the deploy user's authorized_keys (idempotent). Lets you skip the manual ssh-copy-id step.

What it does:

  1. Verifies SSH connectivity (adds host key if needed)
  2. Copies scripts/server-setup.sh to the server
  3. 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/fstab with vm.swappiness=10, unless the box already has swap
  4. If --public-key was passed, appends that key to the deploy user's ~/.ssh/authorized_keys (idempotent — re-running with the same key is a no-op).

Examples:

aegis deploy-setup
aegis deploy-setup --public-key ~/.ssh/id_ed25519.pub

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:

aegis deploy-cd-setup [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)
  • --repo OWNER/NAME, GitHub repo (default: auto-detect from git remote get-url origin)
  • --on-tag, Also trigger the deploy workflow on v* tag pushes (default: workflow_dispatch only)
  • --force, Overwrite existing secrets and deploy.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-init has been run (this command reads .aegis/deploy.yml for 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:

  1. Detects the GitHub repo from git remote get-url origin (or honors --repo)
  2. Generates a fresh ed25519 keypair in a temp dir, no passphrase, comment github-actions-deploy@OWNER/REPO
  3. Appends the public key to the deploy user's authorized_keys on the server (idempotent)
  4. Pushes three secrets to the repo: DEPLOY_SSH_KEY (private key), DEPLOY_HOST, DEPLOY_USER
  5. Writes .github/workflows/deploy.yml (workflow_dispatch by default; --on-tag adds the tag trigger)
  6. Records the key fingerprint in .aegis/deploy.yml under ci.github so re-runs detect "already configured"
  7. 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:

aegis deploy-cd-setup --keep-key ~/.ssh/aegis_deploy_ci

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-cd-setup --force --keep-key ~/.ssh/aegis_deploy_ci
This generates a fresh key, replaces the public key on the server, overwrites the GitHub secret, and saves a local copy. The previous key is rotated out.


aegis deploy

Deploy (or redeploy) the application to the configured server.

Usage:

aegis deploy [OPTIONS]

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:

  1. Creates a backup of the current deployment (files, database, object storage; see aegis deploy-backup)
  2. 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
  3. Copies .env file separately (excluded from rsync for safety)
  4. Stops existing services with docker compose down
  5. Builds and starts services with production compose overrides
  6. Restarts Traefik if the ingress component is present (ensures container re-discovery)
  7. Runs health check against /health/ endpoint (through HTTPS for the app's name when TLS is on)
  8. Auto-rollback if health check fails, restores the backup from step 1
  9. 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 cache
  • data/, Local database files
  • .env and .env.deploy, Environment files (copied separately)
  • .aegis/, Deploy configuration
  • backups/, Server-side backups
  • node_modules/, Node dependencies
  • traefik/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:

aegis deploy-backup [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)

What it does:

  1. Creates a timestamped backup directory on the server, backups/<timestamp>/
  2. 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
  3. Copies all deployed files into files/ via server-side rsync (the code and the server's .env)
  4. Dumps the PostgreSQL database to db_backup.sql if a postgres container is running
  5. Archives the object store (the storage-data volume) to storage.tar.gz
  6. 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-backup


aegis deploy-backups

List available deployment backups on the remote server.

Usage:

aegis deploy-backups [OPTIONS]

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:

aegis deploy-rollback [OPTIONS]

Options:

  • --backup, -b TEXT, Backup timestamp to rollback to (default: latest)
  • --project-path TEXT, Path to the project (default: current directory)

What it does:

  1. Stops all running services
  2. Restores files from the backup snapshot (skipped for a backup without files/, such as one restored from off-box)
  3. Restores the SQLite snapshot and the object store if the backup has them
  4. Restores the PostgreSQL database if a dump exists in the backup
  5. Rebuilds and starts all services
  6. 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:

aegis deploy-logs [OPTIONS]

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:

aegis deploy-status [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)

Examples:

aegis deploy-status

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:

aegis deploy-stop [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)

Examples:

aegis deploy-stop

Runs docker compose down on the remote server.


aegis deploy-restart

Restart all deployed services.

Usage:

aegis deploy-restart [OPTIONS]

Options:

  • --project-path TEXT, Path to the project (default: current directory)

Examples:

aegis deploy-restart

Runs docker compose restart on the remote server.


aegis deploy-shell

Open an interactive shell in a deployed container.

Usage:

aegis deploy-shell [OPTIONS]

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:

aegis ingress-enable [OPTIONS]

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:

  1. Adds the ingress component if not already present
  2. Configures traefik/traefik.yml with HTTPS entrypoint and ACME resolver
  3. Updates Docker Compose labels for TLS routing
  4. Adds traefik-letsencrypt Docker volume for certificate storage
  5. 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.com redirects to https://example.com
  • Certificates renew automatically before expiration
  • Certificate data persists in the traefik-letsencrypt Docker 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

Was this page helpful?