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.
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()}
let info = try await docker.info()
print("⢠Docker daemon info: \(info)")
let version = try await docker.version()
print("⢠Docker API version: \(version.apiVersion)")
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)")
}
Add all: true
to also return stopped containers.
let containers = try await docker.containers.list()
let container = try await docker.containers.get("nameOrId")
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)
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)
try await docker.containers.start("nameOrId")
try await docker.containers.stop("nameOrId")
try await docker.containers.rename("nameOrId", to: "hahi")
If the container is running, deletion can be forced by passing force: true
try await docker.containers.remove("nameOrId")
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")
}
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")
let images = try await docker.images.list()
let image = try await docker.images.get("nameOrId")
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.
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.
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")!
)
let networks = try await docker.networks.list()
let network = try await docker.networks.get("nameOrId")
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")]
)
)
)
try await docker.networks.remove("nameOrId")
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)
let volumes = try await docker.volumes.list()
let volume = try await docker.volumes.get("nameOrId")
let volume = try await docker.volumes.create(
spec: .init(name: "myVolume", labels: ["myLabel": "value"])
)
try await docker.volumes.remove("nameOrId")
let swarmId = try await docker.swarm.initSwarm()
The client must be connected to a Swarm manager node.
let swarm = try await docker.swarm.get()
// 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"]
)
)
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.
let nodes = try await docker.nodes.list()
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.
let services = try await docker.services.list()
let service = try await docker.services.get("nameOrId")
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:
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)
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)
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")
}
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")
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.
let secrets = try await docker.secrets.list()
Note: The Docker API doesn't return secret data/values.
let secret = try await docker.secrets.get("nameOrId")
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)
)
Currently, only the
labels
field can be updated (Docker limitation).
try await docker.secrets.update("nameOrId", labels: ["myKey": "myValue"])
try await docker.secrets.remove("nameOrId")
let plugins = try await docker.plugins.list()
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 :)
link |
Stars: 10 |
Last commit: 3 weeks ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics