From c7e435ea7311929389a57cbbc17fe9129a27e901 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sun, 23 Aug 2026 11:51:16 -0700 Subject: [PATCH] Fix broken and outdated README examples Audited every example in the README against a live daemon. All ten Ruby blocks now parse under `ruby -c`; three did not before. Syntax errors: - `container.update("CpuShares" => 50000")` had an unbalanced quote. - `Docker::Event.stream({ read_timeout: 60 * 60 * 24) })` had a stray paren. - Pasted command output (a tar dump, event to_s lines, streaming log lines) sat inside ruby blocks as bare tokens. Commented out. Wrong documented return values, all checked against a running daemon: - `image.save('my_export.tar')` returns nil, not a Docker::Image. Image#save only returns a String when no filename is given. - `container.run('pwd', 10)` returns a Docker::Container, not a Docker::Image. It commits the container and runs the command in the resulting image, and Image#run returns a Container. - `image.remove(:force => true)` returns the raw response body, not true. Event read timeouts. The first argument to Event.stream and Event.since is the query parameter hash for /events, not Excon options, so `Event.stream({ read_timeout: 0 })` sent read_timeout=0 to Docker as a query parameter and left the timeout untouched. Documented setting it on the connection instead. Also noted that the global `Docker.options[:read_timeout] = ...` form only takes effect if set before the default connection is first used, since Docker.connection is memoized and Connection#initialize copies the options hash. `container.exec(['date'], stderr: false)` was documented as returning only stdout. It does not: Container#exec computes stderr as `options.delete(:stderr) || !detach`, so passing false still resolves to true and stderr is captured. Replaced with destructuring the documented [stdout, stderr, exit_code] return value. The underlying behaviour looks like a bug, but that is a code change rather than a docs one, so it is left alone here. Stale references: - The `base` image no longer exists on Docker Hub ("pull access denied for base, repository does not exist"). Four examples used it; switched to debian:stable, which is what the specs use. - `sudo docker -d` fails with "unknown shorthand flag: 'd'". Now dockerd. - The docker_remote_api_v1.14 doc links 404. - `docker images ` is not the equivalent of Image.get, which hits /images/{id}/json. That is docker inspect. - ubuntu:14.04 went EOL in 2019; use 24.04. - Dropped the "meant to interface with Docker version 1.4.*" claim. The gem sends no API version prefix, so requests use the daemon default. --- README.md | 107 +++++++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index dea7f7bb..0cf0a3ca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ docker-api ========== [![Gem Version](https://badge.fury.io/rb/docker-api.svg)](https://badge.fury.io/rb/docker-api) [![Code Climate](https://codeclimate.com/github/upserve/docker-api.svg)](https://codeclimate.com/github/upserve/docker-api) -This gem provides an object-oriented interface to the [Docker Engine API](https://docs.docker.com/develop/sdk/). Every method listed there is implemented. At the time of this writing, docker-api is meant to interface with Docker version 1.4.* +This gem provides an object-oriented interface to the [Docker Engine API](https://docs.docker.com/develop/sdk/). Every method listed there is implemented. The gem does not pin an API version, so requests use the daemon's default. It is tested against Docker 26 and 27. If you're interested in using Docker to package your apps, we recommend the [dockly](https://github.com/upserve/dockly) gem. Dockly provides a simple DSL for describing Docker containers that install as Debian packages and are controlled by upstart scripts. @@ -41,7 +41,7 @@ At this time, basic `podman` support has been added via the podman docker-compat Follow the [installation instructions](https://docs.docker.com/install/), and then run: ```shell -$ sudo docker -d +$ sudo dockerd ``` This will daemonize Docker so that it can be used for the remote API calls. @@ -138,15 +138,15 @@ Docker.authenticate!('username' => 'docker-fan-boi', 'password' => 'i<3docker', ## Images -Just about every method here has a one-to-one mapping with the [Images](https://docs.docker.com/engine/reference/api/docker_remote_api_v1.14/#2-2-images) section of the API. If an API call accepts query parameters, these can be passed as an Hash to it's corresponding method. Also, note that `Docker::Image.new` is a private method, so you must use `.create`, `.build`, `.build_from_dir`, `build_from_tar`, or `.import` to make an instance. +Just about every method here has a one-to-one mapping with the [Images](https://docs.docker.com/reference/api/engine/) section of the API. If an API call accepts query parameters, these can be passed as an Hash to it's corresponding method. Also, note that `Docker::Image.new` is a private method, so you must use `.create`, `.build`, `.build_from_dir`, `build_from_tar`, or `.import` to make an instance. ```ruby require 'docker' # => true # Pull an Image. -# docker command for reference: docker pull ubuntu:14.04 -image = Docker::Image.create('fromImage' => 'ubuntu:14.04') +# docker command for reference: docker pull ubuntu:24.04 +image = Docker::Image.create('fromImage' => 'ubuntu:24.04') # => Docker::Image { :id => ae7ffbcd1, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } # Insert a local file into an Image. @@ -194,12 +194,12 @@ image.run('ls -l') # Remove the Image from the server. # docker command for reference: docker rmi -f image.remove(:force => true) -# => true +# => "[{\"Untagged\":\"base2:latest\"}]\n" # Export a single Docker Image to a file # docker command for reference: docker save my_export.tar image.save('my_export.tar') -# => Docker::Image { :id => 66b712aef, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } +# => nil # Return the raw image binary data image.save @@ -225,7 +225,7 @@ File.open('my-export.tar') do |file| end # Create an Image from a Dockerfile as a String. -Docker::Image.build("from base\nrun touch /test") +Docker::Image.build("FROM debian:stable\nRUN touch /test") # => Docker::Image { :id => b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } # Create an Image from a Dockerfile. @@ -256,7 +256,7 @@ Docker::Image.all # => [Docker::Image { :id => b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } }, Docker::Image { :id => 8dbd9e392a964056420e5d58ca5cc376ef18e2de93b5cc90e868a1bbc8318c1c, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } }] # Get Image from the server, with id -# docker command for reference: docker images +# docker command for reference: docker inspect Docker::Image.get('df4f1bdecf40') # => Docker::Image { :id => eb693ec80, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } @@ -298,13 +298,13 @@ Docker::Image.search('term' => 'sshd') ## Containers -Much like the Images, this object also has a one-to-one mapping with the [Containers](https://docs.docker.com/engine/reference/api/docker_remote_api_v1.14/#2-1-containers) section of the API. Also like Images, `.new` is a private method, so you must use `.create` to make an instance. +Much like the Images, this object also has a one-to-one mapping with the [Containers](https://docs.docker.com/reference/api/engine/) section of the API. Also like Images, `.new` is a private method, so you must use `.create` to make an instance. ```ruby require 'docker' # Create a Container. -container = Docker::Container.create('Cmd' => ['ls'], 'Image' => 'base') +container = Docker::Container.create('Cmd' => ['ls'], 'Image' => 'debian:stable') # => Docker::Container { :id => 492510dd38e4, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } # Get more information about the Container. @@ -425,13 +425,13 @@ container.changes # Copy files/directories from the Container. Note that these are exported as tars. container.archive_out('/etc/hosts') { |chunk| puts chunk } -hosts0000644000000000000000000000023412100405636007023 0ustar -127.0.0.1 localhost -::1 localhost ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters +# hosts0000644000000000000000000000023412100405636007023 0ustar +# 127.0.0.1 localhost +# ::1 localhost ip6-localhost ip6-loopback +# fe00::0 ip6-localnet +# ff00::0 ip6-mcastprefix +# ff02::1 ip6-allnodes +# ff02::2 ip6-allrouters # => Docker::Container { :id => a1759f3e2873, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } # Wait for the current command to finish executing. If an argument is given, @@ -445,13 +445,13 @@ container.attach(:stream => true, :stdin => nil, :stdout => true, :stderr => tru # => [["bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nselinux\nsrv\nsys\ntmp\nusr\nvar", []] # If you wish to stream the attach method, a block may be supplied. -container = Docker::Container.create('Image' => 'base', 'Cmd' => ['find / -name *']) +container = Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ['find / -name *']) container.tap(&:start).attach { |stream, chunk| puts "#{stream}: #{chunk}" } -stderr: 2013/10/30 17:16:24 Unable to locate find / -name * +# stderr: 2013/10/30 17:16:24 Unable to locate find / -name * # => [[], ["2013/10/30 17:16:24 Unable to locate find / -name *\n"]] # If you want to attach to stdin of the container, supply an IO-like object: -container = Docker::Container.create('Image' => 'base', 'Cmd' => ['cat'], 'OpenStdin' => true, 'StdinOnce' => true) +container = Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ['cat'], 'OpenStdin' => true, 'StdinOnce' => true) container.tap(&:start).attach(stdin: StringIO.new("foo\nbar\n")) # => [["foo\nbar\n"], []] @@ -470,16 +470,16 @@ container.logs(stdout: true) # Streaming logs from non-TTY container removing multiplex prefix with a block printing out each line (block not possible with Container#logs) container.streaming_logs(stdout: true) { |stream, chunk| puts "#{stream}: #{chunk}" } -stdout: 1 -stdout: 2 -stdout: 3 -stdout: 4 -stdout: 5 -stdout: 6 -stdout: 7 -stdout: 8 -stdout: 9 -stdout: 10 +# stdout: 1 +# stdout: 2 +# stdout: 3 +# stdout: 4 +# stdout: 5 +# stdout: 6 +# stdout: 7 +# stdout: 8 +# stdout: 9 +# stdout: 10 # => "1\n\n2\n\n3\n\n4\n\n5\n\n6\n\n7\n\n8\n\n9\n\n10\n" # If the container has TTY enabled, set `tty => true` to get the raw stream: @@ -499,7 +499,7 @@ container.commit # Commit the Container and run a new command. The second argument is the number # of seconds the Container should wait before stopping its current command. container.run('pwd', 10) -# => Docker::Image { :id => 4427be4199ac, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } +# => Docker::Container { :id => 4427be4199ac, :connection => Docker::Connection { :url => tcp://localhost, :options => {:port=>2375} } } # Run an Exec instance inside the container and capture its output and exit status container.exec(['date']) @@ -511,12 +511,13 @@ container.exec(['./my_service'], detach: true) # Parse the output of an Exec instance container.exec(['find', '/', '-name *']) { |stream, chunk| puts "#{stream}: #{chunk}" } -stderr: 2013/10/30 17:16:24 Unable to locate find / -name * +# stderr: 2013/10/30 17:16:24 Unable to locate find / -name * # => [[], ["2013/10/30 17:16:24 Unable to locate find / -name *\n"], 1] -# Run an Exec instance by grab only the STDOUT output -container.exec(['date'], stderr: false) -# => [["Wed Nov 26 11:10:30 CST 2014\n"], [], 0] +# The return value is [stdout_lines, stderr_lines, exit_code], so destructure +# it if you only care about one stream. +stdout, _stderr, _exit_code = container.exec(['date']) +# stdout => ["Wed Nov 26 11:10:30 CST 2014\n"] # Pass input to an Exec instance command via Stdin container.exec(['cat'], stdin: StringIO.new("foo\nbar\n")) @@ -529,7 +530,7 @@ container.exec(command, tty: true) # Wait for the current command to finish executing. If an argument is given, # will timeout after that number of seconds. The default is one minute. -command = ["bash", "-c", "if [ -t 1 ]; then echo -n \"Set max seconds for exec!!\"; fi"] +command = ["bash", "-c", "if [ -t 1 ]; then echo -n \"Set max seconds for exec!\"; fi"] container.exec(command, wait: 120) # => [["Set max seconds for exec!"], [], 0] @@ -538,7 +539,7 @@ container.delete(:force => true) # => nil # Update the container. -container.update("CpuShares" => 50000") +container.update("CpuShares" => 50000) # Request a Container by ID or name. Docker::Container.get('500f53b25e6e') @@ -578,28 +579,36 @@ require 'docker' # Action on a stream of events as they come in Docker::Event.stream { |event| puts event; break } -Docker::Event { :status => create, :id => aeb8b55726df63bdd69d41e1b2650131d7ce32ca0d2fa5cbc75f24d0df34c7b0, :from => base:latest, :time => 1416958554 } +# Docker::Event { :status => create, :id => aeb8b55726df63bdd69d41e1b2650131d7ce32ca0d2fa5cbc75f24d0df34c7b0, :from => debian:stable, :time => 1416958554 } # => nil # Action on all events after a given time (will execute the block for all events up till the current time, and wait to execute on any new events after) Docker::Event.since(1416958763) { |event| puts event; puts Time.now.to_i; break } -Docker::Event { :status => die, :id => 663005cdeb56f50177c395a817dbc8bdcfbdfbdaef329043b409ecb97fb68d7e, :from => base:latest, :time => 1416958764 } -1416959041 +# Docker::Event { :status => die, :id => 663005cdeb56f50177c395a817dbc8bdcfbdfbdaef329043b409ecb97fb68d7e, :from => debian:stable, :time => 1416958764 } +# 1416959041 # => nil ``` -These methods are prone to read timeouts. The timeout can be disabled by setting it to zero, or simply made much higher: +These methods are prone to read timeouts. Note that the first argument to +`Docker::Event.stream` and `Docker::Event.since` is a hash of **query parameters** +for the `/events` endpoint, not Excon options. Passing `read_timeout` there sends +it to Docker as a query parameter and has no effect on the timeout. Set it on the +connection instead: ```ruby -# Disable timeouts completely -Docker::Event.stream({ read_timeout: 0 }) { |event| … } +# Disable timeouts completely, for this call only +conn = Docker::Connection.new(Docker.url, Docker.options.merge(read_timeout: 0)) +Docker::Event.stream({}, conn) { |event| puts event } # Timeout if no events are received in 24h -Docker::Event.stream({ read_timeout: 60 * 60 * 24) }) { |event| … } +conn = Docker::Connection.new(Docker.url, Docker.options.merge(read_timeout: 60 * 60 * 24)) +Docker::Event.stream({}, conn) { |event| puts event } -# Set a high timeout globally. Be warned that you probably don't want this for other methods. +# Or set a high timeout globally. Be warned that you probably don't want this +# for other methods. This has to happen before the default connection is first +# used, since `Docker.connection` is memoized. Docker.options[:read_timeout] = 60 * 60 * 24 -Docker::Event.stream { |event| … } +Docker::Event.stream { |event| puts event } ``` ## Connecting to Multiple Servers @@ -636,8 +645,8 @@ end ## Not supported (yet) -* Generating a tarball of images and metadata for a repository specified by a name: https://docs.docker.com/engine/reference/api/docker_remote_api_v1.14/#get-a-tarball-containing-all-images-and-tags-in-a-repository -* Load a tarball generated from docker that contains all the images and metadata of a repository: https://docs.docker.com/engine/reference/api/docker_remote_api_v1.14/#load-a-tarball-with-a-set-of-images-and-tags-into-docker +* Generating a tarball of images and metadata for a repository specified by a name +* Load a tarball generated from docker that contains all the images and metadata of a repository License -----