This is a low-level Docker Client written in Swift. It very closely follows the Docker API.
It fully uses the Swift concurrency features introduced with Swift 5.5 (async
/await
).
This client library aims at implementing the Docker API version 1.41 (https://docs.docker.com/engine/api/v1.41). This means that it will work with Docker >= 20.10.
Section | Operation | Support | Notes |
---|---|---|---|
Client connection | Local Unix socket | ✅ | |
HTTP | ✅ | ||
HTTPS | ✅ | ||
Docker daemon & System info | Ping | ✅ | |
Info | ✅ | ||
Version | ✅ | ||
Events | ✅ | ||
Get data usage info | ✅ | ||
Containers | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Update | ✅ | ||
Rename | ✅ | ||
Start/Stop/Kill | ✅ | ||
Pause/Unpause | ✅ | ||
Get logs | ✅ | ||
Get stats | ✅ | ||
Get processes (top) | ✅ | ||
Delete | ✅ | ||
Prune | ✅ | ||
Wait | ✅ | ||
Filesystem changes | ✅ | untested | |
Attach | ✅ | basic support 1 | |
Exec | ❌ | unlikely 2 | |
Resize TTY | ❌ | ||
Images | List | ✅ | |
Inspect | ✅ | ||
History | ✅ | ||
Pull | ✅ | basic support | |
Build | ✅ | basic support | |
Tag | ✅ | ||
Push | ✅ | ||
Create (container commit) | ✅ | ||
Delete | ✅ | ||
Prune | ✅ | ||
Swarm | Init | ✅ | |
Join | ✅ | ||
Inspect | ✅ | ||
Leave | ✅ | ||
Update | ✅ | ||
Nodes | List | ✅ | |
Inspect | ✅ | ||
Update | ✅ | ||
Delete | ✅ | ||
Services | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Get logs | ✅ | ||
Update | ✅ | ||
Rollback | ✅ | ||
Delete | ✅ | ||
Networks | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Delete | ✅ | ||
Prune | ✅ | ||
(Dis)connect container | ✅ | ||
Volumes | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Delete | ✅ | ||
Prune | ✅ | ||
Secrets | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Update | ✅ | ||
Delete | ✅ | ||
Configs | List | ✅ | |
Inspect | ✅ | ||
Create | ✅ | ||
Update | ✅ | ||
Delete | ✅ | ||
Tasks | List | ✅ | |
Inspect | ✅ | ||
Get logs | ✅ | ||
Plugins | List | ✅ | |
Inspect | ✅ | ||
Get Privileges | ✅ | ||
Install | ✅ | ||
Remove | ✅ | ||
Enable/disable | ✅ | ||
Upgrade | ✅ | untested | |
Configure | ✅ | untested | |
Create | ❌ | TBD | |
Push | ❌ | TBD | |
Registries | Login | ✅ | basic support |
Docker error responses mgmt | 🚧 |
✅ : done or mostly done
🚧 : work in progress, partially implemented, might not work
❌ : not implemented/supported at the moment.
Note: various Docker endpoints such as list or prune support filters. These are currently not implemented.
1 Attach is currently not supported when connecting to Docker via local Unix socket, or when using a proxy. It uses the Websocket protocol.
2 Docker exec is using an unconventional protocol that requires raw access to the TCP socket. Significant work needed in order to support it (swift-server/async-http-client#353).
import PackageDescription
let package = Package(
dependencies: [
.package(url: "https://github.com/m-barthelemy/DockerSwift.git", .branch("main")),
],
targets: [
.target(name: "App", dependencies: [
...
.product(name: "DockerSwift", package: "DockerSwift")
]),
...
]
)
To add DockerClientSwift to your existing Xcode project, select File -> Swift Packages -> Add Package Dependancy.
Enter https://github.com/m-barthelemy/DockerSwift.git
for the URL.
Local socket (defaults to /var/run/docker.sock
):
import DockerSwift
let docker = DockerClient()
defer {try! docker.syncShutdown()}
Remote daemon over HTTP:
import DockerSwift
let docker = DockerClient(daemonURL: .init(string: "http://127.0.0.1:2375")!)
defer {try! docker.syncShutdown()}
Remote daemon over HTTPS, using a client certificate for authentication:
import DockerSwift
var tlsConfig = TLSConfiguration.makeClientConfiguration()
tlsConfig.privateKey = NIOSSLPrivateKeySource.file("client-key.pem")
tlsConfig.certificateChain.append(NIOSSLCertificateSource.file("client-certificate.pem"))
tlsConfig.additionalTrustRoots.append(.file("docker-daemon-ca.pem"))
tlsConfig.certificateVerification = .noHostnameVerification
let docker = DockerClient(
daemonURL: .init(string: "https://your.docker.daemon:2376")!,
tlsConfig: tlsConfig
)
defer {try! docker.syncShutdown()}
Get detailed information about the Docker daemon
let info = try await docker.info()
print("• Docker daemon info: \(info)")
Get versions information about the Docker daemon
let version = try await docker.version()
print("• Docker API version: \(version.apiVersion)")
Listen for Docker daemon events
We start by listening for docker events, then we create a container:
async let events = try await docker.events()
let container = try await docker.containers.create(
name: "hello",
spec: .init(
config: .init(image: "hello-world:latest"),
hostConfig: .init()
)
)
Now, we should get an event whose action
is "create" and whose type
is "container".
for try await event in try await events {
print("\n••• event: \(event)")
}
List containers
Add all: true
to also return stopped containers.
let containers = try await docker.containers.list()
Get a container details
let container = try await docker.containers.get("nameOrId")
Create a container
Note: you will also need to start it for the container to actually run.
The simplest way of creating a new container is to only specify the image to run:
let spec = ContainerSpec(
config: .init(image: "hello-world:latest")
)
let container = try await docker.containers.create(name: "test", spec: spec)
Docker allows customizing many parameters:
let spec = ContainerSpec(
config: .init(
// Override the default command of the Image
command: ["/custom/command", "--option"],
// Add new environment variables
environmentVars: ["HELLO=hi"],
// Expose port 80
exposedPorts: [.tcp(80)],
image: "nginx:latest",
// Set custom container labels
labels: ["label1": "value1", "label2": "value2"]
),
hostConfig: .init(
// Memory the container is allocated when starting
memoryReservation: .mb(64),
// Maximum memory the container can use
memoryLimit: .mb(128),
// Needs to be either disabled (-1) or be equal to, or greater than, `memoryLimit`
memorySwap: .mb(128),
// Let's publish the port we exposed in `config`
portBindings: [.tcp(80): [.publishTo(hostIp: "0.0.0.0", hostPort: 8000)]]
)
)
let container = try await docker.containers.create(name: "nginx-test", spec: spec)
Update a container
Let's update the memory limits for an existing container:
let newConfig = ContainerUpdate(memoryLimit: .mb(64), memorySwap: .mb(64))
try await docker.containers.update("nameOrId", spec: newConfig)
Start a container
try await docker.containers.start("nameOrId")
Stop a container
try await docker.containers.stop("nameOrId")
Rename a container
try await docker.containers.rename("nameOrId", to: "hahi")
Delete a container
If the container is running, deletion can be forced by passing force: true
try await docker.containers.remove("nameOrId")
Get container logs
Logs are streamed progressively in an asynchronous way.
Get all logs:
let container = try await docker.containers.get("nameOrId")
for try await line in try await docker.containers.logs(container: container, timestamps: true) {
print(line.message + "\n")
}
Wait for future log messages:
let container = try await docker.containers.get("nameOrId")
for try await line in try await docker.containers.logs(container: container, follow: true) {
print(line.message + "\n")
}
Only the last 100 messages:
let container = try await docker.containers.get("nameOrId")
for try await line in try await docker.containers.logs(container: container, tail: 100) {
print(line.message + "\n")
}
Attach to a container
Let's create a container that defaults to running a shell, and attach to it:
let _ = try await docker.images.pull(byIdentifier: "alpine:latest")
let spec = ContainerSpec(
config: .init(
attachStdin: true,
attachStdout: true,
attachStderr: true,
image: "alpine:latest",
openStdin: true
)
)
let container = try await docker.containers.create(spec: spec)
let attach = try await docker.containers.attach(container: container, stream: true, logs: true)
// Let's display any output from the container
Task {
for try await output in attach.output {
print("• \(output)")
}
}
// We need to be sure that the container is really running before being able to send commands to it.
try await docker.containers.start(container.id)
try await Task.sleep(nanoseconds: 1_000_000_000)
// Now let's send the command; the response will be printed to the screen.
try await attach.send("uname")
List the Docker images
let images = try await docker.images.list()
Get an image details
let image = try await docker.images.get("nameOrId")
Pull an image
Pull an image from a public repository:
let image = try await docker.images.pull(byIdentifier: "hello-world:latest")
Pull an image from a registry that requires authentication:
var credentials = RegistryAuth(username: "myUsername", password: "....")
try await docker.registries.login(credentials: &credentials)
let image = try await docker.images.pull(byIdentifier: "my-private-image:latest", credentials: credentials)
NOTE:
RegistryAuth
also accepts aserverAddress
parameter in order to use a custom registry.
Creating images from a remote URL or from the standard input is currently not supported.
Push an image
Supposing that the Docker daemon has an image named "my-private-image:latest":
var credentials = RegistryAuth(username: "myUsername", password: "....")
try await docker.registries.login(credentials: &credentials)
try await docker.images.push("my-private-image:latest", credentials: credentials)
NOTE:
RegistryAuth
also accepts aserverAddress
parameter in order to use a custom registry.
Build an image
The current implementation of this library is very bare-bones. The Docker build context, containing the Dockerfile and any other resources required during the build, must be passed as a TAR archive.
Supposing we already have a TAR archive of the build context:
let tar = FileManager.default.contents(atPath: "/tmp/docker-build.tar")
let buffer = ByteBuffer.init(data: tar)
let buildOutput = try await docker.images.build(
config: .init(dockerfile: "./Dockerfile", repoTags: ["build:test"]),
context: buffer
)
// The built Image ID is returned towards the end of the build output
var imageId: String!
for try await item in buildOutput {
if item.aux != nil {
imageId = item.aux!.id
}
else {
print("\n• Build output: \(item.stream)")
}
}
print("\n• Image ID: \(imageId)")
You can use external libraries to create TAR archives of your build context. Example with Tarscape (only available on macOS):
import Tarscape
let tarContextPath = "/tmp/docker-build.tar"
try FileManager.default.createTar(
at: URL(fileURLWithPath: tarContextPath),
from: URL(string: "file:///path/to/your/context/folder")!
)
List networks
let networks = try await docker.networks.list()
Get a network details
let network = try await docker.networks.get("nameOrId")
Create a network
Create a new network without any custom options:
let network = try await docker.networks.create(
spec: .init(name: "my-network")
)
Create a new network with custom IPs range:
let network = try await docker.networks.create(
spec: .init(
name: "my-network",
ipam: .init(
config: [.init(subnet: "192.168.2.0/24", gateway: "192.168.2.1")]
)
)
)
Delete a network
try await docker.networks.remove("nameOrId")
Connect an existing Container to a Network
let network = try await docker.networks.create(spec: .init(name: "myNetwork"))
var container = try await docker.containers.create(
name: "myContainer",
spec: .init(config: .init(image: image.id))
)
try await docker.networks.connect(container: container.id, to: network.id)
List volumes
let volumes = try await docker.volumes.list()
Get a volume details
let volume = try await docker.volumes.get("nameOrId")
Create a volume
let volume = try await docker.volumes.create(
spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
Delete a volume
try await docker.volumes.remove("nameOrId")
Initialize Swarm mode
let swarmId = try await docker.swarm.initSwarm()
Get Swarm cluster details (inspect)
The client must be connected to a Swarm manager node.
let swarm = try await docker.swarm.get()
Make the Docker daemon to join an existing Swarm cluster
// This first client points to an existing Swarm cluster manager
let swarmClient = Dockerclient(...)
let swarm = try await swarmClient.swarm.get()
// This client is the docker daemon we want to add to the Swarm cluster
let client = Dockerclient(...)
try await client.swarm.join(
config: .init(
// To join the Swarm cluster as a Manager node
joinToken: swarmClient.joinTokens.manager,
// IP/Host of the existing Swarm managers
remoteAddrs: ["10.0.0.1"]
)
)
Remove the current Node from the Swarm
Note:
force
is needed if the node is a manager
try await docker.swarm.leave(force: true)
This requires a Docker daemon with Swarm mode enabled. Additionally, the client must be connected to a manager node.
List the Swarm nodes
let nodes = try await docker.nodes.list()
Remove a Node from a Swarm
Note:
force
is needed if the node is a manager
try await docker.nodes.delete(id: "xxxxxx", force: true)
This requires a Docker daemon with Swarm mode enabled. Additionally, the client must be connected to a manager node.
List services
let services = try await docker.services.list()
Get a service details
let service = try await docker.services.get("nameOrId")
Create a service
Simplest possible example, we only specify the name of the service and the image to use:
let spec = ServiceSpec(
name: "my-nginx",
taskTemplate: .init(
containerSpec: .init(image: "nginx:latest")
)
)
let service = try await docker.services.create(spec: spec)
Let's specify a number of replicas, a published port and a memory limit of 64MB for our service:
let spec = ServiceSpec(
name: "my-nginx",
taskTemplate: .init(
containerSpec: .init(image: "nginx:latest"),
resources: .init(
limits: .init(memoryBytes: .mb(64))
),
// Uses default Docker routing mesh mode
endpointSpec: .init(ports: [.init(name: "HTTP", targetPort: 80, publishedPort: 8000)])
),
mode: .replicated(2)
)
let service = try await docker.services.create(spec: spec)
What if we then want to know when our service is fully running?
var index = 0 // Keep track of how long we've been waiting
repeat {
try await Task.sleep(nanoseconds: 1_000_000_000)
print("\n Service still not fully running!")
index += 1
} while try await docker.tasks.list()
.filter({$0.serviceId == service.id && $0.status.state == .running})
.count < 1 /* number of replicas */ && index < 15
print("\n Service is fully running!")
What if we want to create a one-off job instead of a service?
let spec = ServiceSpec(
name: "hello-world-job",
taskTemplate: .init(
containerSpec: .init(image: "hello-world:latest"),
...
),
mode: .job(1)
)
let job = try await docker.services.create(spec: spec)
Something more advanced? Let's create a Service:
- connected to a custom Network
- storing data into a custom Volume, for each container
- requiring a Secret
- publishing the port 80 of the containers to the port 8000 of each Docker Swarm node
- getting restarted automatically in case of failure
let network = try await docker.networks.create(spec: .init(name: "myNet", driver: "overlay"))
let secret = try await docker.secrets.create(spec: .init(name: "myPassword", value: "blublublu"))
let spec = ServiceSpec(
name: "my-nginx",
taskTemplate: .init(
containerSpec: .init(
image: "nginx:latest",
// Create and mount a dedicated Volume named "myStorage" on each running container.
mounts: [.volume(name: "myVolume", to: "/mnt")],
// Add our Secret. Will appear as `/run/secrets/myPassword` in the containers.
secrets: [.init(secret)]
),
resources: .init(
limits: .init(memoryBytes: .mb(64))
),
// If a container exits or crashes, replace it with a new one.
restartPolicy: .init(condition: .any, delay: .seconds(2), maxAttempts: 2)
),
mode: .replicated(1),
// Add our custom Network
networks: [.init(target: network.id)],
// Publish our Nginx image port 80 to 8000 on the Docker Swarm nodes
endpointSpec: .init(ports: [.init(name: "HTTP", targetPort: 80, publishedPort: 8000)])
)
let service = try await docker.services.create(spec: spec)
Update a service
Let's scale an existing service up to 3 replicas:
let service = try await docker.services.get("nameOrId")
var updatedSpec = service.spec
updatedSpec.mode = .replicated(3)
try await docker.services.update("nameOrId", spec: updatedSpec)
Get service logs
Logs are streamed progressively in an asynchronous way.
Get all logs:
let service = try await docker.services.get("nameOrId")
for try await line in try await docker.services.logs(service: service) {
print(line.message + "\n")
}
Wait for future log messages:
let service = try await docker.services.get("nameOrId")
for try await line in try await docker.services.logs(service: service, follow: true) {
print(line.message + "\n")
}
Only the last 100 messages:
let service = try await docker.services.get("nameOrId")
for try await line in try await docker.services.logs(service: service, tail: 100) {
print(line.message + "\n")
}
Rollback a service
Suppose that we updated our existing service configuration, and something is not working properly. We want to revert back to the previous, working version.
try await docker.services.rollback("nameOrId")
Delete a service
try await docker.services.remove("nameOrId")
This requires a Docker daemon with Swarm mode enabled.
Note: The API for managing Docker Configs is very similar to the Secrets API and the below examples also apply to them.
List secrets
let secrets = try await docker.secrets.list()
Get a secret details
Note: The Docker API doesn't return secret data/values.
let secret = try await docker.secrets.get("nameOrId")
Create a secret
Create a Secret containing a String
value:
let secret = try await docker.secrets.create(
spec: .init(name: "mySecret", value: "test secret value 💥")
)
You can also pass a Data
value to be stored as a Secret:
let data: Data = ...
let secret = try await docker.secrets.create(
spec: .init(name: "mySecret", data: data)
)
Update a secret
Currently, only the
labels
field can be updated (Docker limitation).
try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
Delete a secret
try await docker.secrets.remove("nameOrId")
List installed plugins
let plugins = try await docker.plugins.list()
Install a plugin
Note: the
install()
method can be passed acredentials
parameter containing credentials for a private registry. See "Pull an image" for more information.
// First, we fetch the privileges required by the plugin:
let privileges = try await docker.plugins.getPrivileges("vieux/sshfs:latest")
// Now, we can install it
try await docker.plugins.install(remote: "vieux/sshfs:latest", privileges: privileges)
// finally, we need to enable it before using it
try await docker.plugins.enable("vieux/sshfs:latest")
This is a fork of the great work at https://github.com/alexsteinerde/docker-client-swift
This project is released under the MIT license. See LICENSE for details.
You can contribute to this project by submitting a detailed issue or by forking this project and sending a pull request. Contributions of any kind are very welcome :)