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:
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 nostill 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
- 01sshone ssh2 session from the API to the server (lazy, reconnects)
- 02dial-stdioevery Docker API request runs through docker system dial-stdio
- 03buildthe server's daemon clones and builds the image
- 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
Infrastructure → Remote servers → New remote server (owner/admin)
Field Notes Name A label, e.g. app-sg-1. Host / Port / Username The SSH address, e.g. 203.0.113.10,22,rootor 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. 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_keysThe platform key is generated automatically (ed25519) the first time it's needed and stored in
./secretson the aoox host — one key for all servers and the terminal.Test the connection
The test button opens a brief shell, then checks Docker over the tunnel. Results:
Result Meaning OK + Docker version Ready to use. Auth rejected + authorization command The key isn't in authorized_keys yet — run the command shown. Connected, Docker error docker 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.
| Aspect | Remote server |
|---|---|
| Build | On the target server's daemon — Dockerfile, Nixpacks, static sites (the helper image is built there too). |
| Registry | No push. The image stays on the server under the ref aoox/<project>/<app>:<tag>; rollback reuses whatever's still there. |
| Domain / Traefik | Yes — 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/green | Yes, if the server has a proxy and the app has no host port; without a proxy it's a plain replace. |
| Container logs | Available (realtime, over the tunnel). |
| CPU/RAM metrics | No — the sampler only reads the aoox host's daemon. |
| Env & database references | Env still applies; ${{database…}} references resolve to an internal host that's unreachable from another server — use an external connection instead. |
| Mounts & volume backups | Yes — volumes, files, and volume backups (including S3 upload/download) run on that server's daemon. |
| Jobs | Yes, on that server's daemon. |
| Container-down notifications | Local host only. |
| PR previews | No (no host/domain). |
| Disk maintenance | Old 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
# 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
statscalls over SSH. - Container down: the server's
dieevents 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.
Removing a server
Common pitfalls
| Symptom | Cause & fix |
|---|---|
| Test: auth failed | The key hasn't been authorized yet, or the username is wrong. Run the authorization command as that same user. |
| Test: docker: command not found | Docker 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 daemon | The user isn't in the docker group. usermod -aG docker <user>, then log in again. |
| Deploy succeeds but isn't reachable | The 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 database | It's using an internal reference/host. Use an external connection + the database's host port instead. |
| Nixpacks/static: the first build takes forever | The helper image and base image are pulled on that server (once per server). |
Next steps
Found a mistake? Edit this page on GitHub ↗