Image Configuration

Configure Docker images, registry authentication, local building, and rollback strategies.

Basic Image Configuration

KeyTypeRequiredDescription
repositorystringNoDocker image name (defaults to the target/app name when omitted)
tagstringNoImage tag (default: “latest”). Can also be included in repository (e.g., nginx:alpine)
pull_policystringNoWhen to pull from the registry: “always”, “if_missing”, or “never”
buildbooleanNoWhether to build the image locally
registryobjectNoPrivate registry authentication
historyobjectNoImage history and rollback strategy
build_configobjectNoBuild 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"

When to use:

  • Use root image when every target should share one default image
  • Use root images + target image_key when targets need different images (for example web vs worker)

Rules:

  • A target can set either image or image_key (not both)
  • Resolution priority is: target image -> target image_key -> root image

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"

This is equivalent to:

name: "my-app" image: repository: "nginx:alpine"

The shorthand also works in the images map:

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"

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"

Note: You can also specify the tag separately using the tag field:

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.

ValueBehavior
alwaysCheck the registry and pull when the local image is missing or outdated. This is default.
if_missingUse the local image if it already exists on the server. Pull only when it is missing.
neverNever pull. The image must already exist on the server.
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"

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

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"

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"

Then set the environment variables:

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"

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"

The server field is optional. Haloy auto-detects it from your repository:

  • ghcr.io/your-org/appghcr.io
  • myregistry.example.com/my-appmyregistry.example.com
  • your-username/appdocker.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"

Docker Hub:

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

KeyTypeRequiredDescription
contextstringNoBuild context directory, relative to config file (default: ”.”)
dockerfilestringNoPath to Dockerfile, relative to config file (default: “Dockerfile”)
platformstringNoTarget platform (default: “linux/amd64”)
argsarrayNoBuild arguments
pushstringNoWhere 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"

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"

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"

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"

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"

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"

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

StrategyDescriptionUse Case
localKeep images locally (default)Fast rollbacks, local development
registryRely on registry tagsSave disk space, versioned releases
noneNo rollback supportMinimal 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"

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"

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"

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"

Notes:

  • For a target, use either image or image_key (not both)
  • Resolution priority is: target image -> target image_key -> root image

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"

Security Best Practices

  1. Use tokens, not passwords: Use access tokens for registry authentication
  2. Store credentials securely: Use secret providers or environment variables
  3. Rotate credentials regularly: Update tokens periodically
  4. Use read-only tokens when possible: Limit registry permissions
  5. Never commit credentials: Add sensitive files to .gitignore

Next Steps

Stay updated on Haloy

Get notified about new docs, deployment patterns, and Haloy updates.