Dispenser is a simple, declarative, and deterministic container orchestrator designed for single virtual machines. It combines continuous deployment (CD), a built-in reverse proxy with automatic SSL, and cron scheduling into a single binary, eliminating the need for complex external tooling or manual bash scripts.
This tool manages containerized applications by continuously monitoring your artifact registry for new versions of Docker images. When updates are detected, dispenser automatically redeploys your services, ensuring the running containers on the host machine match the latest versions in your registry.
dispenser operates as a daemon that runs in the background on the host server that watches your artifact registry, detecting when new versions of your container images are published.
- CLI Reference - Complete command-line options and usage
- Service Configuration - Detailed
service.tomlreference - Reverse Proxy - Built-in proxy and SSL management
- Network Configuration - Docker network setup guide
- Cron Scheduling - Scheduled deployments
- GCP Secrets - Google Secret Manager integration
Before installing Dispenser, ensure the following are installed on your server:
- Docker Engine: Dispenser orchestrates Docker container deployments.
- pass: The standard Unix password manager, used for securely storing registry credentials. It is often available in base repositories or EPEL on RedHat systems.
Download the latest .deb or .rpm package from the releases page.
# Download the .deb package
# wget https://github.com/ixpantia/dispenser/releases/download/v0.11.0/dispenser-0.11.0.0.0-0.x86_64.deb
sudo apt install ./dispenser-0.11.0.0.0-0.x86_64.deb# Download the .rpm package
# wget ...
sudo dnf install ./dispenser-0.11.0.0.0-0.x86_64.rpmThe installation process will:
- Create a dedicated system user named
dispenserwith its home directory at/opt/dispenser. - Add the
dispenseruser to thedockergroup. - Create a default configuration file at
/opt/dispenser/dispenser.toml. - Install and enable a systemd service to run Dispenser automatically.
The following steps guide you through setting up your first continuous deployment.
For security, all configuration is managed by the dispenser user.
sudo su dispenser
cd ~You are now in the /opt/dispenser directory. You will see the dispenser.toml configuration file here.
If your Docker images are stored in a private registry (like GHCR, Docker Hub private repos, etc.), the server needs to be authenticated to pull them.
- Generate an access token from your registry provider with
readpermissions for packages/images. - Log in using the Docker CLI. Replace
<your_registry>and<your_username>accordingly. Paste your access token when prompted for a password.
# Example for GitHub Container Registry (ghcr.io)
docker login ghcr.io -u <your_username>Docker will securely store the credentials in the dispenser user's home directory.
Dispenser deploys applications based on a service.toml file.
-
Create a directory for your application inside
/opt/dispenser. Let's call itmy-app.mkdir my-app cd my-app -
Create a
service.tomlfile that defines your service.vim service.toml
Paste your service definition. Here's a basic example:
# Service metadata (required) [service] name = "my-app" image = "ghcr.io/my-org/my-app:latest" # Restart policy (optional, defaults to "no") restart = "always" # Port mappings (optional) [[port]] host = 8080 container = 80 # Environment variables (optional) [env] DATABASE_URL = "postgres://user:password@host:port/db" API_KEY = "your_secret_api_key" # Dispenser-specific configuration (required) [dispenser] # Watch for image updates watch = true # Initialize immediately on startup initialize = "immediately"
Now, tell Dispenser about your service so it can monitor it for updates.
-
Return to the
dispenserhome directory and edit the configuration file.cd ~ vim dispenser.toml
-
Add a
[[service]]block to the file. This tells Dispenser where your application is located.# How often to check for new images, in seconds. delay = 60 [[service]] # Path is relative to /opt/dispenser path = "my-app"
Dispenser also supports scheduled deployments using
cronexpressions. For more details on configuring periodic restarts, see the cron documentation.
By default, Dispenser starts services as soon as the application launches. However, you can control this behavior using the initialize option in your service's service.toml file. This is particularly useful for services that should only run on a specific schedule.
The initialize option can be set to one of two values:
immediately(Default): The service is started as soon as Dispenser starts. If you don't specify theinitializeoption, this is the default behavior.on-trigger: The service will not start on application launch. Instead, it will be initialized only when a trigger occurs. Triggers can be either a cron schedule or a detected update to a watched image.
This is the default behavior. The following configuration will start the service immediately.
# my-app/service.toml
[service]
name = "my-app"
image = "ghcr.io/my-org/my-app:latest"
[[port]]
host = 8080
container = 80
[dispenser]
watch = true
initialize = "immediately" # This is the defaultThis configuration is useful for scheduled tasks. The service will not start immediately. Instead, it will be triggered to run based on the cron schedule.
# backup-service/service.toml
[service]
name = "backup-job"
image = "ghcr.io/my-org/backup:latest"
[[volume]]
source = "./backups"
target = "/backups"
[dispenser]
watch = false
initialize = "on-trigger"
cron = "0 3 * * *" # Run every day at 3 AMIn this example, the service defined in the backup-service directory will only be started when the cron schedule is met. After its first run, it will continue to be managed by its cron schedule.
Dispenser supports using variables in your configuration files via dispenser.vars or any file ending in .dispenser.vars. These files allow you to define values that can be reused inside dispenser.toml and service.toml files using ${VARIABLE} syntax.
Note: While Dispenser uses the ${} syntax similar to Docker Compose, it does not support all Docker Compose interpolation features (such as default values :- or error messages :?).
Variables defined in these files are substituted directly into your configuration files during loading.
This is useful for reusing the same configuration in multiple deployments.
-
Create a
dispenser.varsfile (or*.dispenser.vars) in/opt/dispenser.vim dispenser.vars
-
Define your variables in TOML format.
registry_url = "ghcr.io" app_version = "latest" org_name = "my-org"
Dispenser also supports fetching secrets from Google Secret Manager. For more details on configuring secrets, see the GCP secrets documentation.
-
Use these variables in your
dispenser.toml.delay = 60 [[service]] path = "my-app"
-
Use these variables in your
service.toml.[service] name = "my-app" image = "${registry_url}/${org_name}/my-app:${app_version}" [[port]] host = 8080 container = 80 [dispenser] watch = true initialize = "immediately"
Dispenser automatically creates a default network called dispenser that all containers are connected to. This network uses the subnet 172.28.0.0/16 with gateway 172.28.0.1, allowing all your services to communicate with each other using their service names as hostnames without any configuration.
For example, if you have two services api and postgres, the api service can connect to the database using postgres as the hostname (e.g., postgres://postgres:5432/mydb).
In addition to the default network, you can declare custom networks in dispenser.toml for more fine-grained control over container communication.
-
Declare custom networks in your
dispenser.toml.delay = 60 # Network declarations [[network]] name = "app-network" driver = "bridge" [[service]] path = "my-app" [[service]] path = "my-database"
-
Reference networks in your service configurations.
# my-app/service.toml [service] name = "my-app" image = "ghcr.io/my-org/my-app:latest" [[port]] host = 8080 container = 80 [[network]] name = "app-network" [dispenser] watch = true initialize = "immediately"
# my-database/service.toml [service] name = "postgres-db" image = "postgres:15" [env] POSTGRES_PASSWORD = "secretpassword" [[network]] name = "app-network" [dispenser] watch = false initialize = "immediately"
Dispenser includes a built-in reverse proxy that handles TLS termination and routes traffic to your services using the
Hostheader. The proxy is enabled by default and listens on ports 80 and 443. You can explicitly disable it in your maindispenser.tomlif you are using an external proxy.-
Add a
[proxy]block to yourservice.toml.[proxy] host = "app.example.com" service_port = 8080
-
(Optional) Enable Let's Encrypt in
dispenser.tomlto automatically manage certificates. Note: This section must be explicitly added; otherwise, Dispenser expects manual certificates.[certbot] email = "admin@example.com"
-
(Optional) Disable the proxy globally in
dispenser.toml. Note: Changing this setting requires a full process restart to take effect.[proxy] enabled = false
-
(Optional) Configure the proxy strategy in
dispenser.toml. Choose betweenhttps-only(default),http-only, orboth.[proxy] strategy = "http-only"
For more details, see the Reverse Proxy Guide.
-
Now both services can communicate with each other using their service names as hostnames. Note that even without the explicit [[network]] declarations, both services would still be able to communicate via the default dispenser network.
For advanced network configuration including external networks, internal networks, labels, and different drivers, see the Network Configuration Guide.
The service.toml format supports many advanced features. For a complete reference of all available configuration options, see the Service Configuration Reference.
[[volume]]
source = "./data"
target = "/app/data"
[[volume]]
source = "./config"
target = "/app/config"
readonly = true[service]
name = "worker"
image = "python:3.11"
command = ["python", "worker.py", "--verbose"]
working_dir = "/app"[service]
name = "my-app"
image = "my-app:latest"
memory = "512m"
cpus = "1.0"[service]
name = "my-app"
image = "my-app:latest"
user = "1000:1000"
hostname = "myapp-container"Before applying changes, you can validate your configuration files (including variable substitution) to ensure there are no syntax errors or missing variables.
Run dispenser with the --test (or -t) flag:
dispenser --testIf the configuration is valid, it will output:
Dispenser config is ok.
For more command-line options, see the CLI Reference.
If there's an error, dispenser will show you a detailed error message.
---------------------------------- <string> -----------------------------------
2 |
3 | [service]
4 | name = "nginx"
5 > image = "${missing}/nginx:latest"
i ^^^^^^^^^^ undefined value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No referenced variables
-------------------------------------------------------------------------------
For local development or testing, use the dev subcommand to run specific services without loading your entire stack.
# Run only the 'api' service with self-signed certificates
dispenser dev -s apiThe dev command:
- Implicitly enables simulation: Generates self-signed certificates on the fly for all proxy hosts.
- Selective loading: Only reads and renders configuration for services matching the filter.
- Dependency pruning: Automatically removes dependencies on services that are not part of the current run, so your services start immediately.
-
Exit the
dispenseruser session to return to your regular user.exit -
Restart the Dispenser service to apply the new configuration.
sudo systemctl restart dispenser
-
Check that the service is running correctly.
sudo systemctl status dispenser
You should see
active (running). -
Verify that your application container is running.
sudo docker ps
You should see a container running with the
ghcr.io/my-org/my-app:latestimage.
From now on, whenever you push a new image to your registry with the latest tag (and watch = true is set in the service configuration), Dispenser will automatically detect it, pull the new version, and redeploy your service with zero downtime.
Dispenser includes a built-in mechanism to send signals to the running daemon using the -s or --signal flag. This allows you to reload the configuration or stop the service without needing to use kill manually.
Note: This command relies on the dispenser.pid file, so you should run it from the same directory where Dispenser is running (typically /opt/dispenser for the default installation).
For complete CLI documentation including all available flags, see the CLI Reference.
Reload Configuration:
To reload the dispenser.toml configuration without restarting the process:
dispenser -s reloadThis is useful for adding new services or changing configuration parameters without interrupting currently monitored services.
Stop Service:
To gracefully stop the Dispenser daemon:
dispenser -s stop- CLI Reference - All command-line flags and options
- Service Configuration Reference - Complete field documentation
- Reverse Proxy - Proxy and SSL configuration
- Network Configuration Guide - Advanced networking setup
- Cron Documentation - Scheduled deployments
- GCP Secrets Integration - Using Google Secret Manager
- Migration Guide - Migrating from Docker Compose format
Before you try to build an rpm package, make sure you have the following installed:
cargo: Rust package manager and build toolrustc: Rust compilermake: Run make filesrpmbuild: Tool to build RPMs
Once these dependencies are installed run:
make build-rpm
This should create a file called something along the lines of
../dispenser-$VERSION.x86_64.rpm. There may be minor variations on the Linux
distribution where you are building the package.
Before you try to build a deb package, make sure you have the following installed:
cargo: Rust package manager and build toolrustc: Rust compilermake: Run make filesdpkg-dev: Tool to build DEB files
Once these dependencies are installed run:
make build-deb
This should create a file called something along the lines of
./dispenser.deb. There may be minor variations on the Linux
distribution where you are building the package.