Docker Client Package with Swift
In this article, I'm going to write about my DockerClientSwift
Package. But what's Docker? Probably you have already heard of Docker. Otherwise here is a short summary: Docker is a set of technologies to containerize software by utilizing virtualization. Nowadays a lot of software is deployed in containers to enable scalability. But Docker can also be very useful for development. Developers can boot up databases or other services in a container environment and test their software against that. As a developer, you can interact with a command-line interface with the Docker engine and execute commands or fetch information. If you haven't heard of Docker, especially as a backend developer, I recommend checking it out (Docker website).
This technologies, as they are focused on developers, provide APIs that are capable of controlling a Docker environment and can automate steps and processes. My Swift package is a wrapper around these APIs which are REST APIs that are called on a socket the Docker engine exposes. The execution of the requests is fully handled by NIO, the non-blocking IO framework provided by Apple. This allows the package to function in an asynchronous environment like on the server perfectly. That's also the main purpose of the Package: automating processes on the server. But as it's written in Swift it can also be used to write UI clients for the Desktop or even for mobile devices.
Usage with SwiftUI
One target platform for this package can be the Mac. In the following, I'll explain a bit about how it can be used with SwiftUI on the Mac.
I published a demo application, fully written in SwiftUI. But as a small disclaimer: This project is not a demonstration on how to use SwiftUI in the best way but instead on how it is possible to integrate the Docker client into a SwiftUI project.
The first thing to do is to create a new instance of the DockerClient
. When using the AppDelegate
lifecycle this is normally done in the applicationDidFinishLaunching(_: Notification)
method. Calling the default initializer will create a new HTTPClient
and a new Logger
the DockerClient
will use over the lifetime of the object. If we want to modify any of these parameters, we can pass customized objects into the .init()
function. We can also pass a different path to the Docker socket if we are using a non-default one.
import DockerClientSwift
// Store a reference to the instance in the `AppDelegate` object.
let dockerClient = DockerClient()
// Get log output of the client to debug potential bugs
LoggingSystem.bootstrap(StreamLogHandler.standardOutput(label:))
// When creating the `ContentView` inject the client as an environment object.
let contentView = ContentView()
.environmentObject(dockerClient)
The following example code shows how we can list all created, running, stopped and exited containers in the system. When the ContentView
appears we load all containers asynchronously. When the loading is completed, we store the result in a State
variable so our SwiftUI layout refreshes automatically.
import SwiftUI
import DockerClientSwift
import NIO // This import is needed as the `DockerClientSwift` exposes `EventLoopFuture` objects that are defined in the `NIO` package.
import Combine
struct ContentView: View {
@EnvironmentObject var dockerClient: DockerClient
@State private var containers: [Container]? = []
var body: some View {
Group {
if let containers = self.containers {
List(0..<containers.count, id: \.self) { index in
Text(containers[index].id.value)
}
} else {
Text("Loading")
}
}
.onAppear {
try! dockerClient.containers.list(all: true)
.whenComplete({ result in
DispatchQueue.main.async {
switch result {
case .failure:
self.containers = []
case .success(let containers):
self.containers = containers
}
}
})
}
}
}
This is a simple example of how to use the DockerClient
to load data from Docker and displaying it in SwiftUI. If you want to do more in SwiftUI, I recommend checking out the demo Mac app project. It contains AppKit-style table views where containers and images are displayed. And it contains code on how to use actions from within SwiftUI/AppKit to start, stop or delete containers or remove unused images.
I think a Mac app is the most often use-case for using this package with SwiftUI. But there is also a possible use case for a mobile application for iPhone or iPad. If you have Docker running on a server and you want to expose the socket to the public (which is not recommended due to security reasons) you could also connect to it from a mobile device and control it from there.
Usage with Vapor
Instead of using the package for looking into a Docker system that is running on our own development Mac, we can use it to run a server application on an external system that exposes some specific endpoints, for example to manage running services there. In the following, we will build a web server that can update the image of a Docker service. This can be useful if we want to update a specific service to a newer image when using continous delivery for example. We are going to use the Vapor packages to expose a REST API that can update services with other images.
Vapor is probably the most popular web framework for Swift but we could also use any other package. Vapor is built on top of NIO
which makes it even easier to use our DockerClient
because, as mentioned above, it is also built on top of that. So the interaction becomes more seamlessly than we have seen it in the SwiftUI example.
Again I provided a GitHub repo with the demo server application based on Vapor 4.
A basic POST
route that accepts a service and a new image name is seen below. If you have already developed something with Vapor this should look familiar. Otherwise I recommend checking out the Vapor documentation. The calls to the Docker client are the same as in SwiftUI. I created namespaces for each group of APIs. So we can see the images
and services
namespaces used in this example.
func routes(_ app: Application) throws {
app.post("deploy") { req throws -> EventLoopFuture<DeployResponse> in
let deploy = try req.content.decode(DeployRequest.self)
return try req.dockerClient.images.pullImage(byName: deploy.imageName, tag: deploy.imageTag ?? "latest")
.and(try req.dockerClient.services.get(serviceByNameOrId: deploy.serviceName))
.hop(to: req.eventLoop)
.flatMap({ image, service in
try req.dockerClient.services.update(service: service, newImage: image)
})
.map({ (service) in
DeployResponse(success: true, imageDigest: service.image.digest?.rawValue)
})
}
struct DeployRequest: Content {
let serviceName: String
let imageName: String
let imageTag: String?
}
struct DeployResponse: Content {
let success: Bool
let imageDigest: String?
}
}
The req.dockerClient
attribute comes from a custom extension which can be found in the demo project as well. It returns an instance of the client that is equipt with the HTTPClient
of the application and the logger instance of the request:
extension Request {
var dockerClient: DockerClient {
DockerClient(client: self.application.http.client.shared, logger: self.logger)
}
}
If we run the application we should have a running web server at http://localhost:8080
. Now we just need a Docker service. One way of creating a Docker service is to initialize a Docker Swarm and deploy a docker-compose.yml
file as a new stack. If you are not familiar with stacks you can read about it on the official Docker documentation.
The demo project comes with an example of such a docker-compose.yml
file. We open a new terminal, go into the demo-swarm
directory and execute the following two commands:
docker swarm init
docker stack deploy --compose-file docker-compose.yml demo
This creates a service called demo_nging
with an Nginx web server based on the nginx:latest
Docker image.
Now if we want to update the service to another image (for example nginx:alpine
) we can call our Vapor server to do that. The following curl command does this for us:
curl --request POST \
--url http://localhost:8080/deploy \
--header 'Content-Type: application/json' \
--data '{
"serviceName": "demo_nginx",
"imageName": "nginx",
"imageTag": "alpine"
}'
Of course we can automate much more with the Docker APIs. We can trigger recurrent actions by creating new containers every hour or store images as a backup on a remote server.
Future of the package
In my opinion, the package covers the basic APIs already. But as Docker is such a huge system with many many APIs, not all APIs are covered yet. The package is still in an alpha stage and is a small subset of all possibilities that could be done with the API. Everyone is invited to make changes and extend the package to their needs. Feel free to create a GitHub issue or a pull request if you have a specific use case.
Conclusion
Controlling and managing your Docker environment with Swift can enable a lot of automation. The DockerClientSwift
package should not be a tailored API for a specific use case but instead should provide a generic API into the Docker engine.
I think both demo applications provide a good look on how to use the DockerClientSwift
package. The API is far from complete. So if you have a use case that is not covered yet, feel free to create an issue or a pull request on GitHub.
What experiences do you have with Docker? Have you somehow build an API around your Docker environment to call into it and manage a server for example? Let me know your thoughts on Twitter or via email.