Module: Kernel

Defined in:
lib/donce.rb

Overview

Execute Docker container and clean up afterwards.

This function helps building temporary Docker images, run Docker containers, and clean up afterwards — may be convenient for automated tests (for example, with Minitest):

class MyTest < Minitest::Test
  def test_prints_hello_world
    stdout = donce(
      dockerfile: '
        FROM ubuntu
        CMD echo "Hello, world!"
      '
    )
    assert_equal("Hello, world!\n", stdout)
  end
end

It’s possible to pass a block to it too, which will lead to background execution of the container (in daemon mode):

def test_runs_daemon
  donce(dockerfile: "FROM ubuntu\nCMD sleep 9999") do |id|
    refute_empty(id)  # the ID of the container
  end
end

If you need to run docker via sudo, simply set DONCE_SUDO environment variable to any value.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2025 Yegor Bugayenko

License

MIT

Instance Method Summary collapse

Instance Method Details

#donce(dockerfile: nil, image: nil, home: nil, log: $stdout, args: '', env: {}, root: false, command: '', timeout: 10, volumes: {}, ports: {}) ⇒ String

Build Docker image (or use existing one), run Docker container, and then clean up.

Parameters:

  • dockerfile (String) (defaults to: nil)

    The content of the Dockerfile

  • home (String) (defaults to: nil)

    The directory with Dockerfile and all other necessary files

  • image (String) (defaults to: nil)

    The name of Docker image, e.g. “ubuntu:24.04”

  • log (Logger) (defaults to: $stdout)

    The logging destination, can be $stdout

  • args (String|Array<String>) (defaults to: '')

    List of extra arguments for the docker command

  • env (Hash<String,String>) (defaults to: {})

    Environment variables going into the container

  • volumes (Hash<String,String>) (defaults to: {})

    Local to container volumes mapping

  • ports (Hash<String,String>) (defaults to: {})

    Local to container port mapping

  • root (Boolean) (defaults to: false)

    Let user inside the container be “root”?

  • command (String|Array<String>) (defaults to: '')

    The command for the script inside the container

  • timeout (Integer) (defaults to: 10)

    Maximum seconds to spend on each docker call

Returns:

  • (String)

    The stdout of the container



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/donce.rb', line 83

def donce(dockerfile: nil, image: nil, home: nil, log: $stdout, args: '', env: {}, root: false, command: '',
          timeout: 10, volumes: {}, ports: {})
  raise 'Either use "dockerfile" or "home"' if dockerfile && home
  raise 'Either use "dockerfile" or "image"' if dockerfile && image
  raise 'Either use "image" or "home"' if home && image
  raise 'Either "dockerfile", or "home", or "image" must be provided' if !dockerfile && !home && !image
  docker = ENV['DONCE_SUDO'] ? 'sudo docker' : 'docker'
  img =
    if image
      image
    else
      i = "donce-#{SecureRandom.hex(6)}"
      if dockerfile
        Dir.mktmpdir do |tmp|
          File.write(File.join(tmp, 'Dockerfile'), dockerfile)
          qbash("#{docker} build #{Shellwords.escape(tmp)} -t #{i}", log:)
        end
      else
        qbash("#{docker} build #{Shellwords.escape(home)} -t #{i}", log:)
      end
      i
    end
  container = "donce-#{SecureRandom.hex(6)}"
  begin
    stdout = nil
    code = 0
    begin
      cmd = [
        docker, 'run',
        block_given? ? '-d' : nil,
        '--name', Shellwords.escape(container),
        OS.linux? ? nil : "--add-host #{donce_host}:host-gateway",
        args,
        env.map { |k, v| "-e #{Shellwords.escape("#{k}=#{v}")}" }.join(' '),
        ports.map { |k, v| "-p #{Shellwords.escape("#{k}:#{v}")}" }.join(' '),
        volumes.map { |k, v| "-v #{Shellwords.escape("#{k}:#{v}")}" }.join(' '),
        root ? nil : "--user=#{Shellwords.escape("#{Process.uid}:#{Process.gid}")}",
        Shellwords.escape(img),
        command
      ].compact.join(' ')
      stdout, code =
        Timeout.timeout(timeout) do
          qbash(
            cmd,
            log:,
            accept: nil,
            both: true,
            env:
          )
        end
      unless code.zero?
        log.error(stdout)
        raise \
          "Failed to run #{cmd} " \
          "(exit code is ##{code}, stdout has #{stdout.split("\n").count} lines)"
      end
      yield container if block_given?
    ensure
      logs = qbash(
        "#{docker} logs #{Shellwords.escape(container)}",
        level: code.zero? ? Logger::DEBUG : Logger::ERROR,
        log:
      )
      stdout = logs if block_given?
      qbash("#{docker} rm -f #{Shellwords.escape(container)}", log:)
    end
    stdout
  ensure
    Timeout.timeout(10) do
      qbash("#{docker} rmi #{img}", log:) unless image
    end
  end
end

#donce_hostString

The name of the localhost inside Docker container.

Returns:

  • (String)

    The hostname



65
66
67
# File 'lib/donce.rb', line 65

def donce_host
  OS.linux? ? '172.17.0.1' : 'host.docker.internal'
end