skip to content

In Testcontainers, how do you seed a container with a SQL init script or other file?

level: juniorimportance: should knowfreq 45%

answer

  1. Copy the bytes, or mount a path?
  2. Which one needs a local daemon?
  3. MountableFile travels over the API
  4. Init scripts run after startup
  5. Bind mounts fail in CI

basics

~20 s

Use withCopyFileToContainer with a MountableFile to push a file through the Docker API before the container starts, or withInitScript on a JDBC module container to run a SQL script after startup. Bind mounts break with remote daemons.

solid answer

~40 s

Three options, in the order you should prefer them. `withCopyFileToContainer(MountableFile.forClasspathResource("db/init.sql"), "/docker-entrypoint-initdb.d/init.sql")` copies the file into the container's filesystem through the Docker API before it starts, so an image that runs scripts from a well-known directory picks it up on boot. For generated content there is `withCopyToContainer(Transferable.of(text), path)`, which needs no file on disk. For JDBC module containers, `withInitScript("db/init.sql")` is simpler still: Testcontainers connects after startup and executes the script's statements. What you should avoid is `withFileSystemBind(...)` and `withClasspathResourceMapping(...)`: those create bind mounts, which require the file to exist on the machine running the **Docker daemon**. That is true on a laptop and false with a remote daemon, Docker-in-Docker or a cloud runner, so bind-mounted tests pass locally and fail in CI.

code

java · 5 lines
java
PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine")
        .withCopyFileToContainer(
                MountableFile.forClasspathResource("db/extensions.sql"),
                "/docker-entrypoint-initdb.d/01-extensions.sql")
        .withInitScript("db/seed.sql");

go deeper

for a junior

Recall the two everyday calls: withCopyFileToContainer with a MountableFile to place a file, and withInitScript on a JDBC container to run SQL after startup.

for a middle

Explain that copying uploads bytes through the Docker API while a bind mount resolves a path on the daemon's own filesystem, and why that changes behaviour.

for a senior

Predict the CI failure before it happens and standardise on copying, including for generated content via a Transferable, so suites are portable across daemon setups.

for a principal

Own the convention across teams: no bind mounts in tests, so suites move unchanged between laptops, remote daemons and hosted container runtimes.

## The question behind the question A container needs something that is not in its image: a schema, a fixture data file, a certificate, a config file. There are two mechanically different ways to get it there, and the difference decides whether your test survives CI. ## Copying through the Docker API `withCopyFileToContainer(MountableFile.forClasspathResource("db/init.sql"), "/docker-entrypoint-initdb.d/init.sql")` reads the resource from your test classpath and *uploads* it to the container over the Docker API before the container starts. `MountableFile.forHostPath(...)` does the same from a filesystem path. Because the bytes travel through the API, it works no matter where the daemon runs. The target path matters: many images execute anything found in a conventional initialisation directory during boot, which is how a copied SQL file ends up applied without you doing anything else. Copying after start is possible too via `copyFileToContainer(...)`, but then nothing runs it for you. For content you generate in the test — a rendered config, a temporary credential — `withCopyToContainer(Transferable.of("..."), "/etc/app/config.yaml")` writes it directly without a file on disk. ## Running a script instead of placing a file JDBC module containers expose `withInitScript("db/init.sql")`. Here the file is not copied at all: after the database is up, Testcontainers opens a connection and executes the script's statements. That has a useful property — you see SQL errors as a failure in the test JVM rather than buried in container logs — and a limit: it is SQL against the database, so it cannot place a certificate or a config file. ## The bind-mount trap `withFileSystemBind(hostPath, containerPath)` and `withClasspathResourceMapping(resource, containerPath, mode)` do not copy anything. They ask the daemon to mount a path from *its own* filesystem into the container. On a laptop the daemon and your build share a filesystem, so the illusion holds. The moment the daemon is elsewhere — a remote host, Docker-in-Docker, a hosted container runtime — the path does not exist there, and you get an empty directory, a missing file, or a start failure. This is one of the most common "works on my machine" failures in container-backed suites, and the fix is always the same: copy instead of mount. Bind mounts also make the container depend on host file permissions and on build-directory layout, both of which differ between environments. ## Choosing Need SQL applied to a database module? `withInitScript`. Need a file in the image's filesystem, especially in an entrypoint init directory? `withCopyFileToContainer`. Need content computed at test time? `withCopyToContainer` with a `Transferable`. Need a bind mount? Almost never — reach for it only when you knowingly control the daemon's filesystem, and expect it to be the first thing that breaks in CI. ## A note on scope What the script or Dockerfile *contains* is ordinary database or Docker knowledge. What this topic asks is how Testcontainers gets it into the container, and whether your chosen mechanism depends on the daemon being local. ## Interview framing Name one copy mechanism and one init mechanism, and volunteer the bind-mount caveat with its reason — daemon locality — rather than as folklore.

  • Why does a bind mount work locally but fail with a remote Docker daemon?
    A bind mount is resolved on the machine running the daemon, not the machine running your build. Locally these are the same filesystem, so the path exists. With a remote daemon, Docker-in-Docker or a hosted runtime, that path is simply absent, so the container sees nothing or fails to start. Copying through the Docker API sends the bytes instead, so daemon locality stops mattering.
  • When would you copy a file rather than use withInitScript on a database container?
    Whenever the thing you need is not SQL run against the database: a TLS certificate, a server configuration file, a fixture consumed by the image's entrypoint. withInitScript only executes statements over a JDBC connection after startup, so anything that must exist in the container's filesystem — especially before the process boots — has to be copied.
  • How do you get content generated inside the test into the container?
    Use withCopyToContainer with Transferable.of(...), which writes the given bytes or string to a path in the container without needing a file on disk. That suits rendered config, generated credentials or a schema assembled at runtime, and it keeps the temporary content out of your source tree entirely.

saying these in an interview costs you the question

  • Uses withFileSystemBind and is surprised when CI fails
  • Thinks withClasspathResourceMapping copies the file
  • Assumes an init script placement works without knowing the image reads it
  • Expects withInitScript to place arbitrary files
  • Blames the image when a bind-mounted path is empty

context