Docker Client
[]()
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
).
Docker API version support
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.
Current implementation status
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 (https://github.com/swift-server/async-http-client/issues/353).
Installation
Package.swift
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")
]),
...
]
)
Xcode Project
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.
Usage Examples
Connect to a Docker daemon
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: URL(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()}
Docker system info
Get detailed information about the Docker daemon
```swift
let info = try await docker.info()
print("• Docker daemon info: \(info)")
```
Get versions information about the Docker daemon
```swift
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:
```swift
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".
```swift
for try await event in try await events {
print("\n••• event: \(event)")
}
```
Containers
List containers
Add `all: true` to also return stopped containers.
```swift
let containers = try await docker.containers.list()
```
Get a container details
```swift
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:
```swift
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:
```swift
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:
```swift
let newConfig = ContainerUpdate(memoryLimit: .mb(64), memorySwap: .mb(64))
try await docker.containers.update("nameOrId", spec: newConfig)
```
Start a container
```swift
try await docker.containers.start("nameOrId")
```
Stop a container
```swift
try await docker.containers.stop("nameOrId")
```
Rename a container
```swift
try await docker.containers.rename("nameOrId", to: "hahi")
```
Delete a container
If the container is running, deletion can be forced by passing `force: true`
```swift
try await docker.containers.remove("nameOrId")
```
Get container logs
> Logs are streamed progressively in an asynchronous way.
Get all logs:
```swift
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:
```swift
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:
```swift
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:
```swift
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(for: .seconds(1))
// Now let's send the command; the response will be printed to the screen.
try await attach.send("uname")
```
Images
List the Docker images
```swift
let images = try await docker.images.list()
```
Get an image details
```swift
let image = try await docker.images.get("nameOrId")
```
Pull an image
Pull an image from a public repository:
```swift
let image = try await docker.images.pull(byIdentifier: "hello-world:latest")
```
Pull an image from a registry that requires authentication:
```swift
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 a `serverAddress` 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":
```swift
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 a `serverAddress` 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:
```swift
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](https://github.com/kayembi/Tarscape) (only available on macOS):
```swift
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")!
)
```
Networks
List networks
```swift
let networks = try await docker.networks.list()
```
Get a network details
```swift
let network = try await docker.networks.get("nameOrId")
```
Create a network
Create a new network without any custom options:
```swift
let network = try await docker.networks.create(
spec: .init(name: "my-network")
)
```
Create a new network with custom IPs range:
```swift
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
```swift
try await docker.networks.remove("nameOrId")
```
Connect an existing Container to a Network
```swift
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)
```
Volumes
List volumes
```swift
let volumes = try await docker.volumes.list()
```
Get a volume details
```swift
let volume = try await docker.volumes.get("nameOrId")
```
Create a volume
```swift
let volume = try await docker.volumes.create(
spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
```
Delete a volume
```swift
try await docker.volumes.remove("nameOrId")
```
Swarm
Initialize Swarm mode
```swift
let swarmId = try await docker.swarm.initSwarm()
```
Get Swarm cluster details (inspect)
> The client must be connected to a Swarm manager node.
```swift
let swarm = try await docker.swarm.get()
```
Make the Docker daemon to join an existing Swarm cluster
```swift
// 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
```swift
try await docker.swarm.leave(force: true)
```
Nodes
This requires a Docker daemon with Swarm mode enabled.
Additionally, the client must be connected to a manager node.
List the Swarm nodes
```swift
let nodes = try await docker.nodes.list()
```
Remove a Node from a Swarm
> Note: `force` is needed if the node is a manager
```swift
try await docker.nodes.delete(id: "xxxxxx", force: true)
```
Services
This requires a Docker daemon with Swarm mode enabled.
Additionally, the client must be connected to a manager node.
List services
```swift
let services = try await docker.services.list()
```
Get a service details
```swift
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:
```swift
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:
```swift
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?
```swift
var index = 0 // Keep track of how long we've been waiting
repeat {
try await Task.sleep(for: .seconds(1))
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?
```swift
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
```swift
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:
```swift
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:
```swift
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:
```swift
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:
```swift
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.
```swift
try await docker.services.rollback("nameOrId")
```
Delete a service
```swift
try await docker.services.remove("nameOrId")
```
Secrets
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
```swift
let secrets = try await docker.secrets.list()
```
Get a secret details
> Note: The Docker API doesn't return secret data/values.
```swift
let secret = try await docker.secrets.get("nameOrId")
```
Create a secret
Create a Secret containing a `String` value:
```swift
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:
```swift
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).
```swift
try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
```
Delete a secret
```swift
try await docker.secrets.remove("nameOrId")
```
Plugins
List installed plugins
```swift
let plugins = try await docker.plugins.list()
```
Install a plugin
> Note: the `install()` method can be passed a `credentials` parameter containing credentials for a private registry.
> See "Pull an image" for more information.
```swift
// 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")
```
Credits
This is a fork of the great work at https://github.com/alexsteinerde/docker-client-swift
License
This project is released under the MIT license. See LICENSE for details.
Contribute
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 :)