Server Installation
Haloy installs two server daemons:
haloydmanages deployments, Docker discovery, health checks, API access, certificates, and routing snapshots.haloy-proxyowns ports80and443, terminates HTTPS, and keeps serving traffic whilehaloydrestarts 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
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
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
curl -fsSL https://sh.haloy.dev/install-haloyd.sh | sh
wget -qO- 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
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
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
haloyd config get api-token
Options
The install script accepts both command-line flags and environment variables:
| Flag | Environment Variable | Description |
|---|---|---|
--api-domain=DOMAIN | API_DOMAIN | Domain for the haloy API (e.g., api.example.com) |
--version=VERSION | VERSION | Install a specific version (default: latest) |
--skip-start | SKIP_START=true | Don’t start the services after installation |
--skip-docker-install | SKIP_DOCKER_INSTALL=true | Skip 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 | sh -s -- --api-domain=api.example.com
curl -fsSL https://sh.haloy.dev/install-haloyd.sh | VERSION=v0.1.0 sh
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
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
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
# 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
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
# 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
haloyd init --api-domain haloy.yourserver.com
This creates:
/etc/haloy/haloyd.yaml- Daemon configuration/etc/haloy/.env- API token/var/lib/haloy/- Data directoryhaloyDocker 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
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
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
# 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
# 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"
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
# 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
/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
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>
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
✗ 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
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 tohaloy-proxy - Permission issues: Ensure the
haloyuser is in thedockergroup
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
# 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
# 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:
-
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.
-
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
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
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
# 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
nano /etc/cloud/templates/hosts.debian.tmpl
# Remove the FQDN from the 127.0.1.1 line
Then restart haloyd:
systemctl restart haloyd
systemctl restart haloyd
Uninstalling
To completely remove Haloy from your server:
curl -sL https://sh.haloy.dev/uninstall-server.sh | sh
curl -sL https://sh.haloy.dev/uninstall-server.sh | sh
This will:
- Stop and remove the
haloydandhaloy-proxyservices - 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.