Server Installation

Haloy installs two server daemons:

  • haloyd manages deployments, Docker discovery, health checks, API access, certificates, and routing snapshots.
  • haloy-proxy owns ports 80 and 443, terminates HTTPS, and keeps serving traffic while haloyd restarts or upgrades.

After running the installation script, you’ll need to configure your server with a domain for remote access.

Prerequisites

  • Linux server (Debian/Ubuntu, RHEL/CentOS, Alpine, or other distributions)
  • Docker installed and running (can be auto-installed)
  • Root or sudo access
  • A domain or subdomain pointing to your server (for HTTPS API access)

Quick Install (with configuration)

If you already have a domain pointing to your server:

curl -fsSL https://sh.haloy.dev/install-haloyd.sh | API_DOMAIN=api.example.com sh
wget -qO- https://sh.haloy.dev/install-haloyd.sh | API_DOMAIN=api.example.com sh

Standard Install (configure after)

curl -fsSL https://sh.haloy.dev/install-haloyd.sh | sh
wget -qO- https://sh.haloy.dev/install-haloyd.sh | sh

Note: Server-side examples in this guide assume you’re logged in as root. If you’re using a sudo user, use | sudo sh for the install command, then run sudo -i before following the remaining server commands.

After installation, the script will display your server’s public IP address. You’ll need to:

1. Point your domain to the server

Create a DNS A record pointing your chosen domain to the server’s IP address.

Cloudflare users: Set the proxy status to “DNS only” (grey cloud), or ensure your SSL/TLS encryption mode is set to “Full (Strict)”. See Troubleshooting if you experience redirect loops.

This domain is for the Haloy server API. Application domains are configured separately in haloy.yaml; see Domain Configuration.

2. Configure haloy

haloyd config set api-domain YOUR_DOMAIN systemctl restart haloyd

Alpine Linux (OpenRC): Use rc-service haloyd restart instead of systemctl restart.

3. Add the server to your local CLI

On your local machine, run the command shown at the end of installation:

haloy server add YOUR_DOMAIN YOUR_API_TOKEN

You can retrieve the API token later by running on the server:

haloyd config get api-token

Options

The install script accepts both command-line flags and environment variables:

FlagEnvironment VariableDescription
--api-domain=DOMAINAPI_DOMAINDomain for the haloy API (e.g., api.example.com)
--version=VERSIONVERSIONInstall a specific version (default: latest)
--skip-startSKIP_START=trueDon’t start the services after installation
--skip-docker-installSKIP_DOCKER_INSTALL=trueSkip automatic Docker installation

Examples:

curl -fsSL https://sh.haloy.dev/install-haloyd.sh | sh -s -- --api-domain=api.example.com
curl -fsSL https://sh.haloy.dev/install-haloyd.sh | VERSION=v0.1.0 sh

Docker Installation

If Docker is not installed on your server, the install script will automatically install it for you. This is the default behavior.

If you prefer to install Docker manually or use a custom Docker installation, you can skip automatic installation:

curl -fsSL https://sh.haloy.dev/install-haloyd.sh | sh -s -- --skip-docker-install

Standalone Docker Install Script

You can also install Docker separately using the standalone script:

curl -fsSL https://sh.haloy.dev/install-docker.sh | sh

This script automatically detects your Linux distribution and installs Docker accordingly:

  • Alpine Linux: Uses apk add docker docker-cli-compose
  • Ubuntu, Debian, CentOS, Fedora, RHEL, and others: Uses the official Docker installation script from get.docker.com

The script enables and starts the Docker daemon automatically.

Manual Installation

If you prefer to install manually or the script doesn’t work for your system:

1. Install Server Binaries

Download and install the binaries:

# Download latest release (adjust architecture as needed: amd64, arm64) curl -fsSL -o /usr/local/bin/haloyd https://releases.haloy.dev/haloyd-linux-amd64 curl -fsSL -o /usr/local/bin/haloy-proxy https://releases.haloy.dev/haloy-proxy-linux-amd64 chmod +x /usr/local/bin/haloyd chmod +x /usr/local/bin/haloy-proxy

For OpenRC or SysVinit installs, grant the proxy binary permission to bind ports 80 and 443. Systemd installs use the service unit capability instead.

setcap cap_net_bind_service=+ep /usr/local/bin/haloy-proxy

2. Create System User

# Create haloy user and add to docker group useradd -r -s /bin/false haloy usermod -aG docker haloy

3. Initialize haloyd

Run the init command to create configuration and directories:

haloyd init --api-domain haloy.yourserver.com

This creates:

  • /etc/haloy/haloyd.yaml - Daemon configuration
  • /etc/haloy/.env - API token
  • /var/lib/haloy/ - Data directory
  • haloy Docker network

4. Install Service

Systemd

cat > /etc/systemd/system/haloy-proxy.service << 'EOF' [Unit] Description=Haloy Proxy After=network-online.target Wants=network-online.target [Service] Type=simple User=haloy Group=haloy ExecStart=/usr/local/bin/haloy-proxy serve Restart=always RestartSec=5 Environment=HALOY_DATA_DIR=/var/lib/haloy # Security hardening NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ReadWritePaths=/var/lib/haloy CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictSUIDSGID=true LimitNOFILE=65536 [Install] WantedBy=multi-user.target EOF cat > /etc/systemd/system/haloyd.service << 'EOF' [Unit] Description=Haloy Daemon After=network-online.target docker.service haloy-proxy.service Requires=docker.service Wants=network-online.target haloy-proxy.service [Service] Type=simple User=haloy Group=haloy ExecStart=/usr/local/bin/haloyd serve Restart=always RestartSec=5 Environment=HALOY_DATA_DIR=/var/lib/haloy Environment=HALOY_CONFIG_DIR=/etc/haloy # Security hardening NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ReadWritePaths=/var/lib/haloy ReadOnlyPaths=/etc/haloy ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true RestrictSUIDSGID=true LimitNOFILE=65536 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now haloy-proxy haloyd

OpenRC

cat > /etc/init.d/haloy-proxy << 'EOF' #!/sbin/openrc-run name="haloy-proxy" description="Haloy Proxy" command="/usr/local/bin/haloy-proxy" command_args="serve" command_user="haloy:haloy" pidfile="/run/haloy-proxy/haloy-proxy.pid" command_background="yes" output_log="/var/log/haloy-proxy.log" error_log="/var/log/haloy-proxy.log" export HALOY_DATA_DIR="/var/lib/haloy" depend() { need net after firewall } start_pre() { checkpath --directory --owner haloy:haloy --mode 0755 /run/haloy-proxy checkpath --file --owner haloy:haloy --mode 0644 /var/log/haloy-proxy.log } EOF cat > /etc/init.d/haloyd << 'EOF' #!/sbin/openrc-run name="haloyd" description="Haloy Daemon" command="/usr/local/bin/haloyd" command_args="serve" command_user="haloy:haloy" pidfile="/run/haloyd/haloyd.pid" command_background="yes" output_log="/var/log/haloyd.log" error_log="/var/log/haloyd.log" export HALOY_DATA_DIR="/var/lib/haloy" export HALOY_CONFIG_DIR="/etc/haloy" depend() { need net docker use haloy-proxy after firewall haloy-proxy } start_pre() { checkpath --directory --owner haloy:haloy --mode 0755 /run/haloyd checkpath --file --owner haloy:haloy --mode 0644 /var/log/haloyd.log } EOF chmod +x /etc/init.d/haloy-proxy chmod +x /etc/init.d/haloyd rc-update add haloy-proxy default rc-update add haloyd default rc-service haloy-proxy start rc-service haloyd start

Service Management

Systemd

# Start the service systemctl start haloy-proxy haloyd # Stop the service systemctl stop haloyd haloy-proxy # Restart only the control plane; traffic keeps flowing through haloy-proxy systemctl restart haloyd # Check service status systemctl status haloyd haloy-proxy # View control-plane logs journalctl -u haloyd -f # View proxy logs journalctl -u haloy-proxy -f

OpenRC

# Start the service rc-service haloy-proxy start rc-service haloyd start # Stop the service rc-service haloyd stop rc-service haloy-proxy stop # Restart only the control plane; traffic keeps flowing through haloy-proxy rc-service haloyd restart # Check service status rc-service haloyd status rc-service haloy-proxy status

Configuration

The haloyd configuration file is located at /etc/haloy/haloyd.yaml:

api_domain: haloy.yourserver.com health_monitor: enabled: true interval: "15s" fall: 3 rise: 2 timeout: "5s"

View or Update Configuration

# View a configuration value haloyd config get api-domain # Set a configuration value haloyd config set api-domain haloy.newdomain.com # Get the API token haloyd config get api-token

Directory Structure

/etc/haloy/ # Configuration ├── haloyd.yaml # Daemon settings └── .env # API token /var/lib/haloy/ # Data ├── cert-storage/ # SSL certificates ├── db/ # Deployment database └── proxy/ # Proxy socket and routing snapshot

Verify Installation

Run the verify command to check that everything is working:

haloyd verify

This checks:

  • Configuration directory and files
  • Data directory
  • Docker connectivity
  • Docker network
  • API health

Same-Server Deployment

If you want to run the haloy CLI directly on the server (instead of from your local machine), you can add the server using localhost:

haloy server add localhost <api-token>

This is useful for:

  • Single-server setups
  • CI/CD pipelines running on the server
  • Situations where external API access isn’t available

Troubleshooting

Docker Not Installed

If you used --skip-docker-install and Docker is not installed, the script will show:

✗ Docker is not installed Troubleshooting: - Install Docker manually: curl -fsSL https://sh.haloy.dev/install-docker.sh | sh - Or remove SKIP_DOCKER_INSTALL to install automatically

See Docker Installation for installation options.

Service Won’t Start

Check the logs:

journalctl -u haloyd -n 50 journalctl -u haloy-proxy -n 50

Common issues:

  • Docker not running: systemctl start docker
  • Port 80/443 in use: Check with ss -tlnp | grep -E ':80|:443'; these ports should belong to haloy-proxy
  • Permission issues: Ensure the haloy user is in the docker group

Cannot Connect to API

haloyd serves its API on a loopback port only. haloy-proxy terminates HTTPS and forwards API traffic to it, so the API is unreachable if haloy-proxy is down, even when haloyd is running.

Verify both services are running and listening:

# Check service status systemctl status haloyd haloy-proxy # Check if ports are open ss -tlnp | grep -E 'haloyd|haloy-proxy' # Test API locally curl -k https://localhost/health

Verify Docker Access

# Test that haloy user can access Docker sudo -u haloy docker ps

Redirect Loop (ERR_TOO_MANY_REDIRECTS)

If you see “too many redirects” errors in the browser, this is typically caused by Cloudflare proxy settings.

Cause: Cloudflare’s “Flexible” SSL mode connects to your origin via HTTP. Haloy redirects HTTP to HTTPS, creating an infinite loop.

Solutions:

  1. Use DNS-only mode (recommended for simplicity): In Cloudflare, click the orange cloud icon to turn it grey. This disables the proxy and lets traffic go directly to your server.

  2. Use Full (Strict) SSL mode: In Cloudflare dashboard, go to SSL/TLS → Overview and change the mode to “Full (Strict)”. This makes Cloudflare connect to your origin via HTTPS.

After changing settings, you may need to:

  • Clear your browser cache or use incognito mode
  • Flush your local DNS cache: sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder (macOS)
  • Wait a few minutes for DNS changes to propagate

Certificate Issuance Fails (Domain Resolves to Loopback)

If haloyd can’t obtain a TLS certificate and logs show your API domain resolving to 127.0.1.1 or another loopback address, your /etc/hosts file is likely the culprit.

Cause: Debian and Ubuntu cloud-init images often add a line like this to /etc/hosts:

127.0.1.1 myserver.example.com myserver

When myserver.example.com is also your Haloy API domain, haloyd resolves it to 127.0.1.1 instead of the server’s public IP. haloyd uses the system resolver, which reads /etc/hosts before querying DNS.

Note that dig won’t show this, since it queries DNS servers directly and skips /etc/hosts. Use getent instead to see what the system resolver returns:

getent ahostsv4 myserver.example.com

If the output shows 127.0.1.1 instead of your server’s public IP, /etc/hosts is overriding DNS.

Fix: Edit /etc/hosts so only the short hostname maps to 127.0.1.1, not the FQDN:

# Before (problematic) 127.0.1.1 myserver.example.com myserver # After (fixed) 127.0.1.1 myserver

To prevent cloud-init from restoring the old entry on reboot, make the same change in the template:

nano /etc/cloud/templates/hosts.debian.tmpl # Remove the FQDN from the 127.0.1.1 line

Then restart haloyd:

systemctl restart haloyd

Uninstalling

To completely remove Haloy from your server:

curl -sL https://sh.haloy.dev/uninstall-server.sh | sh

This will:

  • Stop and remove the haloyd and haloy-proxy services
  • Optionally back up your data
  • Remove configuration and data directories
  • Remove the server binaries
  • Optionally remove the haloy user

See Uninstalling for more details.

Upgrading

To upgrade an existing server installation, use one of the dedicated upgrade methods described in Upgrading. Do not re-run the install script to upgrade, as it will overwrite your systemd/OpenRC service files and discard any manual customizations you’ve made to them.

Next Steps

Stay updated on Haloy

Get notified about new docs, deployment patterns, and Haloy updates.