Skip to content

Instantly share code, notes, and snippets.

@pipethedev
Last active October 20, 2025 12:47
Show Gist options
  • Select an option

  • Save pipethedev/2b1fd8781e22664fc3eb0b1734c20f7e to your computer and use it in GitHub Desktop.

Select an option

Save pipethedev/2b1fd8781e22664fc3eb0b1734c20f7e to your computer and use it in GitHub Desktop.
Script to setup consul in client or server mode with auto server discovery in client mode
#!/bin/bash
set -e
CONSUL_VERSION="1.21.5"
CONSUL_URL="https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip"
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc/consul.d"
DATA_DIR="/opt/consul"
TEMP_DIR="/tmp/consul-install"
MODE=""
BIND_ADDR=""
BOOTSTRAP_EXPECT=3
RETRY_JOIN="consul-servers.internal"
DATACENTER="dc1"
ENABLE_UI="true"
NUKE="false"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
detect_bind_address() {
local addr=""
if ip addr show tailscale0 &>/dev/null; then
addr=$(ip addr show tailscale0 | grep "inet " | awk '{print $2}' | cut -d/ -f1 | head -n1)
if [ -n "$addr" ]; then
echo "$addr"
return
fi
fi
addr=$(ip route get 8.8.8.8 | grep -oP 'src \K\S+')
if [ -n "$addr" ]; then
echo "$addr"
return
fi
addr=$(hostname -I | awk '{print $1}')
if [ -n "$addr" ]; then
echo "$addr"
return
fi
>&2 echo -e "${RED}[ERROR]${NC} Could not detect IP address"
exit 1
}
show_usage() {
cat << EOF
Usage: $0 --mode=<server|client> [OPTIONS]
Required:
--mode=<server|client> Installation mode
Optional:
--bind-addr=<IP> IP address to bind to (auto-detects if not provided)
--data-dir=<PATH> Data directory (default: /opt/consul)
--bootstrap-expect=<N> Expected servers (default: 3, server mode only)
--retry-join=<DNS> DNS name for retry-join (default: consul-servers.internal)
--datacenter=<NAME> Datacenter name (default: dc1)
--enable-ui=<true|false> Enable UI (default: true for server, false for client)
--nuke=<true|false> Delete existing data directory (default: false)
Examples:
$0 --mode=server
$0 --mode=server --nuke=true
$0 --mode=client --bind-addr=10.0.1.10
EOF
exit 1
}
for arg in "$@"; do
case $arg in
--mode=*)
MODE="${arg#*=}"
;;
--bind-addr=*)
BIND_ADDR="${arg#*=}"
;;
--data-dir=*)
DATA_DIR="${arg#*=}"
;;
--bootstrap-expect=*)
BOOTSTRAP_EXPECT="${arg#*=}"
;;
--retry-join=*)
RETRY_JOIN="${arg#*=}"
;;
--datacenter=*)
DATACENTER="${arg#*=}"
;;
--enable-ui=*)
ENABLE_UI="${arg#*=}"
;;
--nuke=*)
NUKE="${arg#*=}"
;;
--help|-h)
show_usage
;;
*)
log_error "Unknown argument: $arg"
show_usage
;;
esac
done
if [ -z "$MODE" ]; then
log_error "Missing required argument: --mode"
show_usage
fi
if [ "$MODE" != "server" ] && [ "$MODE" != "client" ]; then
log_error "Mode must be 'server' or 'client'"
show_usage
fi
if [ "$MODE" = "client" ] && [ "$ENABLE_UI" = "true" ]; then
ENABLE_UI="false"
fi
if [ -z "$BIND_ADDR" ]; then
log_info "No bind address provided, auto-detecting..."
BIND_ADDR=$(detect_bind_address)
fi
if [ "$EUID" -ne 0 ]; then
log_error "Please run as root (use sudo)"
exit 1
fi
echo "======================================"
echo "Consul Installation Script"
echo "======================================"
echo "Mode: $MODE"
echo "Bind Address: $BIND_ADDR"
echo "Data Directory: $DATA_DIR"
echo "UI Enabled: $ENABLE_UI"
echo "Nuke Data: $NUKE"
[ "$MODE" = "server" ] && echo "Bootstrap Expect: $BOOTSTRAP_EXPECT"
echo "Retry Join: $RETRY_JOIN"
echo "Datacenter: $DATACENTER"
echo "======================================"
echo ""
log_info "Stopping any existing Consul services..."
systemctl stop consul 2>/dev/null || true
log_info "Killing any remaining Consul processes..."
pkill -9 consul 2>/dev/null || true
sleep 2
log_info "Cleaning old config files..."
rm -f ${CONFIG_DIR}/*.hcl ${CONFIG_DIR}/*.json
if [ "$NUKE" = "true" ]; then
log_warn "Nuking existing data directory: ${DATA_DIR}"
rm -rf ${DATA_DIR}/*
fi
log_info "Removing any existing Consul containers..."
docker ps -q --filter "ancestor=hashicorp/consul" | xargs -r docker stop 2>/dev/null || true
docker ps -aq --filter "ancestor=hashicorp/consul" | xargs -r docker rm 2>/dev/null || true
if ! command -v unzip &> /dev/null; then
log_info "Installing unzip..."
apt-get update -qq
apt-get install -y unzip
fi
log_info "Downloading Consul ${CONSUL_VERSION}..."
mkdir -p ${TEMP_DIR}
cd ${TEMP_DIR}
wget -q ${CONSUL_URL}
log_info "Installing Consul..."
unzip -oq consul_${CONSUL_VERSION}_linux_amd64.zip
if [ -f "${INSTALL_DIR}/consul" ]; then
log_warn "Backing up existing Consul binary..."
cp ${INSTALL_DIR}/consul ${INSTALL_DIR}/consul.backup
fi
mv consul ${INSTALL_DIR}/consul
chmod +x ${INSTALL_DIR}/consul
INSTALLED_VERSION=$(consul version | head -n1)
log_info "Installed: ${INSTALLED_VERSION}"
log_info "Creating directories..."
mkdir -p ${CONFIG_DIR} ${DATA_DIR}
chmod 755 ${CONFIG_DIR} ${DATA_DIR}
log_info "Creating Consul configuration..."
if [ "$MODE" = "server" ]; then
cat > ${CONFIG_DIR}/consul.hcl <<'EOF'
datacenter = "dc1"
node_name = "HOSTNAME_PLACEHOLDER"
data_dir = "DATA_DIR_PLACEHOLDER"
log_level = "INFO"
server = true
bootstrap_expect = BOOTSTRAP_EXPECT_PLACEHOLDER
bind_addr = "BIND_ADDR_PLACEHOLDER"
client_addr = "0.0.0.0"
advertise_addr = "BIND_ADDR_PLACEHOLDER"
retry_join = ["RETRY_JOIN_PLACEHOLDER"]
connect {
enabled = true
}
ui_config {
enabled = ENABLE_UI_PLACEHOLDER
}
ports {
grpc = 8502
grpc_tls = 8503
http = 8500
dns = 8600
https = 8501
serf_lan = 8301
serf_wan = 8302
server = 8300
}
performance {
raft_multiplier = 1
}
autopilot {
cleanup_dead_servers = true
last_contact_threshold = "200ms"
max_trailing_logs = 250
server_stabilization_time = "10s"
}
raft_protocol = 3
EOF
sed -i "s|HOSTNAME_PLACEHOLDER|$(hostname)|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|DATA_DIR_PLACEHOLDER|${DATA_DIR}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|BOOTSTRAP_EXPECT_PLACEHOLDER|${BOOTSTRAP_EXPECT}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|BIND_ADDR_PLACEHOLDER|${BIND_ADDR}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|RETRY_JOIN_PLACEHOLDER|${RETRY_JOIN}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|ENABLE_UI_PLACEHOLDER|${ENABLE_UI}|g" ${CONFIG_DIR}/consul.hcl
else
cat > ${CONFIG_DIR}/consul.hcl <<'EOF'
datacenter = "dc1"
node_name = "HOSTNAME_PLACEHOLDER"
data_dir = "DATA_DIR_PLACEHOLDER"
log_level = "INFO"
server = false
bind_addr = "BIND_ADDR_PLACEHOLDER"
client_addr = "0.0.0.0"
advertise_addr = "BIND_ADDR_PLACEHOLDER"
retry_join = ["RETRY_JOIN_PLACEHOLDER"]
connect {
enabled = true
}
ui_config {
enabled = ENABLE_UI_PLACEHOLDER
}
ports {
grpc = 8502
http = 8500
dns = 8600
https = 8501
}
performance {
raft_multiplier = 1
}
EOF
sed -i "s|HOSTNAME_PLACEHOLDER|$(hostname)|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|DATA_DIR_PLACEHOLDER|${DATA_DIR}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|BIND_ADDR_PLACEHOLDER|${BIND_ADDR}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|RETRY_JOIN_PLACEHOLDER|${RETRY_JOIN}|g" ${CONFIG_DIR}/consul.hcl
sed -i "s|ENABLE_UI_PLACEHOLDER|${ENABLE_UI}|g" ${CONFIG_DIR}/consul.hcl
fi
log_info "Validating Consul configuration..."
if ! consul validate ${CONFIG_DIR}/consul.hcl; then
log_error "Config validation failed!"
log_error "Config file content:"
cat ${CONFIG_DIR}/consul.hcl
exit 1
fi
log_info "Configuration validated successfully"
log_info "Creating systemd service..."
cat > /etc/systemd/system/consul.service <<EOF
[Unit]
Description=Consul Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
ExecStart=${INSTALL_DIR}/consul agent -config-dir=${CONFIG_DIR}
ExecReload=/bin/kill -HUP \$MAINPID
KillMode=process
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
log_info "Enabling and starting Consul service..."
systemctl daemon-reload
systemctl enable consul
systemctl start consul
log_info "Waiting for Consul to start..."
sleep 5
if systemctl is-active --quiet consul; then
log_info "Consul is running successfully!"
echo ""
log_info "Consul Status:"
consul members 2>/dev/null || log_warn "Cluster not yet formed (normal on first server)"
echo ""
log_info "Consul is accessible at:"
if [ "$ENABLE_UI" = "true" ]; then
echo " - UI: http://${BIND_ADDR}:8500"
fi
echo " - HTTP API: http://${BIND_ADDR}:8500"
echo " - DNS: ${BIND_ADDR}:8600"
else
log_error "Consul failed to start!"
echo ""
log_error "Check logs with: journalctl -u consul -n 50"
exit 1
fi
log_info "Cleaning up..."
cd /
rm -rf ${TEMP_DIR}
echo ""
echo "======================================"
log_info "Installation Complete!"
echo "======================================"
echo ""
echo "Config file: ${CONFIG_DIR}/consul.hcl"
echo "Data directory: ${DATA_DIR}"
echo ""
echo "Useful commands:"
echo " - Check status: systemctl status consul"
echo " - View logs: journalctl -u consul -f"
echo " - Restart: systemctl restart consul"
echo " - Stop: systemctl stop consul"
echo " - Members: consul members"
echo " - Services: consul catalog services"
echo ""
if [ "$MODE" = "server" ]; then
echo "NOTE: Make sure DNS is configured for '${RETRY_JOIN}' pointing to all server IPs"
fi
@pipethedev
Copy link
Author

pipethedev commented Oct 18, 2025

Consul Bare Metal Setup Guide

Prerequisites

  • 3+ servers with private network
  • Root access
  • All servers can reach each other

Step 1: Configure DNS Resolution

On ALL nodes, add server IPs to /etc/hosts:

cat >> /etc/hosts << EOF
10.0.1.10 consul-servers.internal
10.0.1.11 consul-servers.internal
10.0.1.12 consul-servers.internal
EOF

Replace IPs with your actual server IPs.

Step 2: Download Installation Script

wget https://gist.githubusercontent.com/.../install-consul.sh
chmod +x install-consul.sh

Step 3: Install on Server Nodes

Run on each of your 3 server nodes:

sudo ./install-consul.sh --mode=server

Options:

# With custom data directory
sudo ./install-consul.sh --mode=server --data-dir=/mnt/consul-data

# Override bind address (auto-detects Tailscale by default)
sudo ./install-consul.sh --mode=server --bind-addr=10.0.1.10

# Custom bootstrap expect
sudo ./install-consul.sh --mode=server --bootstrap-expect=5

Step 4: Install on Client Nodes

Run on each client node:

sudo ./install-consul.sh --mode=client
# Client with UI enabled
sudo ./install-consul.sh --mode=client --enable-ui=true

Step 5: Add More Servers (Optional)

To add server 4, 5, etc after initial cluster is running:

# Just run the same command, they'll join automatically
sudo ./install-consul.sh --mode=server
# Server without UI
sudo ./install-consul.sh --mode=server --enable-ui=false

Note: bootstrap_expect stays at 3 forever. New servers join via retry_join.

Verification

Check cluster:

consul members

Access UI:

http://<any-server-ip>:8500

Common Commands

systemctl status consul
journalctl -u consul -f
consul members
consul catalog services
systemctl restart consul

Data Location

Default: /opt/consul

Custom: --data-dir=/your/path

Troubleshooting

Cluster not forming?

  • Check /etc/hosts on all nodes
  • Verify ports 8300-8302, 8500, 8600 are open
  • Check logs: journalctl -u consul -n 100

Service won't start?

  • Verify bind address is correct
  • Check port conflicts: netstat -tlnp | grep 8500

@codehakase
Copy link

Thanks, this will come in very handy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment