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 peek 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
- Explore the documentation and examples.
- Chat with the team on Discord.
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.