Build Locally with Hooks
For advanced scenarios where you need custom build processes beyond Haloy’s built-in builder, use deployment hooks to implement your own build and upload workflow.
When to Use Hooks vs Built-in Builder
Use Built-in Builder (Recommended)
For most use cases, use Haloy’s Image Build Config feature:
- Standard Docker builds
- Simple build arguments
- Push to registry or server
- Consistent build process
Use Deployment Hooks (Advanced)
Use hooks for complex scenarios:
- Custom build processes with multiple steps
- Integration with external build tools
- Complex image transformations
- Custom authentication mechanisms
- Multi-stage build pipelines
Single Target Example
Build and upload to a single server:
name: "my-app"
server: "haloy.yourserver.com"
image:
repository: "my-app"
source: "local" # Tell Haloy not to pull from registry
tag: "latest"
domains:
- domain: "my-app.com"
acme_email: "acme@my-app.com"
pre_deploy:
- "docker build --platform linux/amd64 -t my-app ."
- "docker save -o my-app.tar my-app"
- "scp my-app.tar $(whoami)@server-ip:/tmp/my-app.tar"
- "ssh $(whoami)@server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
- "rm my-app.tar"
name: "my-app"
server: "haloy.yourserver.com"
image:
repository: "my-app"
source: "local" # Tell Haloy not to pull from registry
tag: "latest"
domains:
- domain: "my-app.com"
acme_email: "acme@my-app.com"
pre_deploy:
- "docker build --platform linux/amd64 -t my-app ."
- "docker save -o my-app.tar my-app"
- "scp my-app.tar $(whoami)@server-ip:/tmp/my-app.tar"
- "ssh $(whoami)@server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
- "rm my-app.tar"
Process:
- Build Docker image locally for server architecture
- Save image to tar file
- Upload tar to server via SCP
- Load image on server
- Clean up local tar file
- Haloy deploys using local image
Multi-Target Example
Build once, deploy to multiple servers:
name: "my-app"
image:
repository: "my-app"
source: "local"
tag: "latest"
# Build once before all deployments
global_pre_deploy:
- "docker build --platform linux/amd64 -t my-app ."
- "docker save -o my-app.tar my-app"
# Cleanup after all deployments
global_post_deploy:
- "rm my-app.tar"
targets:
production:
server: "prod.haloy.com"
domains: - domain: "my-app.com"
pre_deploy: - "scp my-app.tar $(whoami)@prod-server-ip:/tmp/my-app.tar" - "ssh $(whoami)@prod-server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
staging:
server: "staging.haloy.com"
domains: - domain: "staging.my-app.com"
pre_deploy: - "scp my-app.tar $(whoami)@staging-server-ip:/tmp/my-app.tar" - "ssh $(whoami)@staging-server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
name: "my-app"
image:
repository: "my-app"
source: "local"
tag: "latest"
# Build once before all deployments
global_pre_deploy:
- "docker build --platform linux/amd64 -t my-app ."
- "docker save -o my-app.tar my-app"
# Cleanup after all deployments
global_post_deploy:
- "rm my-app.tar"
targets:
production:
server: "prod.haloy.com"
domains: - domain: "my-app.com"
pre_deploy: - "scp my-app.tar $(whoami)@prod-server-ip:/tmp/my-app.tar" - "ssh $(whoami)@prod-server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
staging:
server: "staging.haloy.com"
domains: - domain: "staging.my-app.com"
pre_deploy: - "scp my-app.tar $(whoami)@staging-server-ip:/tmp/my-app.tar" - "ssh $(whoami)@staging-server-ip \"docker load -i /tmp/my-app.tar && rm /tmp/my-app.tar\""
Process:
- Global pre-deploy builds image once
- Saves to tar file
- Each target uploads to its server
- Each target loads image on server
- Global post-deploy cleans up tar file
Advanced Build Pipeline
Complex build with testing and multi-stage process:
name: "complex-app"
image:
repository: "complex-app"
source: "local"
tag: "latest"
global_pre_deploy:
# 1. Run tests
- "npm run test"
- "npm run lint"
# 2. Build optimized production bundle
- "npm run build"
# 3. Build Docker image with build args
- "docker build --platform linux/amd64 --build-arg VERSION=$(git rev-parse --short HEAD) -t complex-app ."
# 4. Run security scan
- "docker scan complex-app || echo 'Security scan completed'"
# 5. Save image
- "docker save -o complex-app.tar complex-app"
global_post_deploy:
# Cleanup
- "rm complex-app.tar"
- "docker image prune -f"
targets:
production:
server: "prod.haloy.com"
domains: - domain: "complex-app.com"
pre_deploy: - "scp complex-app.tar deploy@prod-server:/tmp/" - "ssh deploy@prod-server \"docker load -i /tmp/complex-app.tar && rm /tmp/complex-app.tar\"
name: "complex-app"
image:
repository: "complex-app"
source: "local"
tag: "latest"
global_pre_deploy:
# 1. Run tests
- "npm run test"
- "npm run lint"
# 2. Build optimized production bundle
- "npm run build"
# 3. Build Docker image with build args
- "docker build --platform linux/amd64 --build-arg VERSION=$(git rev-parse --short HEAD) -t complex-app ."
# 4. Run security scan
- "docker scan complex-app || echo 'Security scan completed'"
# 5. Save image
- "docker save -o complex-app.tar complex-app"
global_post_deploy:
# Cleanup
- "rm complex-app.tar"
- "docker image prune -f"
targets:
production:
server: "prod.haloy.com"
domains: - domain: "complex-app.com"
pre_deploy: - "scp complex-app.tar deploy@prod-server:/tmp/" - "ssh deploy@prod-server \"docker load -i /tmp/complex-app.tar && rm /tmp/complex-app.tar\"
Custom Registry Push
Build and push to a private registry with custom logic:
name: "registry-app"
image:
repository: "registry.example.com/my-app"
tag: "v1.2.3"
pre_deploy:
# 1. Build image
- "docker build --platform linux/amd64 -t registry.example.com/my-app:v1.2.3 ."
# 2. Login to custom registry
- "echo $REGISTRY_PASSWORD | docker login registry.example.com -u $REGISTRY_USERNAME --password-stdin"
# 3. Push to registry
- "docker push registry.example.com/my-app:v1.2.3"
# 4. Also push as latest
- "docker tag registry.example.com/my-app:v1.2.3 registry.example.com/my-app:latest"
- "docker push registry.example.com/my-app:latest"
# 5. Logout
- "docker logout registry.example.com"
post_deploy:
# Send notification
- "curl -X POST https://hooks.slack.com/services/... -d '{\"text\": \"Deployed v1.2.3\"}'"
name: "registry-app"
image:
repository: "registry.example.com/my-app"
tag: "v1.2.3"
pre_deploy:
# 1. Build image
- "docker build --platform linux/amd64 -t registry.example.com/my-app:v1.2.3 ."
# 2. Login to custom registry
- "echo $REGISTRY_PASSWORD | docker login registry.example.com -u $REGISTRY_USERNAME --password-stdin"
# 3. Push to registry
- "docker push registry.example.com/my-app:v1.2.3"
# 4. Also push as latest
- "docker tag registry.example.com/my-app:v1.2.3 registry.example.com/my-app:latest"
- "docker push registry.example.com/my-app:latest"
# 5. Logout
- "docker logout registry.example.com"
post_deploy:
# Send notification
- "curl -X POST https://hooks.slack.com/services/... -d '{\"text\": \"Deployed v1.2.3\"}'"
SSH Configuration
For reliable SSH connections in hooks:
Setup SSH Keys
# Generate SSH key if needed
ssh-keygen -t ed25519 -C "deploy@local"
# Copy to server
ssh-copy-id user@server-ip
# Test connection
ssh user@server-ip "echo 'Connection successful'"
# Generate SSH key if needed
ssh-keygen -t ed25519 -C "deploy@local"
# Copy to server
ssh-copy-id user@server-ip
# Test connection
ssh user@server-ip "echo 'Connection successful'"
SSH Config File
Create ~/.ssh/config:
Host prod-server
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
Host staging-server
HostName 203.0.113.20
User deploy
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
Host prod-server
HostName 203.0.113.10
User deploy
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
Host staging-server
HostName 203.0.113.20
User deploy
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
Then use in hooks:
pre_deploy:
- "scp my-app.tar prod-server:/tmp/"
- "ssh prod-server \"docker load -i /tmp/my-app.tar\""
pre_deploy:
- "scp my-app.tar prod-server:/tmp/"
- "ssh prod-server \"docker load -i /tmp/my-app.tar\""
Environment Variables in Hooks
Access environment variables in deployment hooks:
name: "my-app"
image:
repository: "my-app"
source: "local"
pre_deploy:
# Use environment variables
- "echo \"Building version $BUILD_VERSION\""
- "docker build --build-arg VERSION=$BUILD_VERSION -t my-app ."
# Use secrets from environment
- "echo $REGISTRY_TOKEN | docker login ghcr.io -u $REGISTRY_USERNAME --password-stdin"
- "docker push ghcr.io/my-org/my-app:$BUILD_VERSION"
post_deploy:
# Send notification with webhook URL from environment
- "curl -X POST $SLACK_WEBHOOK_URL -d '{\"text\": \"Deployed $BUILD_VERSION\"}'"
name: "my-app"
image:
repository: "my-app"
source: "local"
pre_deploy:
# Use environment variables
- "echo \"Building version $BUILD_VERSION\""
- "docker build --build-arg VERSION=$BUILD_VERSION -t my-app ."
# Use secrets from environment
- "echo $REGISTRY_TOKEN | docker login ghcr.io -u $REGISTRY_USERNAME --password-stdin"
- "docker push ghcr.io/my-org/my-app:$BUILD_VERSION"
post_deploy:
# Send notification with webhook URL from environment
- "curl -X POST $SLACK_WEBHOOK_URL -d '{\"text\": \"Deployed $BUILD_VERSION\"}'"
Set before deploying:
export BUILD_VERSION="v1.2.3"
export REGISTRY_USERNAME="username"
export REGISTRY_TOKEN="token"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/..."
haloy deploy
export BUILD_VERSION="v1.2.3"
export REGISTRY_USERNAME="username"
export REGISTRY_TOKEN="token"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/..."
haloy deploy
Build with External Tools
Using Docker Compose
pre_deploy:
- "docker-compose build app"
- "docker save -o app.tar project_app:latest"
- "scp app.tar server:/tmp/"
- "ssh server \"docker load -i /tmp/app.tar && rm /tmp/app.tar\""
pre_deploy:
- "docker-compose build app"
- "docker save -o app.tar project_app:latest"
- "scp app.tar server:/tmp/"
- "ssh server \"docker load -i /tmp/app.tar && rm /tmp/app.tar\""
Using Buildx for Multi-Platform
pre_deploy:
- "docker buildx build --platform linux/amd64,linux/arm64 -t my-app --load ."
- "docker save -o my-app.tar my-app"
- "scp my-app.tar server:/tmp/"
- "ssh server \"docker load -i /tmp/my-app.tar\""
pre_deploy:
- "docker buildx build --platform linux/amd64,linux/arm64 -t my-app --load ."
- "docker save -o my-app.tar my-app"
- "scp my-app.tar server:/tmp/"
- "ssh server \"docker load -i /tmp/my-app.tar\""
Using Makefile
pre_deploy:
- "make build"
- "make package"
- "make upload SERVER=prod-server"
post_deploy:
- "make cleanup"
pre_deploy:
- "make build"
- "make package"
- "make upload SERVER=prod-server"
post_deploy:
- "make cleanup"
Notifications
Slack Notifications
pre_deploy:
- "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Starting deployment of my-app\"}'"
post_deploy:
- "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Successfully deployed my-app\"}'"
pre_deploy:
- "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Starting deployment of my-app\"}'"
post_deploy:
- "curl -X POST $SLACK_WEBHOOK -d '{\"text\": \"Successfully deployed my-app\"}'"
Discord Notifications
post_deploy:
- "curl -H \"Content-Type: application/json\" -d '{\"content\": \"Deployment complete!\"}' $DISCORD_WEBHOOK"
post_deploy:
- "curl -H \"Content-Type: application/json\" -d '{\"content\": \"Deployment complete!\"}' $DISCORD_WEBHOOK"
Email Notifications
post_deploy:
- "echo 'Deployment successful' | mail -s 'Deployment Status' admin@example.com"
post_deploy:
- "echo 'Deployment successful' | mail -s 'Deployment Status' admin@example.com"
Error Handling
Hooks stop execution on first error. Use || true to continue on errors:
pre_deploy:
# Critical - must succeed
- "npm run test"
# Optional - continue if fails
- "npm run lint || echo 'Lint warnings present'"
# Required again
- "docker build -t my-app ."
pre_deploy:
# Critical - must succeed
- "npm run test"
# Optional - continue if fails
- "npm run lint || echo 'Lint warnings present'"
# Required again
- "docker build -t my-app ."
Best Practices
- Use built-in builder when possible: Simpler and more maintainable
- Test hooks locally: Run commands manually before adding to config
- Use SSH keys: Avoid password prompts in automated deployments
- Set source to local: Prevents registry pulls when using local images
- Clean up artifacts: Remove temporary files in post-deploy
- Use global hooks: Build once for multi-target deployments
- Handle errors gracefully: Use
|| truefor non-critical commands - Document custom processes: Add comments explaining complex hooks
Troubleshooting
SSH Connection Issues
# Test SSH connection
ssh user@server "echo 'Connected'"
# Check SSH config
cat ~/.ssh/config
# Verify key permissions
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
# Test SSH connection
ssh user@server "echo 'Connected'"
# Check SSH config
cat ~/.ssh/config
# Verify key permissions
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
Docker Build Failures
# Build locally first
docker build --platform linux/amd64 -t my-app .
# Check Docker daemon
docker ps
# Check disk space
df -h
# Build locally first
docker build --platform linux/amd64 -t my-app .
# Check Docker daemon
docker ps
# Check disk space
df -h
Image Load Failures on Server
# SSH to server and test manually
ssh server "docker load -i /tmp/my-app.tar"
# Check server disk space
ssh server "df -h"
# Check Docker on server
ssh server "docker ps"
# SSH to server and test manually
ssh server "docker load -i /tmp/my-app.tar"
# Check server disk space
ssh server "df -h"
# Check Docker on server
ssh server "docker ps"