Services
Dagger Functions support service containers, enabling users to spin up additional long-running services (as containers) and communicate with those services from Dagger Functions.
This makes it possible to:
- Instantiate and return services from a Dagger Function, and then:
- Use those services in other Dagger Functions (container-to-container networking)
- Use those services from the calling host (container-to-host networking)
- Expose host services for use in a Dagger Function (host-to-container networking).
Some common scenarios for using services with Dagger Functions are:
- Running a database service for local storage or testing
- Running end-to-end integration tests against a service
- Running sidecar services
Service containers
Services instantiated by a Dagger Function run in service containers, which have the following characteristics:
- Each service container has a canonical, content-addressed hostname and an optional set of exposed ports.
- Service containers are started just-in-time, de-duplicated, and stopped when no longer needed.
- Service containers are health checked prior to running clients.
Bind services in functions
A Dagger Function can create and return a service, which can then be used from another Dagger Function or from the calling host. Services in Dagger Functions are returned using the Service
core type.
Here is an example of a Dagger Function that returns an HTTP service. This service is used by another Dagger Function, which creates a service binding using the alias www
and then accesses the HTTP service using this alias.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Start and return an HTTP service
func (m *MyModule) HttpService() *dagger.Service {
return dag.Container().
From("python").
WithWorkdir("/srv").
WithNewFile("index.html", "Hello, world!").
WithExposedPort(8080).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"python", "-m", "http.server", "8080"}})
}
// Send a request to an HTTP service and return the response
func (m *MyModule) Get(ctx context.Context) (string, error) {
return dag.Container().
From("alpine").
WithServiceBinding("www", m.HttpService()).
WithExec([]string{"wget", "-O-", "http://www:8080"}).
Stdout(ctx)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
def http_service(self) -> dagger.Service:
"""Start and return an HTTP service."""
return (
dag.container()
.from_("python")
.with_workdir("/srv")
.with_new_file("index.html", "Hello, world!")
.with_exposed_port(8080)
.as_service(args=["python", "-m", "http.server", "8080"])
)
@function
async def get(self) -> str:
"""Send a request to an HTTP service and return the response."""
return await (
dag.container()
.from_("alpine")
.with_service_binding("www", self.http_service())
.with_exec(["wget", "-O-", "http://www:8080"])
.stdout()
)
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Start and return an HTTP service
*/
@func()
httpService(): Service {
return dag
.container()
.from("python")
.withWorkdir("/srv")
.withNewFile("index.html", "Hello, world!")
.withExposedPort(8080)
.asService({ args: ["python", "-m", "http.server", "8080"] })
}
/**
* Send a request to an HTTP service and return the response
*/
@func()
async get(): Promise<string> {
return await dag
.container()
.from("alpine")
.withServiceBinding("www", this.httpService())
.withExec(["wget", "-O-", "http://www:8080"])
.stdout()
}
}
Here is an example call for this Dagger Function:
dagger call get
The result will be:
Hello, world!
Expose services returned by functions to the host
Services returned by Dagger Functions can also be exposed directly to the host. This enables clients on the host to communicate with services running in Dagger.
One use case is for testing, where you need to be able to spin up ephemeral databases against which to run tests. You might also use this to access a web UI in a browser on your desktop.
Here is another example call for the Dagger Function shown previously, this time exposing the HTTP service on the host
dagger call http-service up
By default, each service port maps to the same port on the host - in this case, port 8080. The service can then be accessed by clients on the host. Here's an example:
curl localhost:8080
The result will be:
Hello, world!
To specify a different mapping, use the additional --ports
argument to dagger call ... up
with a list of host/service port mappings. Here's an example, which exposes the service on host port 9000:
dagger call http-service up --ports 9000:8080
To bind ports randomly, use the --random
argument.
Expose host services to functions
Dagger Functions can also receive host services as function arguments of type Service
, in the form tcp://<host>:<port>
. This enables client containers in Dagger Functions to communicate with services running on the host.
This implies that a service is already listening on a port on the host, out-of-band of Dagger.
Here is an example of how a container running in a Dagger Function can access and query a MariaDB database service (bound using the alias db
) running on the host.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Send a query to a MariaDB service and return the response
func (m *MyModule) UserList(
ctx context.Context,
// Host service
svc *dagger.Service,
) (string, error) {
return dag.Container().
From("mariadb:10.11.2").
WithServiceBinding("db", svc).
WithExec([]string{"/usr/bin/mysql", "--user=root", "--password=secret", "--host=db", "-e", "SELECT Host, User FROM mysql.user"}).
Stdout(ctx)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
async def user_list(
self, svc: Annotated[dagger.Service, Doc("Host service")]
) -> str:
"""Send a query to a MariaDB service and return the response."""
return await (
dag.container()
.from_("mariadb:10.11.2")
.with_service_binding("db", svc)
.with_exec(
[
"/usr/bin/mysql",
"--user=root",
"--password=secret",
"--host=db",
"-e",
"SELECT Host, User FROM mysql.user",
]
)
.stdout()
)
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Send a query to a MariaDB service and returns the response
*/
@func()
async userList(
/**
* Host service
*/
svc: Service,
): Promise<string> {
return await dag
.container()
.from("mariadb:10.11.2")
.withServiceBinding("db", svc)
.withExec([
"/usr/bin/mysql",
"--user=root",
"--password=secret",
"--host=db",
"-e",
"SELECT Host, User FROM mysql.user",
])
.stdout()
}
}
Before calling this Dagger Function, use the following command to start a MariaDB database service on the host:
docker run --rm --detach -p 3306:3306 --name my-mariadb --env MARIADB_ROOT_PASSWORD=secret mariadb:10.11.2
Here is an example call for this Dagger Function:
dagger call user-list --svc=tcp://localhost:3306
The result will be:
Host User
% root
localhost mariadb.sys
localhost root
Create interdependent services
Global hostnames can be assigned to services. This feature is especially valuable for complex networking configurations, such as circular dependencies between services, by allowing services to reference each other by predefined hostnames, without requiring an explicit service binding.
Custom hostnames follow a structured format (<host>.<module id>.<session id>.dagger.local
), ensuring unique identifiers across modules and sessions.
For example, you can now run two services that depend on each other, each using a hostname to refer to the other by name:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Run two services which are dependent on each other
func (m *MyModule) Services(ctx context.Context) (*dagger.Service, error) {
svcA := dag.Container().From("nginx").
WithExposedPort(80).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"sh", "-c", `nginx & while true; do curl svcb:80 && sleep 1; done`}}).
WithHostname("svca")
_, err := svcA.Start(ctx)
if err != nil {
return nil, err
}
svcB := dag.Container().From("nginx").
WithExposedPort(80).
AsService(dagger.ContainerAsServiceOpts{Args: []string{"sh", "-c", `nginx & while true; do curl svca:80 && sleep 1; done`}}).
WithHostname("svcb")
svcB, err = svcB.Start(ctx)
if err != nil {
return nil, err
}
return svcB, nil
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def services(self) -> dagger.Service:
"""Run two services which are dependent on each other"""
svc_a = (
dag.container()
.from_("nginx")
.with_exposed_port(80)
.as_service(
args=[
"sh",
"-c",
"nginx & while true; do curl svcb:80 && sleep 1; done",
]
)
.with_hostname("svca")
)
await svc_a.start()
svc_b = (
dag.container()
.from_("nginx")
.with_exposed_port(80)
.as_service(
args=[
"sh",
"-c",
"nginx & while true; do curl svca:80 && sleep 1; done",
]
)
.with_hostname("svcb")
)
await svc_b.start()
return svc_b
import { dag, object, func, Service } from "@dagger.io/dagger"
@object()
class MyModule {
// Run two services which are dependent on each other
@func()
async services(): Promise<Service> {
const svcA = dag
.container()
.from("nginx")
.withExposedPort(80)
.asService({
args: [
"sh",
"-c",
`nginx & while true; do curl svcb:80 && sleep 1; done`,
],
})
.withHostname("svca")
await svcA.start()
const svcB = dag
.container()
.from("nginx")
.withExposedPort(80)
.asService({
args: [
"sh",
"-c",
`nginx & while true; do curl svca:80 && sleep 1; done`,
],
})
.withHostname("svcb")
await svcB.start()
return svcB
}
}
In this example, service A and service B are set up with custom hostnames svca
and svcb
, allowing each service to communicate with the other by hostname. This capability provides enhanced flexibility for managing service dependencies and interconnections within modular workflows, making it easier to handle complex setups in Dagger.
Here is an example call for this Dagger Function:
dagger call services up --ports 8080:80
Persist service state
Dagger cancels each service run after a 10 second grace period to avoid frequent restarts. To avoid relying on the grace period, use a cache volume to persist a service's data, as in the following example:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Create Redis service and client
func (m *MyModule) Redis(ctx context.Context) *dagger.Container {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
WithMountedCache("/data", dag.CacheVolume("my-redis")).
WithWorkdir("/data").
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv).
WithEntrypoint([]string{"redis-cli", "-h", "redis-srv"})
return redisCLI
}
var execOpts = dagger.ContainerWithExecOpts{
UseEntrypoint: true,
}
// Set key and value in Redis service
func (m *MyModule) Set(
ctx context.Context,
// The cache key to set
key string,
// The cache value to set
value string,
) (string, error) {
return m.Redis(ctx).
WithExec([]string{"set", key, value}, execOpts).
WithExec([]string{"save"}, execOpts).
Stdout(ctx)
}
// Get value from Redis service
func (m *MyModule) Get(
ctx context.Context,
// The cache key to get
key string,
) (string, error) {
return m.Redis(ctx).
WithExec([]string{"get", key}, execOpts).
Stdout(ctx)
}
from typing import Annotated
import dagger
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
@function
def redis(self) -> dagger.Container:
"""Create Redis service and client"""
redis_srv = (
dag.container()
.from_("redis")
.with_exposed_port(6379)
.with_mounted_cache("/data", dag.cache_volume("my-redis"))
.with_workdir("/data")
.as_service(use_entrypoint=True)
)
# create Redis client container
redis_cli = (
dag.container()
.from_("redis")
.with_service_binding("redis-srv", redis_srv)
.with_entrypoint(["redis-cli", "-h", "redis-srv"])
)
return redis_cli
@function
async def set(
self,
key: Annotated[str, Doc("The cache key to set")],
value: Annotated[str, Doc("The cache value to set")],
) -> str:
"""Set key and value in Redis service"""
return (
await self.redis()
.with_exec(["set", key, value], use_entrypoint=True)
.with_exec(["save"], use_entrypoint=True)
.stdout()
)
@function
async def get(
self,
key: Annotated[str, Doc("The cache key to get")],
) -> str:
"""Get value from Redis service"""
return await self.redis().with_exec(["get", key], use_entrypoint=True).stdout()
import { dag, object, func, Container } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Create Redis service and client
*/
@func()
redis(): Container {
const redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.withMountedCache("/data", dag.cacheVolume("my-redis"))
.withWorkdir("/data")
.asService({ useEntrypoint: true })
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
.withEntrypoint(["redis-cli", "-h", "redis-srv"])
return redisCLI
}
/**
* Set key and value in Redis service
*/
@func()
async set(
/**
* The cache key to set
*/
key: string,
/**
* The cache value to set
*/
value: string,
): Promise<string> {
return await this.redis()
.withExec(["set", key, value], { useEntrypoint: true })
.withExec(["save"], { useEntrypoint: true })
.stdout()
}
/**
* Get value from Redis service
*/
@func()
async get(
/**
* The cache key to get
*/
key: string,
): Promise<string> {
// set and save value
return await this.redis()
.withExec(["get", key], { useEntrypoint: true })
.stdout()
}
}
This example uses Redis's SAVE
command to save the service's data to a cache volume. When a new instance of the service is created, it uses the same cache volume to recreate the original state.
Here is an example of using these Dagger Functions:
dagger call set --key=foo --value=123
dagger call get --key=foo
The result will be:
123
Start and stop services
Services are designed to be expressed as a Directed Acyclic Graph (DAG) with explicit bindings allowing services to be started lazily, just like every other DAG node. But sometimes, you may need to explicitly manage the lifecycle in a Dagger Function.
For example, this may be needed if the application in the service has certain behavior on shutdown (such as flushing data) that needs careful coordination with the rest of your logic.
The following example explicitly starts the Redis service and stops it at the end, ensuring the 10 second grace period doesn't get in the way, without the need for a persistent cache volume:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Explicitly start and stop a Redis service
func (m *MyModule) RedisService(ctx context.Context) (string, error) {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
// start Redis ahead of time so it stays up for the duration of the test
redisSrv, err := redisSrv.Start(ctx)
if err != nil {
return "", err
}
// stop the service when done
defer redisSrv.Stop(ctx)
// create Redis client container
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv)
args := []string{"redis-cli", "-h", "redis-srv"}
// set value
setter, err := redisCLI.
WithExec(append(args, "set", "foo", "abc")).
Stdout(ctx)
if err != nil {
return "", err
}
// get value
getter, err := redisCLI.
WithExec(append(args, "get", "foo")).
Stdout(ctx)
if err != nil {
return "", err
}
return setter + getter, nil
}
import contextlib
import dagger
from dagger import dag, function, object_type
@contextlib.asynccontextmanager
async def managed_service(svc: dagger.Service):
"""Start and stop a service."""
yield await svc.start()
await svc.stop()
@object_type
class MyModule:
@function
async def redis_service(self) -> str:
"""Explicitly start and stop a Redis service."""
redis_srv = dag.container().from_("redis").with_exposed_port(6379).as_service()
# start Redis ahead of time so it stays up for the duration of the test
# and stop when done
async with managed_service(redis_srv) as redis_srv:
# create Redis client container
redis_cli = (
dag.container()
.from_("redis")
.with_service_binding("redis-srv", redis_srv)
)
args = ["redis-cli", "-h", "redis-srv"]
# set value
setter = await redis_cli.with_exec([*args, "set", "foo", "abc"]).stdout()
# get value
getter = await redis_cli.with_exec([*args, "get", "foo"]).stdout()
return setter + getter
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Explicitly start and stop a Redis service
*/
@func()
async redisService(): Promise<string> {
let redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.asService()
// start Redis ahead of time so it stays up for the duration of the test
redisSrv = await redisSrv.start()
// stop the service when done
await redisSrv.stop()
// create Redis client container
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
const args = ["redis-cli", "-h", "redis-srv"]
// set value
const setter = await redisCLI
.withExec([...args, "set", "foo", "abc"])
.stdout()
// get value
const getter = await redisCLI.withExec([...args, "get", "foo"]).stdout()
return setter + getter
}
}
Example: MariaDB database service for application tests
The following example demonstrates how services can be used in Dagger Functions, by creating a Dagger Function for application unit/integration testing against a bound MariaDB database service.
The application used in this example is Drupal, a popular open-source PHP CMS. Drupal includes a large number of unit tests, including tests which require an active database connection. All Drupal 10.x tests are written and executed using the PHPUnit testing framework. Read more about running PHPUnit tests in Drupal.
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// Run unit tests against a database service
func (m *MyModule) Test(ctx context.Context) (string, error) {
mariadb := dag.Container().
From("mariadb:10.11.2").
WithEnvVariable("MARIADB_USER", "user").
WithEnvVariable("MARIADB_PASSWORD", "password").
WithEnvVariable("MARIADB_DATABASE", "drupal").
WithEnvVariable("MARIADB_ROOT_PASSWORD", "root").
WithExposedPort(3306).
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
// get Drupal base image
// install additional dependencies
drupal := dag.Container().
From("drupal:10.0.7-php8.2-fpm").
WithExec([]string{"composer", "require", "drupal/core-dev", "--dev", "--update-with-all-dependencies"})
// add service binding for MariaDB
// run kernel tests using PHPUnit
return drupal.
WithServiceBinding("db", mariadb).
WithEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal").
WithEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled").
WithWorkdir("/opt/drupal/web/core").
WithExec([]string{"../../vendor/bin/phpunit", "-v", "--group", "KernelTests"}).
Stdout(ctx)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def test(self) -> str:
"""Run unit tests against a database service."""
# get MariaDB base image
mariadb = (
dag.container()
.from_("mariadb:10.11.2")
.with_env_variable("MARIADB_USER", "user")
.with_env_variable("MARIADB_PASSWORD", "password")
.with_env_variable("MARIADB_DATABASE", "drupal")
.with_env_variable("MARIADB_ROOT_PASSWORD", "root")
.with_exposed_port(3306)
.as_service(use_entrypoint=True)
)
# get Drupal base image
# install additional dependencies
drupal = (
dag.container()
.from_("drupal:10.0.7-php8.2-fpm")
.with_exec(
[
"composer",
"require",
"drupal/core-dev",
"--dev",
"--update-with-all-dependencies",
]
)
)
# add service binding for MariaDB
# run kernel tests using PHPUnit
return await (
drupal.with_service_binding("db", mariadb)
.with_env_variable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.with_env_variable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.with_workdir("/opt/drupal/web/core")
.with_exec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout()
)
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Run unit tests against a database service
*/
@func()
async test(): Promise<string> {
const mariadb = dag
.container()
.from("mariadb:10.11.2")
.withEnvVariable("MARIADB_USER", "user")
.withEnvVariable("MARIADB_PASSWORD", "password")
.withEnvVariable("MARIADB_DATABASE", "drupal")
.withEnvVariable("MARIADB_ROOT_PASSWORD", "root")
.withExposedPort(3306)
.asService({ useEntrypoint: true })
// get Drupal base image
// install additional dependencies
const drupal = dag
.container()
.from("drupal:10.0.7-php8.2-fpm")
.withExec([
"composer",
"require",
"drupal/core-dev",
"--dev",
"--update-with-all-dependencies",
])
// add service binding for MariaDB
// run kernel tests using PHPUnit
return await drupal
.withServiceBinding("db", mariadb)
.withEnvVariable("SIMPLETEST_DB", "mysql://user:password@db/drupal")
.withEnvVariable("SYMFONY_DEPRECATIONS_HELPER", "disabled")
.withWorkdir("/opt/drupal/web/core")
.withExec(["../../vendor/bin/phpunit", "-v", "--group", "KernelTests"])
.stdout()
}
}
Here is an example call for this Dagger Function:
dagger call test
The result will be:
PHPUnit 9.6.17 by Sebastian Bergmann and contributors.
Runtime: PHP 8.2.5
Configuration: /opt/drupal/web/core/phpunit.xml.dist
Testing
.....................S 22 / 22 (100%)
Time: 00:15.806, Memory: 315.00 MB
There was 1 skipped test:
1) Drupal\Tests\pgsql\Kernel\pgsql\KernelTestBaseTest::testSetUp
This test only runs for the database driver 'pgsql'. Current database driver is 'mysql'.
/opt/drupal/web/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificKernelTestBase.php:44
/opt/drupal/vendor/phpunit/phpunit/src/Framework/TestResult.php:728
OK, but incomplete, skipped, or risky tests!
Tests: 22, Assertions: 72, Skipped: 1.
Reference: How service binding works in Dagger Functions
If you're not interested in what's happening in the background, you can skip this section and just trust that services are running when they need to be. If you're interested in the theory, keep reading.
Consider this example:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// creates Redis service and client
func (m *MyModule) RedisService(ctx context.Context) (string, error) {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
// create Redis client container
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv)
// send ping from client to server
return redisCLI.
WithExec([]string{"redis-cli", "-h", "redis-srv", "ping"}).
Stdout(ctx)
}
Here's what happens on the last line:
- The client requests the
ping
container's stdout, which requires the container to run. - Dagger sees that the
ping
container has a service binding,redisSrv
. - Dagger starts the
redisSrv
container, which recurses into this same process. - Dagger waits for health checks to pass against
redisSrv
. - Dagger runs the
ping
container with theredis-srv
alias magically added to/etc/hosts
.
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def redis_service(self) -> str:
"""Creates Redis service and client."""
redis_srv = (
dag.container()
.from_("redis")
.with_exposed_port(6379)
.as_service(use_entrypoint=True)
)
# create Redis client container
redis_cli = (
dag.container().from_("redis").with_service_binding("redis-srv", redis_srv)
)
# send ping from client to server
return await redis_cli.with_exec(
["redis-cli", "-h", "redis-srv", "ping"]
).stdout()
Here's what happens on the last line:
- The client requests the
ping
container's stdout, which requires the container to run. - Dagger sees that the
ping
container has a service binding,redis_srv
. - Dagger starts the
redis_srv
container, which recurses into this same process. - Dagger waits for health checks to pass against
redis_srv
. - Dagger runs the
ping
container with theredis-srv
alias magically added to/etc/hosts
.
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Creates Redis service and client
*/
@func()
async redisService(): Promise<string> {
const redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.asService({ useEntrypoint: true })
// create Redis client container
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
// send ping from client to server
return await redisCLI
.withExec(["redis-cli", "-h", "redis-srv", "ping"])
.stdout()
}
}
Here's what happens on the last line:
- The client requests the
ping
container's stdout, which requires the container to run. - Dagger sees that the
ping
container has a service binding,redisSrv
. - Dagger starts the
redisSrv
container, which recurses into this same process. - Dagger waits for health checks to pass against
redisSrv
. - Dagger runs the
ping
container with theredis-srv
alias magically added to/etc/hosts
.
Dagger cancels each service run after a 10 second grace period to avoid frequent restarts, unless the explicit Start
and Stop
APIs are used.
Services are based on containers, but they run a little differently. Whereas regular containers in Dagger are de-duplicated across the entire Dagger Engine, service containers are only de-duplicated within a Dagger client session. This means that if you run separate Dagger sessions that use the exact same services, they will each get their own "instance" of the service. This process is carefully tuned to preserve caching at each client call-site, while prohibiting "cross-talk" from one Dagger session's client to another Dagger session's service.
Content-addressed services are very convenient. You don't have to come up with names and maintain instances of services; just use them by value. You also don't have to manage the state of the service; you can just trust that it will be running when needed and stopped when not.
If you need multiple instances of a service, just attach something unique to each one, such as an instance ID.
Here's a more detailed client-server example of running commands against a Redis service:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// creates Redis service and client
func (m *MyModule) RedisService(ctx context.Context) (string, error) {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
// create Redis client container
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv)
args := []string{"redis-cli", "-h", "redis-srv"}
// set value
setter, err := redisCLI.
WithExec(append(args, "set", "foo", "abc")).
Stdout(ctx)
if err != nil {
return "", err
}
// get value
getter, err := redisCLI.
WithExec(append(args, "get", "foo")).
Stdout(ctx)
if err != nil {
return "", err
}
return setter + getter, nil
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def redis_service(self) -> str:
"""Creates Redis service and client."""
redis_srv = (
dag.container()
.from_("redis")
.with_exposed_port(6379)
.as_service(use_entrypoint=True)
)
# create Redis client container
redis_cli = (
dag.container().from_("redis").with_service_binding("redis-srv", redis_srv)
)
args = ["redis-cli", "-h", "redis-srv"]
# set value
setter = await redis_cli.with_exec([*args, "set", "foo", "abc"]).stdout()
# get value
getter = await redis_cli.with_exec([*args, "get", "foo"]).stdout()
return setter + getter
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Creates Redis service and client
*/
@func()
async redisService(): Promise<string> {
const redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.asService({ useEntrypoint: true })
// create Redis client container
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
.withEntrypoint()
const args = ["redis-cli", "-h", "redis-srv"]
// set value
const setter = await redisCLI
.withExec([...args, "set", "foo", "abc"])
.stdout()
// get value
const getter = await redisCLI.withExec([...args, "get", "foo"]).stdout()
return setter + getter
}
}
This example relies on the 10-second grace period, which you should try to avoid. Depending on the 10-second grace period is risky because there are many factors which could cause a 10-second delay between calls to Dagger, such as excessive CPU load, high network latency between the client and Dagger, or Dagger operations that require a variable amount of time to process.
It would be better to chain both commands together, which ensures that the service stays running for both, as in the revision below:
- Go
- Python
- TypeScript
package main
import (
"context"
"dagger/my-module/internal/dagger"
)
type MyModule struct{}
// creates Redis service and client
func (m *MyModule) RedisService(ctx context.Context) (string, error) {
redisSrv := dag.Container().
From("redis").
WithExposedPort(6379).
AsService(dagger.ContainerAsServiceOpts{UseEntrypoint: true})
// create Redis client container
redisCLI := dag.Container().
From("redis").
WithServiceBinding("redis-srv", redisSrv)
args := []string{"redis-cli", "-h", "redis-srv"}
// set and get value
return redisCLI.
WithExec(append(args, "set", "foo", "abc")).
WithExec(append(args, "get", "foo")).
Stdout(ctx)
}
from dagger import dag, function, object_type
@object_type
class MyModule:
@function
async def redis_service(self) -> str:
"""Creates Redis service and client."""
redis_srv = (
dag.container()
.from_("redis")
.with_exposed_port(6379)
.as_service(use_entrypoint=True)
)
# create Redis client container
redis_cli = (
dag.container().from_("redis").with_service_binding("redis-srv", redis_srv)
)
args = ["redis-cli", "-h", "redis-srv"]
# set and get value
return await (
redis_cli.with_exec([*args, "set", "foo", "abc"])
.with_exec([*args, "get", "foo"])
.stdout()
)
import { dag, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
/**
* Creates Redis service and client
*/
@func()
async redisService(): Promise<string> {
const redisSrv = dag
.container()
.from("redis")
.withExposedPort(6379)
.asService({ useEntrypoint: true })
// create Redis client container
const redisCLI = dag
.container()
.from("redis")
.withServiceBinding("redis-srv", redisSrv)
const args = ["redis-cli", "-h", "redis-srv"]
// set and get value
return await redisCLI
.withExec([...args, "set", "foo", "abc"])
.withExec([...args, "get", "foo"])
.stdout()
}
}