$ aoox

DocsInfrastructure

Remote servers

Connect another server over SSH as an additional deploy target — no agent, just the Docker CLI on the target server.

Transport
SSH → docker system dial-stdio
Needed on the server
sshd + Docker CLI, no agent
Who
Owner/admin registers it; anyone can select it

When to use a remote server

  • A heavy application that doesn't fit on the aoox host.
  • Splitting the load: panel + database on one VPS, applications on another.
  • A different region/location for latency.

One aoox instance is still one panel; a remote server just adds another place for containers to run.

Prerequisites on the target server

  • An SSH server running and reachable from the aoox host (port 22 or another).
  • Docker installed, with the SSH user allowed to use it:
On the target server
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy      # if not root
docker system dial-stdio --help     # must exist (Docker 20.10+)
  • Just the CLI and daemon — no SSH port forwarding needed. A hardened sshd with AllowTcpForwarding no still works fine.
  • No need to expose the Docker port (2375/2376) to the network.
  • For a domain served directly on that server: ports 80/443 must be free (see proxy per server).

How it works

  1. 01sshone ssh2 session from the API to the server (lazy, reconnects)
  2. 02dial-stdioevery Docker API request runs through docker system dial-stdio
  3. 03buildthe server's daemon clones and builds the image
  4. 04runthe container runs on the server; logs stream through the same tunnel

This is the same approach as docker -H ssh://user@host: the API talks to the remote daemon as if it were local, over the stdio of a docker process on the server. One SSH session per server is cached and reconnected if it drops.

Adding a server

  1. Infrastructure → Remote servers → New remote server (owner/admin)

    FieldNotes
    NameA label, e.g. app-sg-1.
    Host / Port / UsernameThe SSH address, e.g. 203.0.113.10, 22, root or a user in the docker group.
    Private key (optional)Leave blank to use the aoox platform key (recommended). Paste a PEM/OpenSSH key to bring your own — it's stored encrypted and never shown again.
  2. Authorize the platform key (if not using your own)

    The Remote servers card shows the platform's public key and the command to run once on the target server as that SSH user — aoox can't write to another host's authorized_keys:

    mkdir -p ~/.ssh && chmod 700 ~/.ssh
    touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
    echo 'ssh-ed25519 AAAA… aoox' >> ~/.ssh/authorized_keys

    The platform key is generated automatically (ed25519) the first time it's needed and stored in ./secrets on the aoox host — one key for all servers and the terminal.

  3. Test the connection

    The test button opens a brief shell, then checks Docker over the tunnel. Results:

    ResultMeaning
    OK + Docker versionReady to use.
    Auth rejected + authorization commandThe key isn't in authorized_keys yet — run the command shown.
    Connected, Docker errordocker isn't on that user's PATH, the user isn't in the docker group, or the daemon is down.

Deploying to a remote server

In the application form, pick Server → the target server (the select only appears if a server is registered; members see an empty list because the server endpoint is restricted to owner/admin). The application shows the server's name as a badge.

AspectRemote server
BuildOn the target server's daemon — Dockerfile, Nixpacks, static sites (the helper image is built there too).
RegistryNo push. The image stays on the server under the ref aoox/<project>/<app>:<tag>; rollback reuses whatever's still there.
Domain / TraefikYes — provision a proxy on that server (the Proxy (Traefik) on this server card), then add domains as usual. A host port is still an option too.
Blue/greenYes, if the server has a proxy and the app has no host port; without a proxy it's a plain replace.
Container logsAvailable (realtime, over the tunnel).
CPU/RAM metricsNo — the sampler only reads the aoox host's daemon.
Env & database referencesEnv still applies; ${{database…}} references resolve to an internal host that's unreachable from another server — use an external connection instead.
Mounts & volume backupsYes — volumes, files, and volume backups (including S3 upload/download) run on that server's daemon.
JobsYes, on that server's daemon.
Container-down notificationsLocal host only.
PR previewsNo (no host/domain).
Disk maintenanceOld deployment images are also pruned on the remote server; dangling images/build cache are local only.

Example: app on a remote server + database on the host

Application env (remote server)
# not ${{database.app-db.url}} — the internal host is unreachable from another server
DATABASE_URL=postgresql://app:PASSWORD@203.0.113.5:15432/app_db
  • Set a host port on the database (e.g. 15432) and restrict that port on the host's firewall to the remote server's IP.
  • Copy the password from the database's Overview tab (unmask → copy).

Proxy & domain on a remote server

Each server can run its own Traefik. On the server's card, under Proxy (Traefik) on this server: set the HTTP/HTTPS ports (default 80/443) and an ACME email if you want a Let's Encrypt certificate, then provision.

  • Once the proxy exists, applications on that server get Traefik labels using that server's settings — the Domain tab works just like it does on the host.
  • The domain's DNS must point to that server's IP, not the aoox host. The DNS check compares against the registered server's host.
  • Ports 80/443 on that server must be free; ACME HTTP-01 needs port 80 open to the internet.
  • Without a server proxy, the application can still be reached via a host port, as before.

Terminal to a remote server

The Terminal page has a target selector: aoox host or one of the servers. The target is bound to the ticket by the API, not sent from the browser. See Web terminal.

Metrics, notifications and server status

Applications on a remote server are monitored through the same SSH tunnel deploys use:

  • Metrics: an application's CPU, memory and network show on the Deploy tab (1 hour live, plus 24 h/7 d/30 d history) and in the project's resource usage card, like local applications. Remote servers are sampled every 30 seconds (local every 15) because each sample is two stats calls over SSH.
  • Container down: the server's die events are streamed through the tunnel, with the same rules as on the host (5-second grace, ignored when replaced by a deploy/stopped/ exit 0, at most 1 message per container per 10 minutes). The message names the server.
  • Server status: every minute the API asks the server for its container list. The server list shows an reachable/unreachable/not checked badge, when it was checked, since when it has been down, and how many containers are monitored — read from the last result, so opening the page calls no server.
  • Server unreachable: a notification is sent after two failed checks in a row (about 2 minutes, so a flaky link wakes nobody up), repeated at most once an hour while it stays down, and one "reachable again" message (with the downtime) on recovery. Toggle: remote server unreachable in Notifications.
Limits
The disk warning, disk cleanup/retention, certificate monitoring and the DNS check still cover the local host only. A newly added server starts being monitored within about 30 seconds; while a server is unreachable its metrics are empty (stale numbers are not shown).

Removing a server

A server that still has applications on it can't be deleted (409). Delete or move its applications first (change Server in Settings → the next deploy builds on the new target; the container on the old server isn't removed automatically).

Common pitfalls

SymptomCause & fix
Test: auth failedThe key hasn't been authorized yet, or the username is wrong. Run the authorization command as that same user.
Test: docker: command not foundDocker isn't installed, or the non-interactive shell's PATH doesn't include it (snap). Install via get.docker.com.
Test: permission denied while trying to connect to the Docker daemonThe user isn't in the docker group. usermod -aG docker <user>, then log in again.
Deploy succeeds but isn't reachableThe host port is empty and the server has no proxy yet, or the server's firewall is blocking that port.
The application can't reach the databaseIt's using an internal reference/host. Use an external connection + the database's host port instead.
Nixpacks/static: the first build takes foreverThe helper image and base image are pulled on that server (once per server).
Security
The target server's SSH host key is not verified — vulnerable to MITM on untrusted networks. Use a private network/VPN between servers when possible. SSH credentials give aoox Docker-root-equivalent access on that server.

Next steps

Found a mistake? Edit this page on GitHub ↗