Skip to content

ixpantia/dispenser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dispenser

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.

Documentation

Prerequisites

Before installing Dispenser, ensure the following are installed on your server:

Installation

Download the latest .deb or .rpm package from the releases page.

Debian / Ubuntu

# 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

RHEL / CentOS / Fedora

# Download the .rpm package
# wget ...

sudo dnf install ./dispenser-0.11.0.0.0-0.x86_64.rpm

The installation process will:

  1. Create a dedicated system user named dispenser with its home directory at /opt/dispenser.
  2. Add the dispenser user to the docker group.
  3. Create a default configuration file at /opt/dispenser/dispenser.toml.
  4. Install and enable a systemd service to run Dispenser automatically.

Configuration and Usage

The following steps guide you through setting up your first continuous deployment.

Step 1: Switch to the dispenser User

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.

Step 2: Authenticate with a Private Registry (Optional)

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.

  1. Generate an access token from your registry provider with read permissions for packages/images.
  2. 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.

Step 3: Prepare Your Application Directory

Dispenser deploys applications based on a service.toml file.

  1. Create a directory for your application inside /opt/dispenser. Let's call it my-app.

    mkdir my-app
    cd my-app
  2. Create a service.toml file 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"

Step 4: Configure Dispenser to Monitor Your Service

Now, tell Dispenser about your service so it can monitor it for updates.

  1. Return to the dispenser home directory and edit the configuration file.

    cd ~
    vim dispenser.toml
  2. 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 cron expressions. For more details on configuring periodic restarts, see the cron documentation.

Step 5: Service Initialization (Optional)

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 the initialize option, 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.

Example: Immediate Initialization

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 default

Example: Initialization on Trigger

This 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 AM

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

Step 6: Using Variables (Optional)

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.

  1. Create a dispenser.vars file (or *.dispenser.vars) in /opt/dispenser.

    vim dispenser.vars
  2. 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.

  3. Use these variables in your dispenser.toml.

    delay = 60
    
    [[service]]
    path = "my-app"
  4. 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"

Step 7: Working with Networks (Optional)

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.

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

    Step 9: Reverse Proxy and SSL

    Dispenser includes a built-in reverse proxy that handles TLS termination and routes traffic to your services using the Host header. The proxy is enabled by default and listens on ports 80 and 443. You can explicitly disable it in your main dispenser.toml if you are using an external proxy.

    1. Add a [proxy] block to your service.toml.

      [proxy]
      host = "app.example.com"
      service_port = 8080
    2. (Optional) Enable Let's Encrypt in dispenser.toml to automatically manage certificates. Note: This section must be explicitly added; otherwise, Dispenser expects manual certificates.

      [certbot]
      email = "admin@example.com"
    3. (Optional) Disable the proxy globally in dispenser.toml. Note: Changing this setting requires a full process restart to take effect.

      [proxy]
      enabled = false
    4. (Optional) Configure the proxy strategy in dispenser.toml. Choose between https-only (default), http-only, or both.

      [proxy]
      strategy = "http-only"

    For more details, see the Reverse Proxy Guide.

    Step 10: Validating Configuration

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.

Step 8: Advanced Service Configuration

The service.toml format supports many advanced features. For a complete reference of all available configuration options, see the Service Configuration Reference.

Volume Mounts

[[volume]]
source = "./data"
target = "/app/data"

[[volume]]
source = "./config"
target = "/app/config"
readonly = true

Custom Commands and Working Directory

[service]
name = "worker"
image = "python:3.11"
command = ["python", "worker.py", "--verbose"]
working_dir = "/app"

Resource Limits

[service]
name = "my-app"
image = "my-app:latest"
memory = "512m"
cpus = "1.0"

User and Hostname

[service]
name = "my-app"
image = "my-app:latest"
user = "1000:1000"
hostname = "myapp-container"

Step 9: Validating Configuration

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 --test

If 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
-------------------------------------------------------------------------------

Step 11: Local Development with dev mode

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 api

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

Step 12: Start and Verify the Deployment

  1. Exit the dispenser user session to return to your regular user.

    exit
  2. Restart the Dispenser service to apply the new configuration.

    sudo systemctl restart dispenser
  3. Check that the service is running correctly.

    sudo systemctl status dispenser

    You should see active (running).

  4. Verify that your application container is running.

    sudo docker ps

    You should see a container running with the ghcr.io/my-org/my-app:latest image.

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.

Managing the Service with CLI Signals

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 reload

This 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

Additional Resources

Building from Source

RPM (RHEL)

Before you try to build an rpm package, make sure you have the following installed:

  • cargo: Rust package manager and build tool
  • rustc: Rust compiler
  • make: Run make files
  • rpmbuild: 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.

Deb (Debian & Ubuntu)

Before you try to build a deb package, make sure you have the following installed:

  • cargo: Rust package manager and build tool
  • rustc: Rust compiler
  • make: Run make files
  • dpkg-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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 5

Languages