diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..a13ff12a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+/.bundle/
+/.yardoc
+Gemfile.lock
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+lib/mini_racer_extension.so
+lib/mini_racer_loader.so
+*.bundle
diff --git a/.gitignore b/.gitignore
index ac6d1c17..a13ff12a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ Gemfile.lock
 /spec/reports/
 /tmp/
 lib/mini_racer_extension.so
+lib/mini_racer_loader.so
 *.bundle
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..4cc9f70e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+ARG RUBY_VERSION=2.7
+FROM ruby:${RUBY_VERSION}
+
+# without this `COPY .git`, we get the following error:
+#   fatal: not a git repository (or any of the parent directories): .git
+# but with it we need the full gem just to compile the extension because
+# of gemspec's `git --ls-files`
+# COPY .git /code/.git
+COPY Gemfile mini_racer.gemspec /code/
+COPY lib/mini_racer/version.rb /code/lib/mini_racer/version.rb
+WORKDIR /code
+RUN bundle install
+
+COPY Rakefile /code/
+COPY ext /code/ext/
+RUN bundle exec rake compile
+
+COPY . /code/
+CMD bundle exec irb -rmini_racer
diff --git a/Rakefile b/Rakefile
index e3b92ee8..d125bf74 100644
--- a/Rakefile
+++ b/Rakefile
@@ -11,6 +11,7 @@ end
 task :default => [:compile, :test]
 
 gem = Gem::Specification.load( File.dirname(__FILE__) + '/mini_racer.gemspec' )
+Rake::ExtensionTask.new( 'mini_racer_loader', gem )
 Rake::ExtensionTask.new( 'mini_racer_extension', gem )
 
 
diff --git a/ext/mini_racer_extension/extconf.rb b/ext/mini_racer_extension/extconf.rb
index 7ef61b97..9e60dbe9 100644
--- a/ext/mini_racer_extension/extconf.rb
+++ b/ext/mini_racer_extension/extconf.rb
@@ -14,6 +14,7 @@
 $CPPFLAGS += " -std=c++0x"
 $CPPFLAGS += " -fpermissive"
 $CPPFLAGS += " -DV8_COMPRESS_POINTERS"
+$CPPFLAGS += " -fvisibility=hidden "
 
 $CPPFLAGS += " -Wno-reserved-user-defined-literal" if IS_DARWIN
 
diff --git a/ext/mini_racer_extension/mini_racer_extension.cc b/ext/mini_racer_extension/mini_racer_extension.cc
index 9414407a..fd757688 100644
--- a/ext/mini_racer_extension/mini_racer_extension.cc
+++ b/ext/mini_racer_extension/mini_racer_extension.cc
@@ -1658,7 +1658,7 @@ static void set_ruby_exiting(VALUE value) {
 
 extern "C" {
 
-    void Init_mini_racer_extension ( void )
+    __attribute__((visibility("default"))) void Init_mini_racer_extension ( void )
     {
         VALUE rb_mMiniRacer = rb_define_module("MiniRacer");
         rb_cContext = rb_define_class_under(rb_mMiniRacer, "Context", rb_cObject);
diff --git a/ext/mini_racer_loader/extconf.rb b/ext/mini_racer_loader/extconf.rb
new file mode 100644
index 00000000..e8eaf224
--- /dev/null
+++ b/ext/mini_racer_loader/extconf.rb
@@ -0,0 +1,8 @@
+require 'mkmf'
+
+extension_name = 'mini_racer_loader'
+dir_config extension_name
+
+$CPPFLAGS += " -fvisibility=hidden "
+
+create_makefile extension_name
diff --git a/ext/mini_racer_loader/mini_racer_loader.c b/ext/mini_racer_loader/mini_racer_loader.c
new file mode 100644
index 00000000..0516275d
--- /dev/null
+++ b/ext/mini_racer_loader/mini_racer_loader.c
@@ -0,0 +1,123 @@
+#include <ruby.h>
+#include <dlfcn.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+// Load a Ruby extension like Ruby does, only with flags that:
+// a) hide symbols from other extensions (RTLD_LOCAL)
+// b) bind symbols tightly (RTLD_DEEPBIND, when available)
+
+void Init_mini_racer_loader(void);
+
+static void *_dln_load(const char *file);
+
+static VALUE _load_shared_lib(VALUE self, volatile VALUE fname)
+{
+    (void) self;
+
+    // check that path is not tainted
+    SafeStringValue(fname);
+
+    FilePathValue(fname);
+    VALUE path = rb_str_encode_ospath(fname);
+
+    char *loc = StringValueCStr(path);
+    void *handle = _dln_load(loc);
+
+    return handle ? Qtrue : Qfalse;
+}
+
+// adapted from Ruby's dln.c
+#define INIT_FUNC_PREFIX ((char[]) {'I', 'n', 'i', 't', '_'})
+#define INIT_FUNCNAME(buf, file) do { \
+    const char *base = (file); \
+    const size_t flen = _init_funcname(&base); \
+    const size_t plen = sizeof(INIT_FUNC_PREFIX); \
+    char *const tmp = ALLOCA_N(char, plen + flen + 1); \
+    memcpy(tmp, INIT_FUNC_PREFIX, plen); \
+    memcpy(tmp+plen, base, flen); \
+    tmp[plen+flen] = '\0'; \
+    *(buf) = tmp; \
+} while(0)
+
+// adapted from Ruby's dln.c
+static size_t _init_funcname(const char **file)
+{
+    const char *p = *file,
+               *base,
+               *dot = NULL;
+
+    for (base = p; *p; p++) { /* Find position of last '/' */
+        if (*p == '.' && !dot) {
+            dot = p;
+        }
+        if (*p == '/') {
+            base = p + 1;
+            dot = NULL;
+        }
+    }
+    *file = base;
+    return (uintptr_t) ((dot ? dot : p) - base);
+}
+
+// adapted from Ruby's dln.c
+static void *_dln_load(const char *file)
+{
+    char *buf;
+    const char *error;
+#define DLN_ERROR() (error = dlerror(), strcpy(ALLOCA_N(char, strlen(error) + 1), error))
+
+    void *handle;
+    void (*init_fct)(void);
+
+    INIT_FUNCNAME(&buf, file);
+
+#ifndef RTLD_DEEPBIND
+# define RTLD_DEEPBIND 0
+#endif
+    /* Load file */
+    if ((handle = dlopen(file, RTLD_LAZY|RTLD_LOCAL|RTLD_DEEPBIND)) == NULL) {
+        DLN_ERROR();
+        goto failed;
+    }
+#if defined(RUBY_EXPORT)
+    {
+        static const char incompatible[] = "incompatible library version";
+        void *ex = dlsym(handle, "ruby_xmalloc");
+        if (ex && ex != (void *) &ruby_xmalloc) {
+
+# if defined __APPLE__
+            /* dlclose() segfaults */
+            rb_fatal("%s - %s", incompatible, file);
+# else
+            dlclose(handle);
+            error = incompatible;
+            goto failed;
+#endif
+        }
+    }
+# endif
+
+    init_fct = (void (*)(void)) dlsym(handle, buf);
+    if (init_fct == NULL) {
+        error = DLN_ERROR();
+        dlclose(handle);
+        goto failed;
+    }
+
+    /* Call the init code */
+    (*init_fct)();
+
+    return handle;
+
+failed:
+    rb_raise(rb_eLoadError, "%s", error);
+}
+
+__attribute__((visibility("default"))) void Init_mini_racer_loader()
+{
+    VALUE mMiniRacer = rb_define_module("MiniRacer");
+    VALUE mLoader = rb_define_module_under(mMiniRacer, "Loader");
+    rb_define_singleton_method(mLoader, "load", _load_shared_lib, 1);
+}
diff --git a/lib/mini_racer.rb b/lib/mini_racer.rb
index 2e62e469..9190cea9 100644
--- a/lib/mini_racer.rb
+++ b/lib/mini_racer.rb
@@ -1,5 +1,15 @@
 require "mini_racer/version"
-require "mini_racer_extension"
+require "mini_racer_loader"
+require "pathname"
+
+ext_filename = "mini_racer_extension.#{RbConfig::CONFIG['DLEXT']}"
+ext_path = Gem.loaded_specs['mini_racer'].require_paths
+  .map { |p| (p = Pathname.new(p)).absolute? ? p : Pathname.new(__dir__).parent + p }
+ext_found = ext_path.map { |p| p + ext_filename }.find { |p| p.file? }
+
+raise LoadError, "Could not find #{ext_filename} in #{ext_path.map(&:to_s)}" unless ext_found
+MiniRacer::Loader.load(ext_found.to_s)
+
 require "thread"
 require "json"
 
diff --git a/mini_racer.gemspec b/mini_racer.gemspec
index 9f1243c0..071cd859 100644
--- a/mini_racer.gemspec
+++ b/mini_racer.gemspec
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
   spec.add_dependency 'libv8', MiniRacer::LIBV8_VERSION
   spec.require_paths = ["lib", "ext"]
 
-  spec.extensions = ["ext/mini_racer_extension/extconf.rb"]
+  spec.extensions = ["ext/mini_racer_loader/extconf.rb", "ext/mini_racer_extension/extconf.rb"]
 
   spec.required_ruby_version = '>= 2.3'
 end