Skip to content
Nautilo Documentation
Administrator guideDeploy Nautilo

Deploy to a Linux server

Prepare DNS, SSH, Docker, firewall boundaries, and automatic HTTPS for a Nautilo VPS.

This is the persistent self-hosted path: one public DNS name, one SSH-reachable Linux machine, and one Docker Compose lifecycle owned by your circle. A VPS from any provider can work; Nautilo does not have a provider-specific VPS driver.

Install the current signed stable CLI through the Administrator quickstart. Before creating infrastructure, confirm nautilo profile add --help lists the remote Compose, SSH, domain, and HTTPS options used below, and nautilo deploy --help lists the selected owner-mode options. Update the signed CLI if they are missing.

Decide the public names first

The generated proxy has two HTTPS sites:

  • circle.example.org routes to the Nautilo Server; and
  • auth.circle.example.org routes to Logto.

Replace both synthetic names with names in a public DNS zone you control. Create A records for both names when the VPS has a reachable IPv4 address. Create AAAA records only when IPv6 reaches that same VPS; remove stale AAAA records rather than leaving some clients to fail over IPv6.

Wait until public recursive resolvers return the intended address for both names. Caddy's default HTTP-01 and TLS-ALPN-01 ACME challenges follow public DNS and reach ports 80 or 443. Nautilo and Caddy do not change your DNS zone.

dig +short A circle.example.org
dig +short AAAA circle.example.org
dig +short A auth.circle.example.org
dig +short AAAA auth.circle.example.org

Do not continue while a returned address points to another host.

Prepare the host before creating a profile

Use a maintained Linux distribution supported by Docker Engine. Follow Docker's official installation steps for the host rather than an unofficial convenience script. The target needs:

  • Docker Engine and the Docker Compose plugin running;
  • a dedicated, noninteractive SSH key already authorized for an operator account;
  • permission for that account to use Docker; and
  • an owner-writable deployment directory outside any web root.

On the host, an administrator can create that directory once:

sudo install -d -o operator -g operator -m 700 /srv/nautilo

The SSH account plus Docker access is infrastructure-root-equivalent authority. Nautilo application roles cannot revoke it. Restrict, rotate, and remove that access at the host and VPS-provider layers.

Establish the firewall boundary

Allow inbound traffic as follows before deployment:

PortSourcePurpose
SSH portNamed administrator networks onlyNautilo lifecycle operations
TCP 80Public InternetHTTP redirect and ACME HTTP-01 validation
TCP 443Public InternetNautilo HTTPS and ACME TLS-ALPN-01 validation

Block every other inbound port from the Internet. In particular, do not expose the generated direct Server or Logto ports, database ports, or Logto administrator port. The public path is through Caddy on 80/443.

Docker warns that published container ports can bypass rules managed only by ufw or firewalld. Enforce the public allowlist at the VPS provider or upstream firewall, and make any host rules Docker-aware. Recheck the effective policy after Docker starts; a configured host firewall alone is not proof that a published port is private.

Keep outbound DNS and HTTPS available for image pulls and certificate issuance. Do not add Compose ACME environment flags or install a separate certificate-renewal job.

Pin the SSH host identity

Obtain the expected host-key fingerprint through a trusted provider channel. Then collect the presented key for comparison:

export NAUTILO_SSH_HOST='circle.example.org'
export NAUTILO_SSH_IDENTITY_FILE="$HOME/.ssh/nautilo_vps_operator"
export NAUTILO_SSH_KNOWN_HOSTS_FILE="$HOME/.ssh/nautilo_vps_known_hosts"

ssh-keyscan -t ed25519 -H "$NAUTILO_SSH_HOST" > /tmp/nautilo-vps.known-hosts
ssh-keygen -lf /tmp/nautilo-vps.known-hosts

Stop if the fingerprints differ. Do not replace an existing entry until a key change is independently verified. After a match, install the entry and test the exact noninteractive path Nautilo will use:

install -m 600 /tmp/nautilo-vps.known-hosts "$NAUTILO_SSH_KNOWN_HOSTS_FILE"
rm /tmp/nautilo-vps.known-hosts

ssh -o BatchMode=yes -o IdentitiesOnly=yes -l operator \
  -i "$NAUTILO_SSH_IDENTITY_FILE" \
  -o StrictHostKeyChecking=yes \
  -o UserKnownHostsFile="$NAUTILO_SSH_KNOWN_HOSTS_FILE" \
  "$NAUTILO_SSH_HOST" \
  'docker info && docker compose version && test -w /srv/nautilo'

Resolve Docker permission, missing Compose, or directory ownership now. Do not fall back to an interactive password or disable host-key checking.

Create the remote HTTPS profile

nautilo profile add circle-server \
  --transport=remote \
  --lifecycle=compose \
  --instance-id=circle \
  --ssh-host="$NAUTILO_SSH_HOST" \
  --ssh-user=operator \
  --ssh-identity-file="$NAUTILO_SSH_IDENTITY_FILE" \
  --ssh-known-hosts-file="$NAUTILO_SSH_KNOWN_HOSTS_FILE" \
  --remote-path=/srv/nautilo \
  --domain=circle.example.org \
  --base-url=https://circle.example.org \
  --https=letsencrypt \
  [email protected] \
  --yes

The --domain value is the Server hostname. Nautilo derives the Logto host as auth.<domain> and generates both Caddy sites. The base URL must use the exact Server hostname over HTTPS.

For repeated pre-production testing, create a separate disposable profile with --acme-staging so failed experiments do not consume production CA limits. Staging certificates are not browser-trusted and do not prove production HTTPS.

Deploy with an explicit owner mode

For an interactive first-owner browser claim:

nautilo deploy \
  --profile circle-server \
  --owner-mode claim \
  --finish guide

The claim URL is valid only for the exact HTTPS deployment origin. Check the hostname and certificate before entering the one-time capability. Require owner-bound, save the recovery codes, and sign in again as the new owner.

Alternatively, a compatible signed CLI can create the first owner from a protected file. Follow the exact automated Compose setup steps, including explicit --owner-mode config, --owner-config, and --owner-result. Do not switch between owner modes after an interrupted run.

Bare deploy verifies the signed stable Server release and selects its immutable runtime digest before preparing deployment state. To deploy a specific release, add --image ghcr.io/agentsea/nautilo-runtime@sha256:<64-lowercase-hex-digest>. Profiles do not store image selection.

Verify routing, TLS, and the Server

nautilo status --profile circle-server
curl --fail --silent --show-error https://circle.example.org/health
curl --fail --silent --show-error \
  https://auth.circle.example.org/oidc/.well-known/openid-configuration

openssl s_client -connect circle.example.org:443 \
  -servername circle.example.org </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

openssl s_client -connect auth.circle.example.org:443 \
  -servername auth.circle.example.org </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

The two curl commands perform normal hostname and trust validation; never add --insecure. The openssl output is for certificate identity and date inspection, not chain validation. From a normal client network, open the exact HTTPS Server URL with no certificate warning, confirm the expected Server identity, sign in as the owner, and complete one useful model-backed action. A running container or a successful TLS handshake alone is not Server acceptance.

Certificate issuance troubleshooting

If HTTPS is not ready, preserve the profile and deployment state. Do not keep recreating the Server or repeatedly request production certificates.

  1. Query public DNS for both the Server and auth names. Correct wrong or stale A/AAAA records and allow for their configured TTLs.

  2. Confirm the provider/upstream firewall allows public TCP 80 and 443 to this host, and that no NAT rule sends either port elsewhere.

  3. Check whether another host process already owns those ports:

    ssh -o BatchMode=yes -l operator \
      -i "$NAUTILO_SSH_IDENTITY_FILE" \
      -o StrictHostKeyChecking=yes \
      -o UserKnownHostsFile="$NAUTILO_SSH_KNOWN_HOSTS_FILE" \
      "$NAUTILO_SSH_HOST" 'ss -ltnp | grep -E "(:80|:443)[[:space:]]"'
  4. Inspect Caddy's managed logs without printing environment files:

    nautilo logs --profile circle-server --service caddy
  5. After fixing DNS, firewall, or a port conflict, leave Caddy running. It retries certificate management with backoff. Re-run the TLS checks above; do not edit the generated Caddyfile or add an ad hoc ACME client.

Renewal and certificate custody

Caddy obtains and renews managed certificates automatically. The generated Compose overlay persists Caddy's data and configuration; keep those volumes writable and durable, keep both DNS names directed at the host, and leave the public validation ports reachable. Deleting Caddy storage forces new issuance and can trigger CA limits.

Check the certificate dates and names periodically with the openssl commands above and alert before expiry. If renewal fails, use the same DNS, port, storage, and Caddy-log checks. The domain registration, DNS account, VPS, SSH/Docker authority, and Caddy certificate storage remain operator custody; Nautilo application ownership is not a substitute for any of them.

Day-two release changes use nautilo upgrade --profile circle-server, which wraps backup, runtime acceptance, and automatic rollback around replacement. Do not use deploy as the ordinary upgrade command.