logo

GuidesFrom Docker Compose to Namespace

Niklas Gehlen
Niklas Gehlen
on

Docker Compose is a favorite tool among developers to manage local development stacks. It provides a seamless workflow that bridges building and running applications and dependencies. Its simple syntax and similarity to Docker make it simple to adopt. At Namespace, we're Docker fans too.

But we built Namespace because:

  • We don't want to write Dockerfiles for most applications. Most application developers want the best build setup with incremental building and maximized caching with zero hassle.
  • We need more reusability: How often do we need to set up Redis, Postgres, or that team dependency? We want to plug these dependencies into our stack but focus on our application.
  • We want a development environment representative of production: We've all been hit by bugs that are not reproducible locally. Many aspects contribute to that, including data set differences, but also the runtime where you run.
  • We want system tests that are simple to write and debug: Tests that are hard to write will never be written. Non-reproducible CI failures lead to deleted tests. We need an environment that is reproducible and automatable.

Docker Compose: getting started

In this example, we'll set up a simple Go program that uses Redis as a backend. But you can pick your favorite language.

You can explore the content below at examples/dockercompose/original.

# Based on https://docs.docker.com/compose/gettingstarted/
version: "3.9"
services:
  web:
    build: .
    ports:
      - "8000:5000"
    environment:
      REDIS_URL: redis:6379
  redis:
    image: "redis:alpine"

Simple, and to the point. But there’s also a Dockerfile.

# syntax=docker/dockerfile:1
FROM golang:1.18-alpine
 
WORKDIR /app
 
COPY go.mod ./
COPY go.sum ./
RUN go mod download
 
COPY *.go ./
 
RUN go build -o /go-server
 
EXPOSE 5000
ENV HTTP_PORT 5000
 
CMD [ "/go-server" ]

The Namespace version

If you don’t have Namespace installed yet, now’s a good time to install it.

Let’s take a look at how to define our application in Namespace:

server: {
  name: "go-server"
 
  integration: "go"
 
  env: {
    REDIS_URL: {
      fromServiceEndpoint: "namespacelabs.dev/foundation/library/oss/redis/server:redis"
    }
  }
 
  services: {
    web: {
      port: 5000
      kind: "http"
    }
  }
 
  requires: [
    "namespacelabs.dev/foundation/library/oss/redis/server",
  ]
}

The full example can be found at examples/dockercompose/withnamespace.

Simple syntax

We've designed our configuration syntax to make it simple to migrate from Docker Compose. So it won't look very different from the original at first glance.

Although you can start simple, later, you can unlock many other features that Namespace provides beyond what Docker Compose can do. We'll explore those in later blog posts.

No Dockerfile required

server: {
  name: "go-server"
 
  integration: "go"
 
  env: {
    // ...

In Namespace's example, we did away with the Dockerfile.

Namespace includes an extensible set of integrations, which bring you best-in-class builder and development support per language. The Go integration knows how to build Go binaries, manages Go SDKs for you, and will already maximize caching and do multi-platform builds.

The node.js integration, for example, automatically sets up a hot reload workflow that minimizes build time to near 0. It also integrates with the most popular package managers and handles user-defined scripts while reducing image sizes.

But if you want to do a custom build, you can, whether it's starting from scratch with your own Dockerfile or layering in additional files or builds.

Using a pre-packaged Redis

server: {
  // ...
 
  env: {
    REDIS_URL: {
      fromServiceEndpoint: "namespacelabs.dev/foundation/library/oss/redis/server:redis"
    }
  }
 
  requires: [
    "namespacelabs.dev/foundation/library/oss/redis/server",
  ]

Maintaining dependencies becomes a burden over time. Most of the time, we want to focus on our application, not our dependencies.

Namespace solves this by allowing you to reference pre-packaged server definitions and inject their runtime configuration into your server as needed. No more manually managing ports and endpoints.

In this case, we're reusing the Redis server available in Namespace's component library. But you could be referencing any server definition, whether it lives in the same or different repositories, private or public.

Taking Namespace for a spin

Now that we have a server definition let's prepare an environment to run it.

cd examples
ns prepare local

Namespace supports various target environments, including your workstation. When running locally, it sets up a Kubernetes cluster within Docker. But don't worry; no need to interact directly with the cluster. Kubernetes offers a lot of power, but it's like the assembly language of the data center. We want to help you work with higher-level languages instead.

After we have an environment to run on, we can start a development session.

ns dev dockercompose/withnamespace

That's it! You can now make changes to your server, and they get automatically redeployed.

Logs will be streamed, and ports will be automatically forwarded as you'd expect.

For a more complex set of dependencies or to more easily jump into a terminal of a server, you can also try out the development Web UI.

Taking a peak under the covers

While you were spinning up servers, Namespace created the necessary Kubernetes resources to run those servers: Deployments, Statefulsets, Services, etc. Similarly to Docker, each server is running in a container, but it is now configured in a way that more closely resembles what you'd see in production when using Kubernetes.

Let us know if you target other runtime environments, whether ECS, Google Cloud Run, or others. Our runtime abstraction allows us to support them as well.

Bonus: running a system integration test in one step

One attractive property of using Namespace is that an application's environment is now committed to your repository; it's reproducible and automatable.

That means we can spin up copies of this environment on demand and run system integration tests that exercise real code paths against real dependencies without any additional effort.

The test driver, which validates the stack, can be built from any language and is deployed alongside your stack. It lives within the private networking of the cluster to be able to issue any calls it needs.

We've put together an example at examples/dockercompose/namespacetest.

Give it a try with:

ns test dockercompose/namespacetest

What to do now

Coming next

In a future blog post, we'll cover Namespace's unique take on managing dependencies beyond servers; we call them Resources. Want to use a database, queue, or any other shared resource, without being exposed to their implementation details? Resources make that a breeze.