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

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"

Process:

  1. Build Docker image locally for server architecture
  2. Save image to tar file
  3. Upload tar to server via SCP
  4. Load image on server
  5. Clean up local tar file
  6. 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\""

Process:

  1. Global pre-deploy builds image once
  2. Saves to tar file
  3. Each target uploads to its server
  4. Each target loads image on server
  5. 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\"

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\"}'"

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'"

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

Then use in hooks:

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\"}'"

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

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\""

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\""

Using Makefile

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\"}'"

Discord Notifications

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"

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 ."

Best Practices

  1. Use built-in builder when possible: Simpler and more maintainable
  2. Test hooks locally: Run commands manually before adding to config
  3. Use SSH keys: Avoid password prompts in automated deployments
  4. Set source to local: Prevents registry pulls when using local images
  5. Clean up artifacts: Remove temporary files in post-deploy
  6. Use global hooks: Build once for multi-target deployments
  7. Handle errors gracefully: Use || true for non-critical commands
  8. 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

Docker Build Failures

# 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"

Next Steps