Class: Testcontainers::DockerContainer

Inherits:
Object
  • Object
show all
Defined in:
lib/testcontainers/docker_container.rb

Overview

The DockerContainer class is used to manage Docker containers. It provides an interface to create, start, stop, and manipulate containers using the Docker API.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: nil, image_create_options: {}, port_bindings: nil, volumes: nil, filesystem_binds: nil, env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, logger: Testcontainers.logger) ⇒ DockerContainer

Initializes a new DockerContainer instance.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/testcontainers/docker_container.rb', line 41

def initialize(image, name: nil, command: nil, entrypoint: nil, exposed_ports: nil, image_create_options: {}, port_bindings: nil, volumes: nil, filesystem_binds: nil,
  env: nil, labels: nil, working_dir: nil, healthcheck: nil, wait_for: nil, logger: Testcontainers.logger)

  @image = image
  @name = name
  @command = command
  @entrypoint = entrypoint
  @exposed_ports = add_exposed_ports(exposed_ports) if exposed_ports
  @image_create_options = image_create_options
  @port_bindings = add_fixed_exposed_ports(port_bindings) if port_bindings
  @volumes = add_volumes(volumes) if volumes
  @env = add_env(env) if env
  @filesystem_binds = add_filesystem_binds(filesystem_binds) if filesystem_binds
  @labels = add_labels(labels) if labels
  @working_dir = working_dir
  @healthcheck = add_healthcheck(healthcheck) if healthcheck
  @wait_for = add_wait_for(wait_for)
  @logger = logger
  @_container = nil
  @_id = nil
  @_created_at = nil
end

Instance Attribute Details

#_containerDocker::Container? (readonly)

the underlying Docker::Container object



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def _container
  @_container
end

#_idString? (readonly)

the container's ID



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def _id
  @_id
end

#commandArray<String>?

the command to run in the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def command
  @command
end

#entrypointArray<String>?

the entrypoint to run in the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def entrypoint
  @entrypoint
end

#envArray<String>?

an array of environment variables for the container in the format KEY=VALUE



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def env
  @env
end

#exposed_portsHash?

a hash mapping exposed container ports to an empty hash (used for Docker API compatibility)



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def exposed_ports
  @exposed_ports
end

#filesystem_bindsArray<String>?

an array of strings representing bind mounts from the host to the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def filesystem_binds
  @filesystem_binds
end

#healthcheckHash?

a hash of healthcheck options for the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def healthcheck
  @healthcheck
end

#imageString

the container's image name



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def image
  @image
end

#labelsHash?

a hash of labels to be applied to the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def labels
  @labels
end

#loggerLogger

a logger instance for the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def logger
  @logger
end

#nameString

the container's name



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def name
  @name
end

#port_bindingsHash?

a hash mapping container ports to host port bindings (used for Docker API compatibility)



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def port_bindings
  @port_bindings
end

#volumesHash?

a hash mapping volume paths in the container to an empty hash (used for Docker API compatibility)



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def volumes
  @volumes
end

#wait_forObject

Returns the value of attribute wait_for.



22
23
24
# File 'lib/testcontainers/docker_container.rb', line 22

def wait_for
  @wait_for
end

#working_dirString?

the working directory for the container



21
22
23
# File 'lib/testcontainers/docker_container.rb', line 21

def working_dir
  @working_dir
end

Instance Method Details

#add_env(env_or_key, value = nil) ⇒ Array<String>

Add environment variables to the container configuration.



72
73
74
75
76
77
# File 'lib/testcontainers/docker_container.rb', line 72

def add_env(env_or_key, value = nil)
  @env ||= []
  new_env = process_env_input(env_or_key, value)
  @env.concat(new_env)
  @env
end

#add_exposed_port(port) ⇒ Hash

Add an exposed port to the container configuration.



83
84
85
86
87
88
89
90
# File 'lib/testcontainers/docker_container.rb', line 83

def add_exposed_port(port)
  port = normalize_port(port)
  @exposed_ports ||= {}
  @port_bindings ||= {}
  @exposed_ports[port] ||= {}
  @port_bindings[port] ||= [{"HostPort" => ""}]
  @exposed_ports
end

#add_exposed_ports(*ports) ⇒ Hash

Add multiple exposed ports to the container configuration.



96
97
98
99
100
101
102
103
# File 'lib/testcontainers/docker_container.rb', line 96

def add_exposed_ports(*ports)
  ports = ports.first if ports.first.is_a?(Array)

  ports.each do |port|
    add_exposed_port(port)
  end
  @exposed_ports
end

#add_filesystem_bind(host_or_hash, container_path = nil, mode = "rw") ⇒ Array<String>

Add a filesystem bind to the container configuration.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/testcontainers/docker_container.rb', line 162

def add_filesystem_bind(host_or_hash, container_path = nil, mode = "rw")
  @filesystem_binds ||= []

  if host_or_hash.is_a?(Hash)
    host_path, container_path = host_or_hash.first
  elsif host_or_hash.is_a?(String)
    if container_path
      host_path = host_or_hash
    else
      host_path, container_path, mode = host_or_hash.split(":")
      mode ||= "rw"
    end
  else
    raise ArgumentError, "Invalid input format for add_filesystem_bind"
  end

  @filesystem_binds << "#{host_path}:#{container_path}:#{mode}"
  add_volume(container_path)
  @filesystem_binds
end

#add_filesystem_binds(filesystem_binds) ⇒ Array<String>

Add multiple filesystem binds to the container configuration.



187
188
189
190
191
192
193
194
# File 'lib/testcontainers/docker_container.rb', line 187

def add_filesystem_binds(filesystem_binds)
  @filesystem_binds ||= []
  binds = normalize_filesystem_binds(filesystem_binds)
  binds.each do |bind|
    add_filesystem_bind(*bind)
  end
  @filesystem_binds
end

#add_fixed_exposed_port(container_port, host_port = nil) ⇒ Hash

Add a fixed exposed port to the container configuration.



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/testcontainers/docker_container.rb', line 111

def add_fixed_exposed_port(container_port, host_port = nil)
  if container_port.is_a?(Hash)
    container_port, host_port = container_port.first
  end

  container_port = normalize_port(container_port)
  @exposed_ports ||= {}
  @port_bindings ||= {}
  @exposed_ports[container_port] = {}
  @port_bindings[container_port] = [{"HostPort" => host_port.to_s}]
  @port_bindings
end

#add_fixed_exposed_ports(port_mappings = {}) ⇒ Hash

Add multiple fixed exposed ports to the container configuration.



128
129
130
131
132
133
# File 'lib/testcontainers/docker_container.rb', line 128

def add_fixed_exposed_ports(port_mappings = {})
  port_mappings.each do |container_port, host_port|
    add_fixed_exposed_port(container_port, host_port)
  end
  @port_bindings
end

#add_healthcheck(options = {}) ⇒ Hash

Adds a healthcheck to the container.

Options Hash (options):

  • :test (Array|String)

    the command to run to check the health of the container.

  • :interval (Float)

    the time in seconds between health checks. (default: 30)

  • :timeout (Float)

    the time in seconds to wait for a health check to complete. (default: 30)

  • :retries (Integer)

    the number of times to retry a failed health check before giving up. (default: 3)

  • :shell (Boolean)

    whether or not to run the health check in a shell. (default: false)



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/testcontainers/docker_container.rb', line 226

def add_healthcheck(options = {})
  test = options[:test]

  if test.nil?
    @healthcheck = {"Test" => ["NONE"]}
    return @healthcheck
  end

  interval = options[:interval]&.to_f || 30.0
  timeout = options[:timeout]&.to_f || 30.0
  retries = options[:retries]&.to_i || 3
  shell = options[:shell] || false

  test = test.split(" ") if test.is_a?(String)
  test = shell ? test.unshift("CMD-SHELL") : test.unshift("CMD")

  @healthcheck = {
    "Test" => test,
    "Interval" => (interval * 1_000_000_000).to_i,
    "Timeout" => (timeout * 1_000_000_000).to_i,
    "Retries" => retries,
    "StartPeriod" => 0
  }
end

#add_label(label, value) ⇒ Hash

Add a label to the container configuration.



201
202
203
204
205
# File 'lib/testcontainers/docker_container.rb', line 201

def add_label(label, value)
  @labels ||= {}
  @labels[label] = value
  @labels
end

#add_labels(labels) ⇒ Hash

Add multiple labels to the container configuration.



211
212
213
214
215
# File 'lib/testcontainers/docker_container.rb', line 211

def add_labels(labels)
  @labels ||= {}
  @labels.merge!(labels)
  @labels
end

#add_volume(volume) ⇒ Hash

Add a volume to the container configuration.



139
140
141
142
143
# File 'lib/testcontainers/docker_container.rb', line 139

def add_volume(volume)
  @volumes ||= {}
  @volumes[volume] = {}
  @volumes
end

#add_volumes(volumes = []) ⇒ Hash

Add multiple volumes to the container configuration.



149
150
151
152
153
154
# File 'lib/testcontainers/docker_container.rb', line 149

def add_volumes(volumes = [])
  volumes = normalize_volumes(volumes)
  @volumes ||= {}
  @volumes.merge!(volumes)
  @volumes
end

#add_wait_for(method = nil, *args, **kwargs, &block) ⇒ Proc

Add a wait_for strategy to the container configuration.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/testcontainers/docker_container.rb', line 258

def add_wait_for(method = nil, *args, **kwargs, &block)
  if method.nil?
    if block
      if block.arity == 1
        @wait_for = block
      else
        raise ArgumentError, "Invalid wait_for block: #{block}"
      end
    elsif @exposed_ports && !@exposed_ports.empty?
      port = @exposed_ports.keys.first.split("/").first
      @wait_for = ->(container) { container.wait_for_tcp_port(port) }
    end
  elsif method.is_a?(Proc)
    if method.arity == 1
      @wait_for = method
    else
      raise ArgumentError, "Invalid wait_for method: #{method}"
    end
  elsif method.is_a?(Array)
    method_name = "wait_for_#{method[0]}".to_sym
    args = method[1] || []
    kwargs = method[2] || {}
    if respond_to?(method_name)
      @wait_for = ->(container) { container.send(method_name, *args, **kwargs) }
    else
      raise ArgumentError, "Invalid wait_for method: #{method_name}"
    end
  else
    method_name = "wait_for_#{method}".to_sym
    if respond_to?(method_name)
      @wait_for = ->(container) { container.send(method_name, *args, **kwargs) }
    else
      raise ArgumentError, "Invalid wait_for method: #{method_name}"
    end
  end
  @wait_for
end

#copy_file_from_container(container_path, host_path_or_io) ⇒ String

Copies a file from the container to the host.

Raises:



951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
# File 'lib/testcontainers/docker_container.rb', line 951

def copy_file_from_container(container_path, host_path_or_io)
  raise ContainerNotStartedError, "Container has not been started" unless running?
  raise ArgumentError, "Container path must be a non-empty string" if container_path.to_s.empty?

  begin
    io = host_path_or_io.is_a?(String) ? File.open(host_path_or_io, "w") : host_path_or_io
    io.rewind if io.pos != 0
    content = read_file(container_path)
    io.write(content)
    io.rewind
  rescue => e
    puts "Error while copying file from container: #{e.message}"
    raise e # Optionally re-raise the exception or handle it according to your needs
  ensure
    io.close if io.respond_to?(:close)
  end

  content
end

#copy_file_to_container(container_path, host_path_or_io) ⇒ self

Copies a IO object or a file from the host to the container.

Raises:



925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
# File 'lib/testcontainers/docker_container.rb', line 925

def copy_file_to_container(container_path, host_path_or_io)
  raise ContainerNotStartedError, "Container has not been started" unless running?
  raise ArgumentError, "Container path must be a non-empty string" if container_path.to_s.empty?

  begin
    io = host_path_or_io.is_a?(String) ? File.open(host_path_or_io) : host_path_or_io
    io.rewind if io.pos != 0
    store_file(container_path, io.read)
    io.rewind
  rescue => e
    puts "Error while copying file to container: #{e.message}"
    return false
  ensure
    io.close if io.respond_to?(:close)
  end

  true
end

#created_atString?

Returns the container's created at timestamp. The timestamp is in UTC and formatted as ISO 8601. Example: "2014-10-31T23:22:05.430Z".



673
674
675
# File 'lib/testcontainers/docker_container.rb', line 673

def created_at
  @_created_at
end

#dead?Boolean

Returns whether the container is running.

Raises:



583
584
585
# File 'lib/testcontainers/docker_container.rb', line 583

def dead?
  status == "dead"
end

#exec(cmd, options = {}, &block) ⇒ Array, Integer

Executes a command in the container. See https://docs.docker.com/engine/api/v1.42/#operation/ContainerExec for all available options.

Options Hash (options):

  • :tty (Boolean)

    Allocate a pseudo-TTY.

  • :detach (Boolean) — default: false

    Whether to attach to STDOUT/STDERR.

  • :stdin (Object)

    Attach to stdin of the exec command.

  • :wait (Integer)

    The number of seconds to wait for the command to finish.

  • :user (String)

    The user to execute the command as.



796
797
798
799
800
801
# File 'lib/testcontainers/docker_container.rb', line 796

def exec(cmd, options = {}, &block)
  raise ContainerNotStartedError unless @_container
  @_container.exec(cmd, options, &block)
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#exists?Boolean

Returns whether the container exists.

Raises:



657
658
659
660
661
662
663
664
665
666
# File 'lib/testcontainers/docker_container.rb', line 657

def exists?
  return false unless @_id

  Docker::Container.get(@_id)
  true
rescue Docker::Error::NotFoundError
  false
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#exited?Boolean

Returns whether the container is stopped.

Raises:



621
622
623
# File 'lib/testcontainers/docker_container.rb', line 621

def exited?
  status == "exited"
end

#first_mapped_portInteger

Returns the container's first mapped port.

Raises:



736
737
738
739
# File 'lib/testcontainers/docker_container.rb', line 736

def first_mapped_port
  raise ContainerNotStartedError unless @_container
  container_ports.map { |port| mapped_port(port) }.first
end

#get_env(key) ⇒ String?

Returns the value for the given environment variable.



764
765
766
767
# File 'lib/testcontainers/docker_container.rb', line 764

def get_env(key)
  env_entry = env.find { |entry| entry.start_with?("#{key}=") }
  env_entry&.split("=")&.last
end

#healthy?Boolean, false

Returns whether the container is healthy.

Raises:



631
632
633
634
635
636
637
638
639
640
641
# File 'lib/testcontainers/docker_container.rb', line 631

def healthy?
  if supports_healthcheck?
    @_container&.json&.dig("State", "Health", "Status") == "healthy"
  else
    raise HealthcheckNotSupportedError
  end
rescue ContainerNotStartedError
  false
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#hostString

Returns the container's host.

Raises:



696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/testcontainers/docker_container.rb', line 696

def host
  host = docker_host
  return "localhost" if host.nil?
  raise ContainerNotStartedError unless @_container

  if inside_container? && ENV["DOCKER_HOST"].nil?
    gateway_ip = container_gateway_ip
    return bridge_ip if gateway_ip == host
    return gateway_ip
  end
  host
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#infoHash?

Raises:



684
685
686
687
688
689
# File 'lib/testcontainers/docker_container.rb', line 684

def info
  raise ContainerNotStartedError unless @_container
  @_container.json
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#inside_container?Boolean

Returns whether this is running inside a container.



914
915
916
# File 'lib/testcontainers/docker_container.rb', line 914

def inside_container?
  File.exist?("/.dockerenv")
end

#kill(signal: "SIGKILL") ⇒ DockerContainer?

Kills the container with the specified signal

Raises:



528
529
530
531
532
533
534
# File 'lib/testcontainers/docker_container.rb', line 528

def kill(signal: "SIGKILL")
  raise ContainerNotStartedError unless @_container
  @_container.kill(signal: signal)
  self
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#logs(stdout: true, stderr: true) ⇒ Array<String>

Returns the container's logs.

Raises:



776
777
778
779
780
781
782
783
# File 'lib/testcontainers/docker_container.rb', line 776

def logs(stdout: true, stderr: true)
  raise ContainerNotStartedError unless @_container
  stdout = @_container.logs(stdout: stdout)
  stderr = @_container.logs(stderr: stderr)
  [stdout, stderr]
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#mapped_port(port) ⇒ Integer

Returns the mapped host port for the given container port.

Raises:



717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/testcontainers/docker_container.rb', line 717

def mapped_port(port)
  raise ContainerNotStartedError unless @_container
  mapped_port = container_port(port)

  if inside_container?
    gateway_ip = container_gateway_ip
    host = docker_host

    return port.to_i if gateway_ip == host
  end
  mapped_port.to_i
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#mount_namesArray<String>

Returns the container's mount names.

Raises:



755
756
757
# File 'lib/testcontainers/docker_container.rb', line 755

def mount_names
  mounts.map { |mount| mount["Name"] }
end

#mountsArray<Hash>

Returns the container's mounts.

Raises:



746
747
748
# File 'lib/testcontainers/docker_container.rb', line 746

def mounts
  info["Mounts"]
end

#paused?Boolean

Returns whether the container is paused.

Raises:



592
593
594
# File 'lib/testcontainers/docker_container.rb', line 592

def paused?
  status == "paused"
end

#read_file(path) ⇒ String

Reads the contents of a file inside the container.



975
976
977
978
979
# File 'lib/testcontainers/docker_container.rb', line 975

def read_file(path)
  raise ContainerNotStartedError unless @_container

  @_container.read_file(path)
end

#remove(options = {}) ⇒ DockerContainer? Also known as: delete

Removes the container.

Raises:



542
543
544
545
546
547
548
# File 'lib/testcontainers/docker_container.rb', line 542

def remove(options = {})
  @_container&.remove(options)
  @_container = nil
  self
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#restartDockerContainer

Restarts the container.

Raises:



558
559
560
561
562
563
564
# File 'lib/testcontainers/docker_container.rb', line 558

def restart
  raise ContainerNotStartedError unless @_container
  @_container.restart
  self
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#restarting?Boolean

Returns whether the container is restarting.

Raises:



601
602
603
# File 'lib/testcontainers/docker_container.rb', line 601

def restarting?
  status == "restarting"
end

#running?Boolean, false

Returns whether the container is running.

Raises:



610
611
612
613
614
# File 'lib/testcontainers/docker_container.rb', line 610

def running?
  status == "running"
rescue ContainerNotStartedError
  false
end

#startDockerContainer Also known as: enter

Starts the container.

Raises:



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# File 'lib/testcontainers/docker_container.rb', line 474

def start
  Docker::Image.create({"fromImage" => @image}.merge(@image_create_options))

  @_container ||= Docker::Container.create(_container_create_options)
  @_container.start

  @_id = @_container.id
  json = @_container.json
  @name = json["Name"]
  @_created_at = json["Created"]

  @wait_for&.call(self)

  self
rescue Docker::Error::NotFoundError => e
  raise NotFoundError, e.message
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#statusString

Returns the container's status. Possible values are: "created", "restarting", "running", "removing", "paused", "exited", "dead".

Raises:



571
572
573
574
575
576
# File 'lib/testcontainers/docker_container.rb', line 571

def status
  raise ContainerNotStartedError unless @_container
  @_container.json["State"]["Status"]
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#stop(force: false) ⇒ DockerContainer Also known as: exit

Stops the container.

Raises:



502
503
504
505
506
507
508
# File 'lib/testcontainers/docker_container.rb', line 502

def stop(force: false)
  raise ContainerNotStartedError unless @_container
  @_container.stop(force: force)
  self
rescue Excon::Error::Socket => e
  raise ConnectionError, e.message
end

#stop!DockerContainer

Stops the container forcefully.

See Also:



517
518
519
# File 'lib/testcontainers/docker_container.rb', line 517

def stop!
  stop(force: true)
end

#store_file(path, contents) ⇒ Object

Writes the contents of a file inside the container.

Raises:



987
988
989
990
991
# File 'lib/testcontainers/docker_container.rb', line 987

def store_file(path, contents)
  raise ContainerNotStartedError unless @_container

  @_container.store_file(path, contents)
end

#supports_healthcheck?Boolean

Returns whether the container supports healthchecks. This is determined by the presence of a healthcheck in the container's configuration.

Raises:



648
649
650
651
# File 'lib/testcontainers/docker_container.rb', line 648

def supports_healthcheck?
  raise ContainerNotStartedError unless @_container
  @_container.json["Config"]["Healthcheck"] != nil
end

#use {|DockerContainer| ... } ⇒ DockerContainer

Starts the container, yields the container instance to the block, and stops the container.

Yields:



462
463
464
465
466
467
# File 'lib/testcontainers/docker_container.rb', line 462

def use
  start
  yield self
ensure
  stop
end

#wait_for_healthcheck(timeout: 60, interval: 0.1) ⇒ Boolean

Waits for the container to be healthy.

Raises:



836
837
838
839
840
841
842
843
844
845
846
847
848
849
# File 'lib/testcontainers/docker_container.rb', line 836

def wait_for_healthcheck(timeout: 60, interval: 0.1)
  raise ContainerNotStartedError unless @_container
  raise HealthcheckNotSupportedError unless supports_healthcheck?

  Timeout.timeout(timeout) do
    loop do
      return true if healthy?

      sleep interval
    end
  end
rescue Timeout::Error
  raise TimeoutError, "Timed out waiting for health check to be healthy"
end

#wait_for_http(timeout: 60, interval: 0.1, path: "/", container_port: 80, https: false, status: 200) ⇒ Boolean

Waits for the container to respond to HTTP requests.

Raises:



891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/testcontainers/docker_container.rb', line 891

def wait_for_http(timeout: 60, interval: 0.1, path: "/", container_port: 80, https: false, status: 200)
  raise ContainerNotStartedError unless @_container
  raise PortNotMappedError unless mapped_port(container_port)

  Timeout.timeout(timeout) do
    loop do
      begin
        response = Excon.get("#{https ? "https" : "http"}://#{host}:#{mapped_port(container_port)}#{path}")
        return true if response.status == status
      rescue Excon::Error::Socket
        # The container may not be ready to accept connections yet
      end

      sleep interval
    end
  end
rescue Timeout::Error
  raise TimeoutError, "Timed out waiting for HTTP status #{status} on #{path}"
end

#wait_for_logs(matcher, timeout: 60, interval: 0.1) ⇒ Boolean

Waits for the container logs to match the given regex.

Raises:



812
813
814
815
816
817
818
819
820
821
822
823
824
825
# File 'lib/testcontainers/docker_container.rb', line 812

def wait_for_logs(matcher, timeout: 60, interval: 0.1)
  raise ContainerNotStartedError unless @_container

  Timeout.timeout(timeout) do
    loop do
      stdout, stderr = @_container.logs(stdout: true, stderr: true)
      return true if stdout&.match?(matcher) || stderr&.match?(matcher)

      sleep interval
    end
  end
rescue Timeout::Error
  raise TimeoutError, "Timed out waiting for logs to match #{matcher}"
end

#wait_for_tcp_port(port, timeout: 60, interval: 0.1) ⇒ Boolean

Waits for the container to open the given port.

Raises:



861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
# File 'lib/testcontainers/docker_container.rb', line 861

def wait_for_tcp_port(port, timeout: 60, interval: 0.1)
  raise ContainerNotStartedError unless @_container
  raise PortNotMappedError unless mapped_port(port)

  Timeout.timeout(timeout) do
    loop do
      Timeout.timeout(interval) do
        TCPSocket.new(host, mapped_port(port)).close
        return true
      end
    rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error
      sleep interval
    end
  end
rescue Timeout::Error
  raise TimeoutError, "Timed out waiting for port #{port} to open"
end

#with(options) ⇒ DockerContainer

Set options for the container configuration using "with_" methods.



300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/testcontainers/docker_container.rb', line 300

def with(options)
  options.each do |key, value|
    method_name = "with_#{key}"
    if respond_to?(method_name)
      send(method_name, value)
    else
      raise ArgumentError, "Invalid with_ method: #{method_name}"
    end
  end

  self
end

#with_command(*parts) ⇒ DockerContainer

Set the command for the container.



317
318
319
320
321
# File 'lib/testcontainers/docker_container.rb', line 317

def with_command(*parts)
  @command = parts.first.is_a?(Array) ? parts.first : parts

  self
end

#with_entrypoint(*parts) ⇒ DockerContainer

Set the entrypoint for the container.



327
328
329
330
331
# File 'lib/testcontainers/docker_container.rb', line 327

def with_entrypoint(*parts)
  @entrypoint = parts.first.is_a?(Array) ? parts.first : parts

  self
end

#with_env(env_or_key, value = nil) ⇒ DockerContainer

Sets the container's environment variables.



350
351
352
353
# File 'lib/testcontainers/docker_container.rb', line 350

def with_env(env_or_key, value = nil)
  add_env(env_or_key, value)
  self
end

#with_exposed_port(port) ⇒ DockerContainer

Adds a single exposed port to the container.



377
378
379
380
# File 'lib/testcontainers/docker_container.rb', line 377

def with_exposed_port(port)
  add_exposed_ports(port)
  self
end

#with_exposed_ports(*ports) ⇒ DockerContainer

Adds exposed ports to the container.



368
369
370
371
# File 'lib/testcontainers/docker_container.rb', line 368

def with_exposed_ports(*ports)
  add_exposed_ports(*ports)
  self
end

#with_filesystem_binds(filesystem_binds) ⇒ DockerContainer

Adds filesystem binds to the container.



408
409
410
411
# File 'lib/testcontainers/docker_container.rb', line 408

def with_filesystem_binds(filesystem_binds)
  add_filesystem_binds(filesystem_binds)
  self
end

#with_fixed_exposed_port(container_port, host_port = nil) ⇒ DockerContainer Also known as: with_port_binding

Adds a fixed exposed port to the container.



388
389
390
391
# File 'lib/testcontainers/docker_container.rb', line 388

def with_fixed_exposed_port(container_port, host_port = nil)
  add_fixed_exposed_port(container_port, host_port)
  self
end

#with_healthcheck(options = {}) ⇒ DockerContainer

Adds a healthcheck to the container.

Options Hash (options):

  • :test (Array|String)

    the command to run to check the health of the container.

  • :interval (Float)

    the time in seconds between health checks. (default: 30)

  • :timeout (Float)

    the time in seconds to wait for a health check to complete. (default: 30)

  • :retries (Integer)

    the number of times to retry a failed health check before giving up. (default: 3)

  • :shell (Boolean)

    whether or not to run the health check in a shell. (default: false)



441
442
443
444
# File 'lib/testcontainers/docker_container.rb', line 441

def with_healthcheck(options = {})
  add_healthcheck(options)
  self
end

#with_label(label, value) ⇒ DockerContainer

Adds a label to the container.



427
428
429
430
# File 'lib/testcontainers/docker_container.rb', line 427

def with_label(label, value)
  add_label(label, value)
  self
end

#with_labels(labels) ⇒ DockerContainer

Adds labels to the container.



417
418
419
420
# File 'lib/testcontainers/docker_container.rb', line 417

def with_labels(labels)
  add_labels(labels)
  self
end

#with_name(name) ⇒ DockerContainer

Set the name of the container.



337
338
339
340
# File 'lib/testcontainers/docker_container.rb', line 337

def with_name(name)
  @name = name
  self
end

#with_volumes(volumes = {}) ⇒ DockerContainer

Adds volumes to the container.



400
401
402
403
# File 'lib/testcontainers/docker_container.rb', line 400

def with_volumes(volumes = {})
  add_volumes(volumes)
  self
end

#with_wait_for(method = nil, *args, **kwargs, &block) ⇒ DockerContainer

Add a wait_for strategy to the container configuration.



453
454
455
456
# File 'lib/testcontainers/docker_container.rb', line 453

def with_wait_for(method = nil, *args, **kwargs, &block)
  add_wait_for(method, *args, **kwargs, &block)
  self
end

#with_working_dir(working_dir) ⇒ DockerContainer

Sets the container's working directory.



359
360
361
362
# File 'lib/testcontainers/docker_container.rb', line 359

def with_working_dir(working_dir)
  @working_dir = working_dir
  self
end