Image Configuration
Configure Docker images, registry authentication, local building, and rollback strategies.
Basic Image Configuration
| Key | Type | Required | Description |
|---|---|---|---|
repository | string | No | Docker image name (defaults to the target/app name when omitted) |
tag | string | No | Image tag (default: “latest”). Can also be included in repository (e.g., nginx:alpine) |
pull_policy | string | No | When to pull from the registry: “always”, “if_missing”, or “never” |
build | boolean | No | Whether to build the image locally |
registry | object | No | Private registry authentication |
history | object | No | Image history and rollback strategy |
build_config | object | No | Build configuration for local building |
Multi-Target Image Selection (Quick Start)
For multi-target setups, define reusable images at the root with images, then pick one per target using image_key.
name: "my-app"
server: "haloy.yourserver.com"
images:
web: "ghcr.io/your-org/my-app-web:v1.2.3"
worker: "ghcr.io/your-org/my-app-worker:v1.2.3"
targets:
production:
image_key: "web"
domains:
- domain: "my-app.com"
jobs:
image_key: "worker"
name: "my-app"
server: "haloy.yourserver.com"
images:
web: "ghcr.io/your-org/my-app-web:v1.2.3"
worker: "ghcr.io/your-org/my-app-worker:v1.2.3"
targets:
production:
image_key: "web"
domains:
- domain: "my-app.com"
jobs:
image_key: "worker"
When to use:
- Use root
imagewhen every target should share one default image - Use root
images+ targetimage_keywhen targets need different images (for example web vs worker)
Rules:
- A target can set either
imageorimage_key(not both) - Resolution priority is: target
image-> targetimage_key-> rootimage
String Shorthand
Anywhere an image is expected, you can use a plain string instead of the object form. The string is treated as the repository field.
name: "my-app"
image: "nginx:alpine"
name: "my-app"
image: "nginx:alpine"
This is equivalent to:
name: "my-app"
image:
repository: "nginx:alpine"
name: "my-app"
image:
repository: "nginx:alpine"
The shorthand also works in the images map:
images:
db: "postgres:18"
cache: "redis:7"
images:
db: "postgres:18"
cache: "redis:7"
You can mix shorthand and object forms in the same file:
image: "nginx:alpine"
images:
db: "postgres:18"
api:
repository: "node"
tag: "20"
registry:
username:
value: "user"
password:
from:
secret: "registry-pass"
image: "nginx:alpine"
images:
db: "postgres:18"
api:
repository: "node"
tag: "20"
registry:
username:
value: "user"
password:
from:
secret: "registry-pass"
Tags embedded in the string (e.g., "nginx:1.21") work the same way as in the repository field. All other image fields (tag, pull_policy, registry, history, build, build_config) require the object form.
Simple Configuration
Pull from a public registry using the object form:
name: "my-app"
image:
repository: "nginx:alpine"
name: "my-app"
image:
repository: "nginx:alpine"
Note: You can also specify the tag separately using the tag field:
image:
repository: "nginx"
tag: "alpine"
image:
repository: "nginx"
tag: "alpine"
If no tag is specified (and none is included in the repository), it defaults to latest.
Pull Policy
pull_policy controls when haloyd contacts the registry before starting a container.
| Value | Behavior |
|---|---|
always | Check the registry and pull when the local image is missing or outdated. This is default. |
if_missing | Use the local image if it already exists on the server. Pull only when it is missing. |
never | Never pull. The image must already exist on the server. |
image:
repository: "postgres"
tag: "18"
pull_policy: "if_missing"
image:
repository: "postgres"
tag: "18"
pull_policy: "if_missing"
Use if_missing for stable service images where you do not need Haloy to check the registry on every deploy. This is especially useful for Docker Hub images because registry checks can count against rate limits even when the image already exists locally.
The database and service presets default to pull_policy: "if_missing" when an image is configured. You can override it per target:
targets:
postgres:
preset: "database"
image:
repository: "postgres"
tag: "18"
pull_policy: "always"
targets:
postgres:
preset: "database"
image:
repository: "postgres"
tag: "18"
pull_policy: "always"
Registry Authentication
Authenticate with private Docker registries including Docker Hub, GitHub Container Registry (GHCR), Azure Container Registry (ACR), AWS ECR, and self-hosted registries.
Server-Level Registry Credentials
For credentials that should be available to haloyd on a server, use haloy server registry login. The CLI reads the server URL and API token from haloy.yaml, sends the credentials to the haloyd API, and haloyd uses them for future deploys and rollbacks that pull from that registry.
$DOCKERHUB_TOKEN below is an example environment variable name. Set it yourself before running the command; Haloy only reads the token from stdin.
export DOCKERHUB_TOKEN="paste-your-docker-hub-access-token"
printf '%s' "$DOCKERHUB_TOKEN" | haloy server registry login docker.io --username your-dockerhub-username --password-stdin
haloy server registry list
export DOCKERHUB_TOKEN="paste-your-docker-hub-access-token"
printf '%s' "$DOCKERHUB_TOKEN" | haloy server registry login docker.io --username your-dockerhub-username --password-stdin
haloy server registry list
The pipe is required with --password-stdin; it reads the token from stdin and will error if run from an interactive terminal without piped input.
In CI, create DOCKERHUB_TOKEN as a secret or environment variable in your CI provider, then use the same pipe command.
For Docker Hub, replace your-dockerhub-username with your actual Docker Hub username, not your email address, and use a Docker Hub access token. Docker’s browser one-time-code login is specific to the Docker CLI and is not used by Haloy.
haloyd verifies the credentials with the server’s Docker daemon before saving them. If Docker rejects the username or token, the command fails and no credentials are written.
For multi-target config files that resolve to one Haloy server, no target selector is required. If targets resolve to multiple servers, add --targets <name> or --all. To connect directly without using haloy.yaml, add --server haloy.example.com.
This is the recommended fix when a server hits Docker Hub anonymous pull rate limits. A local docker login on your laptop is not sent to remote deployments, and logging in manually on the VPS can fail if Docker is run by a different user or environment than haloyd.
Server-level registry credentials are stored on the server at /var/lib/haloy/registries.yaml and are used only when the target does not define image.registry. Direct image.registry credentials in haloy.yaml take precedence, so a stale or incorrect image.registry block can override valid server-level credentials.
Use server-level credentials when:
- Many targets on the same server pull from the same registry
- You want Docker Hub authentication without repeating credentials in every
haloy.yaml - You want to manage registry credentials from your local machine instead of SSHing into the server
Use image.registry when credentials should be specific to one image or target, or when you want credentials resolved from local environment variables or secret providers during haloy deploy.
Basic Authentication
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
value: "your-username"
password:
value: "your-password"
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
value: "your-username"
password:
value: "your-password"
With Environment Variables
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
from:
env: "REGISTRY_USERNAME"
password:
from:
env: "REGISTRY_PASSWORD"
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
from:
env: "REGISTRY_USERNAME"
password:
from:
env: "REGISTRY_PASSWORD"
Then set the environment variables:
export REGISTRY_USERNAME="your-username"
export REGISTRY_PASSWORD="your-token"
haloy deploy
export REGISTRY_USERNAME="your-username"
export REGISTRY_PASSWORD="your-token"
haloy deploy
Tip: You have the option to define environment variables in files which the haloy CLI tool will automatically load. See Environment Files for more details.
With Secret Providers
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
from:
secret: "onepassword:registry-credentials:username"
password:
from:
secret: "onepassword:registry-credentials:password"
secret_providers:
onepassword:
registry-credentials:
vault: "Infrastructure"
item: "GitHub Container Registry"
name: "my-app"
image:
repository: "ghcr.io/your-org/private-app"
tag: "latest"
registry:
username:
from:
secret: "onepassword:registry-credentials:username"
password:
from:
secret: "onepassword:registry-credentials:password"
secret_providers:
onepassword:
registry-credentials:
vault: "Infrastructure"
item: "GitHub Container Registry"
Custom Registry Server
name: "my-app"
image:
repository: "myregistry.example.com/my-app"
tag: "latest"
registry:
server: "myregistry.example.com"
username:
value: "your-username"
password:
value: "your-password"
name: "my-app"
image:
repository: "myregistry.example.com/my-app"
tag: "latest"
registry:
server: "myregistry.example.com"
username:
value: "your-username"
password:
value: "your-password"
The server field is optional. Haloy auto-detects it from your repository:
ghcr.io/your-org/app→ghcr.iomyregistry.example.com/my-app→myregistry.example.comyour-username/app→docker.io(Docker Hub)
Registry Examples
GitHub Container Registry (GHCR):
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
registry:
username:
value: "your-github-username"
password:
value: "ghp_your_personal_access_token"
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
registry:
username:
value: "your-github-username"
password:
value: "ghp_your_personal_access_token"
Docker Hub:
image:
repository: "your-dockerhub-username/private-app"
tag: "latest"
registry:
username:
value: "your-dockerhub-username"
password:
value: "your-dockerhub-token"
image:
repository: "your-dockerhub-username/private-app"
tag: "latest"
registry:
username:
value: "your-dockerhub-username"
password:
value: "your-dockerhub-token"
Local Image Building
Haloy can build Docker images locally and distribute them to your servers, eliminating the need for CI/CD pipelines.
Build Configuration
| Key | Type | Required | Description |
|---|---|---|---|
context | string | No | Build context directory, relative to config file (default: ”.”) |
dockerfile | string | No | Path to Dockerfile, relative to config file (default: “Dockerfile”) |
platform | string | No | Target platform (default: “linux/amd64”) |
args | array | No | Build arguments |
push | string | No | Where to push: “registry” or “server” (auto-detected) |
Example:
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
args:
- name: NODE_ENV
value: "production"
- name: API_URL
from:
env: API_URL
push: "registry"
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
args:
- name: NODE_ENV
value: "production"
- name: API_URL
from:
env: API_URL
push: "registry"
Dockerfile and Context
Both context and dockerfile paths are resolved relative to the config file’s directory.
# Dockerfile inside context directory
build_config:
context: "./dockerfiles/static"
dockerfile: "./dockerfiles/static/Dockerfile-static"
# Shared Dockerfile with different context
build_config:
context: "./apps/frontend"
dockerfile: "./dockerfiles/Dockerfile-node"
# Dockerfile at root, context is a subdirectory
build_config:
context: "./src"
dockerfile: "./Dockerfile"
# Dockerfile inside context directory
build_config:
context: "./dockerfiles/static"
dockerfile: "./dockerfiles/static/Dockerfile-static"
# Shared Dockerfile with different context
build_config:
context: "./apps/frontend"
dockerfile: "./dockerfiles/Dockerfile-node"
# Dockerfile at root, context is a subdirectory
build_config:
context: "./src"
dockerfile: "./Dockerfile"
Push to Server (No Registry Required)
Build locally and upload directly to your server:
name: "my-app"
server: "haloy.yourserver.com"
image:
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
# push: "server" is automatically detected
domains:
- domain: "my-app.com"
name: "my-app"
server: "haloy.yourserver.com"
image:
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
# push: "server" is automatically detected
domains:
- domain: "my-app.com"
When pushing to the server, Haloy automatically optimizes uploads using layer-based caching—similar to how Docker registries work. Only layers that don’t already exist on the server are uploaded, so shared base layers (like node:22-alpine or nginx:alpine) are cached and reused across all your applications, making subsequent deploys significantly faster.
Push to Registry
Build locally and push to a registry:
name: "my-app"
server: "haloy.yourserver.com"
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
registry:
username:
from:
env: "GITHUB_USERNAME"
password:
from:
env: "GITHUB_TOKEN"
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
# push: "registry" is automatically detected
domains:
- domain: "my-app.com"
name: "my-app"
server: "haloy.yourserver.com"
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
registry:
username:
from:
env: "GITHUB_USERNAME"
password:
from:
env: "GITHUB_TOKEN"
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
# push: "registry" is automatically detected
domains:
- domain: "my-app.com"
Build Arguments
Pass build-time variables:
image:
repository: "my-app"
tag: "latest"
build_config:
args:
# Direct value
- name: "NODE_ENV"
value: "production"
# From environment variable
- name: "BUILD_VERSION"
from:
env: "VERSION"
# From secret provider
- name: "NPM_TOKEN"
from:
secret: "onepassword:build-secrets:npm-token"
# Pass through from shell environment
- name: "GITHUB_TOKEN"
image:
repository: "my-app"
tag: "latest"
build_config:
args:
# Direct value
- name: "NODE_ENV"
value: "production"
# From environment variable
- name: "BUILD_VERSION"
from:
env: "VERSION"
# From secret provider
- name: "NPM_TOKEN"
from:
secret: "onepassword:build-secrets:npm-token"
# Pass through from shell environment
- name: "GITHUB_TOKEN"
Tip: If you need the same variable as both a runtime environment variable and a build argument, you can define it once in env with build_arg: true instead of duplicating it. See Build Arguments from Environment Variables for details.
Multi-Target with Shared Build
Build once, deploy to multiple targets:
name: "my-app"
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
targets:
production:
server: "prod.haloy.com"
image:
build_config:
push: "server" # Push directly to production server
domains:
- domain: "my-app.com"
staging:
server: "staging.haloy.com"
image:
registry:
username:
from:
env: "GITHUB_USERNAME"
password:
from:
env: "GITHUB_TOKEN"
build_config:
push: "registry" # Push to registry for staging
domains:
- domain: "staging.my-app.com"
name: "my-app"
image:
repository: "ghcr.io/your-org/my-app"
tag: "latest"
build_config:
context: "."
dockerfile: "Dockerfile"
platform: "linux/amd64"
targets:
production:
server: "prod.haloy.com"
image:
build_config:
push: "server" # Push directly to production server
domains:
- domain: "my-app.com"
staging:
server: "staging.haloy.com"
image:
registry:
username:
from:
env: "GITHUB_USERNAME"
password:
from:
env: "GITHUB_TOKEN"
build_config:
push: "registry" # Push to registry for staging
domains:
- domain: "staging.my-app.com"
When to Use Each Method
Push to Server (push: "server"):
- No Docker registry required
- Faster for small deployments
- Simpler setup for single-server deployments
- Ideal for personal projects and development
Push to Registry (push: "registry"):
- Better for multi-server deployments
- Images cached in registry for faster subsequent deploys
- Supports external image inspection and scanning
- Recommended for production environments
Image History & Rollback
Configure how Haloy manages image history for rollbacks.
Rollback Strategies
| Strategy | Description | Use Case |
|---|---|---|
local | Keep images locally (default) | Fast rollbacks, local development |
registry | Rely on registry tags | Save disk space, versioned releases |
none | No rollback support | Minimal storage, no rollback needs |
Local Strategy (Default)
Haloy tags images with deployment IDs and keeps them locally:
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "latest"
history:
strategy: "local"
count: 5 # Keep 5 images locally
domains:
- domain: "my-app.com"
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "latest"
history:
strategy: "local"
count: 5 # Keep 5 images locally
domains:
- domain: "my-app.com"
Pros: Fast rollbacks, no registry required Cons: Uses disk space
Registry Strategy
Rely on registry tags for rollbacks:
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "v1.2.3" # Must use immutable tags
history:
strategy: "registry"
count: 10 # Track 10 deployment versions
pattern: "v*" # Match versioned tags for rollbacks
domains:
- domain: "my-app.com"
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "v1.2.3" # Must use immutable tags
history:
strategy: "registry"
count: 10 # Track 10 deployment versions
pattern: "v*" # Match versioned tags for rollbacks
domains:
- domain: "my-app.com"
Requirements:
- Use immutable tags (no “latest”, “main”, etc.)
- Tags must match the pattern
- Registry must be accessible
Pros: Saves local disk space Cons: Requires tagging discipline, registry dependency
None Strategy
Disable rollback capability:
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "latest"
history:
strategy: "none"
domains:
- domain: "my-app.com"
name: "my-app"
image:
repository: "ghcr.io/my-org/my-app"
tag: "latest"
history:
strategy: "none"
domains:
- domain: "my-app.com"
Pros: Minimal resource usage Cons: No rollback capability
Named Images for Multi-Target Configs
You can define shared images once at the root and let targets choose with image_key.
name: "my-app"
server: "haloy.yourserver.com"
images:
web:
repository: "ghcr.io/your-org/my-app-web"
tag: "v1.2.3"
worker: "ghcr.io/your-org/my-app-worker:v1.2.3"
targets:
production:
image_key: "web"
domains:
- domain: "my-app.com"
jobs:
image_key: "worker"
name: "my-app"
server: "haloy.yourserver.com"
images:
web:
repository: "ghcr.io/your-org/my-app-web"
tag: "v1.2.3"
worker: "ghcr.io/your-org/my-app-worker:v1.2.3"
targets:
production:
image_key: "web"
domains:
- domain: "my-app.com"
jobs:
image_key: "worker"
Notes:
- For a target, use either
imageorimage_key(not both) - Resolution priority is: target
image-> targetimage_key-> rootimage
Complete Example
name: "production-app"
server: "prod.haloy.com"
image:
repository: "ghcr.io/my-org/production-app"
tag: "v1.5.2"
# Registry authentication
registry:
username:
from:
secret: "onepassword:registry:username"
password:
from:
secret: "onepassword:registry:token"
# Local build configuration
build_config:
context: "."
dockerfile: "Dockerfile.prod"
platform: "linux/amd64"
push: "registry"
args:
- name: "NODE_ENV"
value: "production"
- name: "BUILD_VERSION"
value: "v1.5.2"
- name: "NPM_TOKEN"
from:
secret: "onepassword:build:npm-token"
# Rollback configuration
history:
strategy: "registry"
count: 10
pattern: "v*"
secret_providers:
onepassword:
registry:
vault: "Infrastructure"
item: "GitHub Registry"
build:
vault: "Development"
item: "Build Tokens"
domains:
- domain: "production-app.com"
name: "production-app"
server: "prod.haloy.com"
image:
repository: "ghcr.io/my-org/production-app"
tag: "v1.5.2"
# Registry authentication
registry:
username:
from:
secret: "onepassword:registry:username"
password:
from:
secret: "onepassword:registry:token"
# Local build configuration
build_config:
context: "."
dockerfile: "Dockerfile.prod"
platform: "linux/amd64"
push: "registry"
args:
- name: "NODE_ENV"
value: "production"
- name: "BUILD_VERSION"
value: "v1.5.2"
- name: "NPM_TOKEN"
from:
secret: "onepassword:build:npm-token"
# Rollback configuration
history:
strategy: "registry"
count: 10
pattern: "v*"
secret_providers:
onepassword:
registry:
vault: "Infrastructure"
item: "GitHub Registry"
build:
vault: "Development"
item: "Build Tokens"
domains:
- domain: "production-app.com"
Security Best Practices
- Use tokens, not passwords: Use access tokens for registry authentication
- Store credentials securely: Use secret providers or environment variables
- Rotate credentials regularly: Update tokens periodically
- Use read-only tokens when possible: Limit registry permissions
- Never commit credentials: Add sensitive files to
.gitignore
Next Steps
Stay updated on Haloy
Get notified about new docs, deployment patterns, and Haloy updates.