Ravindra BagaleCourses & study guides मराठी

Chapter 47: Docker Architecture, Installation on Linux, Windows and Mac, and Docker Desktop Alternatives

47.15 Dev Containers, Testcontainers and Docker in your daily coding

Chala mitrano! Docker is not only for servers; it helps a developer every day too. Two things today: Dev Containers, the whole development setup in one container, and Testcontainers, a real database in a container for tests that starts and stops by itself.

1. Start from day one at a new job

A new developer joins CRED. On day one she spends eight hours installing the right Node version, the right Python version, Postgres, Redis and twenty editor extensions. On day two her tests still fail, because her Postgres version is different from the team's.

Dev Containers fix the first problem. The whole development setup is described in one file, and the editor opens the project inside a container built from it. Every developer gets the same tools in minutes.

Testcontainers fixes the second problem. Your test code starts a real Postgres or Redis container, runs the tests against it, and removes it. No shared test database, no fake database.

Dev Containersthe same coding setup for everyonedevcontainer.jsondescribes tools and extensionsTestcontainersreal services inside testsLibrary in your testsstarts and stops containers
Picture 1: two tools, two jobs

2. Dev Containers

Step 1: What you need

Why: Without the right parts, the editor will not offer to reopen in a container.

What: You need Docker, Visual Studio Code, and the Dev Containers extension from Microsoft. Other editors such as JetBrains IDEs also support the same file.

Example 1: Check Docker

Type this:

docker version --format '{{.Server.Version}}'

You will see:

27.3.1

What it means: Any recent Docker works, including Docker Desktop or Docker Engine in WSL 2.

Example 2: Install the extension

Type this:

code --install-extension ms-vscode-remote.remote-containers

You will see:

Extension 'ms-vscode-remote.remote-containers' was successfully installed.

What it means: This is the official extension ID.

Example 3: Check it

Type this:

code --list-extensions | grep remote-containers

You will see:

ms-vscode-remote.remote-containers

What it means: Ready.

Step 2: Write devcontainer.json

Why: The file is the recipe for the coding setup.

What: The file lives at .devcontainer/devcontainer.json. It names an image, the ports to forward, extensions to install and a command to run after creation.

Example 4: Create the file

Type this:

mkdir -p .devcontainer
printf '{\n  "name": "cred-api",\n  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",\n  "forwardPorts": [3000],\n  "postCreateCommand": "npm ci"\n}\n' > .devcontainer/devcontainer.json
cat .devcontainer/devcontainer.json

You will see:

{
  "name": "cred-api",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "forwardPorts": [3000],
  "postCreateCommand": "npm ci"
}

What it means: A ready-made Node 20 image from Microsoft's devcontainers set.

Example 5: Open in the container

Type this:

(VS Code) F1, then: Dev Containers: Reopen in Container

You will see:

[2 s] Start: Run: docker build ...
[25 s] Running the postCreateCommand ... npm ci
added 112 packages in 6s

What it means: VS Code builds the container, runs npm ci, and reconnects inside it.

Example 6: Check the tools inside

Type this:

node -v; whoami; pwd

You will see:

v20.18.0
node
/workspaces/cred-api

What it means: The terminal in VS Code is now inside the container. Your project is mounted at /workspaces.

Step 3: Add extensions and settings

Why: Everyone should have the same linter and formatter.

What: The customizations.vscode section lists extensions to install inside the container.

Example 7: Add extensions

Type this:

printf '{\n  "name": "cred-api",\n  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",\n  "customizations": {"vscode": {"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]}}\n}\n' > .devcontainer/devcontainer.json

You will see:

(no output)

What it means: ESLint and Prettier will be installed for everyone.

Example 8: Rebuild

Type this:

(VS Code) F1, then: Dev Containers: Rebuild Container

You will see:

Installing extensions...
Extension 'dbaeumer.vscode-eslint' installed

What it means: Changes to the file need a rebuild.

Example 9: Confirm

Type this:

code --list-extensions | grep -E 'eslint|prettier'

You will see:

dbaeumer.vscode-eslint
esbenp.prettier-vscode

What it means: Same tools on every laptop.

Step 4: Add tools with features

Why: You also need the AWS CLI and Docker inside the dev container.

What: Features are small add-on installers. You list them by ID, and they are added on top of the image.

Example 10: Add features

Type this:

printf '{\n  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",\n  "features": {\n    "ghcr.io/devcontainers/features/aws-cli:1": {},\n    "ghcr.io/devcontainers/features/node:1": {"version": "20"}\n  }\n}\n' > .devcontainer/devcontainer.json

You will see:

(no output)

What it means: Two features: AWS CLI and Node 20 on a plain Ubuntu base.

Example 11: Rebuild and check

Type this:

aws --version; node -v

You will see:

aws-cli/2.18.5 Python/3.12.6 Linux
v20.18.0

What it means: Both tools are there, with no manual install.

Example 12: Your laptop is untouched

Type this:

# on the host terminal
which aws || echo 'not on host'

You will see:

not on host

What it means: The tools live only in the container.

Step 5: Add a database with Compose

Why: Most apps need Postgres or Redis while coding.

What: devcontainer.json can point to a compose.yaml. The editor connects to one service, and the others run beside it.

Example 13: The compose file

Type this:

printf 'services:\n  app:\n    image: mcr.microsoft.com/devcontainers/javascript-node:20\n    volumes: ["..:/workspaces/cred-api"]\n    command: sleep infinity\n  db:\n    image: postgres:16\n    environment:\n      POSTGRES_PASSWORD: devonly\n' > .devcontainer/compose.yaml

You will see:

(no output)

What it means: devonly is a local practice password, not a real secret.

Example 14: Point devcontainer.json at it

Type this:

printf '{\n  "name": "cred-api",\n  "dockerComposeFile": "compose.yaml",\n  "service": "app",\n  "workspaceFolder": "/workspaces/cred-api"\n}\n' > .devcontainer/devcontainer.json

You will see:

(no output)

What it means: The editor attaches to app.

Example 15: Reach the database by name

Type this:

(inside the dev container)
nc -zv db 5432

You will see:

Connection to db 5432 port [tcp/postgresql] succeeded!

What it means: db is the service name from compose.yaml.

Step 6: Use it from the command line

Why: CI servers have no editor, but should use the same setup.

What: The Dev Container CLI reads the same file and builds the same container.

Example 16: Install the CLI

Type this:

npm install -g @devcontainers/cli
devcontainer --version

You will see:

0.71.0

What it means: A Node package. Your version may differ.

Example 17: Start the container

Type this:

devcontainer up --workspace-folder .

You will see:

{"outcome":"success","containerId":"5b1c...","remoteUser":"node"}

What it means: The same container, without VS Code.

Example 18: Run tests inside it

Type this:

devcontainer exec --workspace-folder . npm test

You will see:

Tests: 24 passed, 24 total

What it means: CI and laptops now run tests in the same setup.

Ravindra Bagale's Tip

Commit devcontainer.json to Git. A new developer only clicks 'Reopen in Container' and starts coding on day one.

3. Testcontainers

Test startsJUnit, pytest, Jest or Go testLibrary starts Postgreson a random free portTest runs real SQLagainst a real databaseLibrary removes itnothing left behind
Picture 2: a test with Testcontainers

Step 7: The idea and what you need

Why: Fake databases in tests miss real bugs, like a SQL feature your Postgres version does not have.

What: Testcontainers is a library for Java, Python, Node, Go, .NET and more. In test code you ask for a container; it starts it, gives you the address and removes it after. It only needs a running Docker.

Example 19: Check Docker from Python

Type this:

pip install testcontainers[postgres]==4.8.2 psycopg2-binary
python3 -c 'import docker; print(docker.from_env().ping())'

You will see:

Successfully installed testcontainers-4.8.2 ...
True

What it means: The library can talk to Docker.

Example 20: Start Postgres in three lines

Type this:

python3 -c 'from testcontainers.postgres import PostgresContainer
with PostgresContainer("postgres:16") as pg: print(pg.get_connection_url())'

You will see:

Pulling image postgres:16
postgresql+psycopg2://test:test@localhost:49153/test

What it means: A random free port, 49153, so tests never clash.

Example 21: See the helper container

Type this:

docker ps --format '{{.Image}} {{.Names}}'   # while a test runs

You will see:

testcontainers/ryuk:0.8.1 testcontainers-ryuk-3f1a...
postgres:16 sharp_hopper

What it means: Ryuk is a small cleanup container. If your test crashes, Ryuk removes the leftovers.

Step 8: A pytest example

Why: A Swiggy orders service must save and read an order correctly.

What: A pytest fixture starts Postgres once for all tests in the file.

Example 22: The test file

Type this:

printf 'import pytest, psycopg2\nfrom testcontainers.postgres import PostgresContainer\n@pytest.fixture(scope="module")\ndef db():\n    with PostgresContainer("postgres:16", driver=None) as pg:\n        yield psycopg2.connect(pg.get_connection_url())\ndef test_order(db):\n    c=db.cursor(); c.execute("CREATE TABLE o(id int, item text)")\n    c.execute("INSERT INTO o VALUES (1, %%s)", ("biryani",))\n    c.execute("SELECT item FROM o"); assert c.fetchone()[0]=="biryani"\n' > test_orders.py

You will see:

(no output)

What it means: driver=None gives a plain postgresql:// URL for psycopg2.

Example 23: Run it

Type this:

pytest -q test_orders.py

You will see:

1 passed in 4.21s

What it means: A real Postgres started, the test passed, and Postgres was removed.

Example 24: Check nothing is left

Type this:

docker ps -a --filter ancestor=postgres:16 -q | wc -l

You will see:

0

What it means: Clean.

Step 9: A Java JUnit example

Why: Java teams at banks such as ICICI often use Spring Boot with JUnit 5.

What: The Java library uses the @Testcontainers and @Container annotations.

Example 25: The test class

Type this:

printf '@Testcontainers\nclass RepoTest {\n  @Container\n  static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");\n  @Test void starts() { assertTrue(pg.isRunning()); }\n}\n' > src/test/java/RepoTest.java

You will see:

(no output)

What it means: The container starts before the tests and stops after.

Example 26: Run with Maven

Type this:

mvn -q test

You will see:

[INFO] Tests run: 1, Failures: 0, Errors: 0

What it means: Add the org.testcontainers postgresql and junit-jupiter test dependencies to pom.xml first.

Example 27: With Spring Boot

Type this:

printf '@SpringBootTest\n@Testcontainers\nclass AppTest {\n  @Container @ServiceConnection\n  static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16");\n}\n' > src/test/java/AppTest.java

You will see:

(no output)

What it means: @ServiceConnection wires the database URL into Spring for you.

Step 10: Ready-made modules for other services

Why: Real apps at Zepto talk to Kafka, S3 and MySQL, not only Postgres.

What: Testcontainers has modules, which are ready-made classes for common services: MySQL, MongoDB, Kafka, LocalStack for AWS services, and more. For any other image there is a generic container.

Example 28: MySQL in Python

Type this:

python3 -c 'from testcontainers.mysql import MySqlContainer
with MySqlContainer("mysql:8.4") as m: print(m.get_connection_url().split("@")[1])'

You will see:

localhost:49161/test

What it means: Same pattern, different database. Only the host and port are printed here.

Example 29: Fake AWS S3 with LocalStack

Type this:

python3 -c 'from testcontainers.localstack import LocalStackContainer
with LocalStackContainer("localstack/localstack:3.8") as ls: print(ls.get_url())'

You will see:

http://localhost:49170

What it means: Point your boto3 client at this URL to test S3 code without touching real AWS.

Example 30: Any image with the generic container

Type this:

python3 -c 'from testcontainers.core.container import DockerContainer
with DockerContainer("nginx:1.27").with_exposed_ports(80) as n: print(n.get_exposed_port(80))'

You will see:

49175

What it means: Use this when no module exists.

Step 11: A Node example and CI

Why: Jenkins agents must run these tests too.

What: The Node library works the same way. In CI, the agent needs Docker access.

Example 31: Node test

Type this:

npm i -D @testcontainers/redis redis
printf 'const {RedisContainer}=require("@testcontainers/redis");\ntest("cache",async()=>{const r=await new RedisContainer("redis:7").start();expect(r.getConnectionUrl()).toMatch(/^redis:/);await r.stop();},60000);\n' > cache.test.js
npx jest cache.test.js

You will see:

PASS ./cache.test.js
  ✓ cache (3120 ms)

What it means: Redis 7 started and stopped inside the test.

Example 32: In a Jenkins agent

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/src -w /src maven:3.9-eclipse-temurin-21 mvn -q test

You will see:

[INFO] Tests run: 1, Failures: 0

What it means: The build container uses the host's Docker through the socket. Use this only on trusted build agents.

Example 33: When Docker is missing

Type this:

mvn -q test   # on a machine with no Docker

You will see:

Could not find a valid Docker environment

What it means: Testcontainers needs Docker. Fix the agent, not the test.

Check yourself

Q1. Where does a Dev Container definition live?

In .devcontainer/devcontainer.json in the project.

Q2. What are features in a dev container?

Small add-on installers, such as the AWS CLI or Node, added to the image.

Q3. Can CI use the same dev container?

Yes, with the Dev Container CLI: devcontainer up and devcontainer exec.

Q4. Why does Testcontainers use random ports?

So many tests can run at once without clashing.

Q5. What does the Ryuk container do?

It removes leftover test containers if a test crashes.

Friends, Dev Containers give everyone the same coding setup, and Testcontainers let tests use a real database. Both need only Docker. Did you understand? Let us try it once more. If not, look at Pictures 1 and 2 again.

Lab: try at home

  1. Install the Dev Containers extension and open a Node project in a container.
  2. Add ESLint as an extension and the AWS CLI as a feature, then rebuild.
  3. Add Postgres through compose.yaml and connect to db on port 5432.
  4. Write the pytest example and run it; confirm no container is left.
  5. Run the same tests through devcontainer exec.

Recap

  • Dev Containers put the whole coding setup in one file.
  • Features add tools; Compose adds databases.
  • The Dev Container CLI brings the same setup to CI.
  • Testcontainers starts real services inside tests.
  • Ryuk cleans up, and random ports avoid clashes.

Lab

  1. Install Docker on an Ubuntu EC2 and run hello-world.
  2. Run docker version and name each part.
  3. Start nginx with --memory 256m and find its host PID and namespaces.
  4. Run docker system df and docker events in two terminals while you start and stop a container.
  5. Set up a docker context to a second server over SSH.

Practice task

Install Docker on two machines (one Ubuntu, one Amazon Linux 2023), then use docker context from your laptop to list containers on both without SSHing in.

Ravindra Bagale's Tip

In interviews, 'what happens inside when you run docker run' is sure to come. Learn the order: CLI, dockerd, containerd, runc, kernel.

In short

  • Containers package the app with its runtime, so it runs the same everywhere.
  • Containers share the host kernel; VMs bring their own.
  • CLI → dockerd → containerd → runc → kernel.
  • Namespaces isolate what a process sees; cgroups limit what it uses.
  • Use Docker's official repos; on Windows use WSL 2.
  • Docker Desktop needs a paid plan for larger companies; free alternatives exist.