library / lib6130a2990f6dc37b
Bootstrapping Microservices, Second Edition With Docker, Kubernetes, GitHub Actions, and Terraform
Ashley Davis
In a sentence
A hands-on, practical guide that takes developers from zero to a fully deployed, continuously delivered microservices application using Docker, Kubernetes, and Terraform—cutting through the steep learning curve by building a real video-streaming platform step by step.
Bootstrapping Microservices, Second Edition demolishes the myth that microservices are too complex for individuals or small teams to adopt from the start. Author Ashley Davis guides readers through building FlixTube, a real video-streaming application, starting with a single Node.js microservice and progressively adding Docker packaging, Docker Compose for local development, cloud file storage, databases, inter-service messaging with HTTP and RabbitMQ, Kubernetes production deployment, Terraform infrastructure-as-code, GitHub Actions continuous deployment pipelines, and automated testing with Jest and Playwright. Every concept is introduced at the moment it is needed, and every tool is demonstrated with working code you can run yourself. By the end, readers possess a transferable, language-agnostic recipe for shipping any microservices application with confidence.
The four lenses
- Science
- Statistics
- Systems
- Strategy
Tags
The model
A causal model describing how architectural design choices, tooling adoption decisions, and engineering practices (design levers and contextual conditions) shape the psychological and behavioral states of development teams, which in turn determine application-level and business-level outcomes such as deployment frequency, system reliability, scalability, and developer productivity.
Microservice Granularitydesign lever
The degree to which an application's functionality is decomposed into small, independently deployable processes, each owning a single bounded context and a single area of responsibility. Higher granularity means more, smaller services; lower granularity approaches a monolith.
Inter-Service Loose Couplingdesign lever
The extent to which microservices minimize direct dependencies on one another's internal implementations, preferring well-defined API contracts and indirect messaging over shared databases or tightly synchronized call chains. Loose coupling is a core design principle enabling independent deployability.
Single Responsibility Adherencedesign lever
The degree to which each microservice is designed and maintained to cover only one conceptual area of business domain responsibility, avoiding feature creep that would expand scope and increase coupling with other services.
Containerization Adoption (Docker)design lever
The extent to which the team packages every microservice as an immutable Docker image with a Dockerfile, publishes images to a private container registry, and runs all services in containers consistently across development, testing, and production environments.
Infrastructure-as-Code Adoption (Terraform)design lever
The degree to which cloud infrastructure (container registries, Kubernetes clusters, networking, role assignments) is defined in version-controlled declarative code (Terraform HCL) rather than created manually through UIs or ad-hoc CLI commands, enabling repeatable and automated provisioning.
Continuous Deployment Pipeline Maturitydesign lever
The sophistication and reliability of automated pipelines (e.g., GitHub Actions workflows) that detect code changes, build and test service images, publish them to a registry, and deploy them to production Kubernetes without manual intervention. Higher maturity means fewer manual steps and faster, more reliable delivery.
Automated Test Coverage and Layeringdesign lever
The breadth and depth of automated tests applied to the microservices application across the three tiers of the testing pyramid: unit tests (isolated function-level testing with mocks), integration tests (whole-microservice HTTP-level testing against real dependencies), and end-to-end tests (full-application browser-level testing via Playwright and Docker Compose).
Local Development Environment Qualitydesign lever
The degree to which the local development setup (Docker Compose, live reload with nodemon, separate dev/prod Dockerfiles, shared volumes) enables fast iterative coding cycles with minimal rebuild latency, environment parity with production, and easy project switching across a multi-microservice application.
Indirect (Asynchronous) Messaging Usagedesign lever
The extent to which the application uses message queues and exchanges (RabbitMQ) rather than synchronous HTTP calls for inter-service communication, allowing senders and receivers to be decoupled in time, identity, and failure modes.
Database-Per-Service Disciplinedesign lever
The practice of assigning each microservice its own isolated database (or database namespace), preventing any service from accessing another service's data store directly and thus encapsulating data management behind each service's API boundary.
Deployment Confidencepsychological state
The psychological state in which development team members feel safe and comfortable pushing code changes to production frequently, because automated pipelines, automated tests, and reversible deployment strategies reduce the perceived and actual risk of any individual deployment causing an outage.
Perceived Complexity Manageabilitypsychological state
The degree to which developers and teams feel that the complexity of the overall system is understandable and manageable, even as the application grows, because each individual service remains small, focused, and independently comprehensible without requiring global system knowledge.
Developer Productivitybehavioral pattern
The rate and quality of output by individual developers or development teams, encompassing how quickly new features can be implemented, bugs fixed, and experiments run within the microservices architecture, as shaped by tooling quality, iteration speed, and cognitive load.
Deployment Frequencyoutcome metric
How often the team successfully deploys code changes to production, reflecting the combined effect of pipeline automation maturity, test coverage, deployment confidence, and the granularity and independence of microservices. Higher frequency is associated with faster customer feedback and lower per-deployment risk.
System Reliability and Fault Toleranceoutcome metric
The degree to which the production microservices application continues to operate correctly under load, partial failures, and individual service crashes, enabled by Kubernetes deployment controllers that restart failed pods, independent service fault isolation, and RabbitMQ message durability that prevents data loss during transient failures.
Application Scalabilityoutcome metric
The capacity of the application to accommodate growth in customer demand (horizontal scaling of individual services via additional pod replicas) and growth in development team size (independent team ownership of separate services), without requiring coordinated changes across the entire codebase or monolithic redeployment.
Codebase Evolvability and Disposabilityoutcome metric
The ease with which individual services can be rewritten, replaced, or restructured without cascading changes across the application, enabled by hard process boundaries, well-defined API contracts, loose coupling, and the design principle that each service should be small enough to be disposable and rewritable within days or weeks.
Team Size and Prior Experiencecontextual condition
A contextual moderating condition capturing whether the development team is a solo developer, a small startup team, or a large engineering organization, and the prior experience level with distributed systems, Docker, Kubernetes, and cloud infrastructure, which significantly moderates the cost-benefit trade-off of adopting microservices.
Application Growth Trajectorycontextual condition
A contextual condition representing whether the application is expected to remain small and stable (making a monolith appropriate) or will continuously evolve, grow in feature scope, and scale to a larger customer base and development team (making microservices the more economical long-run choice).
How they connect
- microservice granularity → predicts loose coupling
- microservice granularity → predicts complexity manageability
- microservice granularity → predicts code evolvability
- loose coupling → predicts system reliability
- loose coupling → predicts code evolvability
- indirect messaging usage → predicts loose coupling
- indirect messaging usage → predicts system reliability
- database per service → predicts loose coupling
- containerization adoption → predicts local dev environment quality
- containerization adoption → predicts cd pipeline maturity
- infra as code adoption → predicts cd pipeline maturity
- infra as code adoption → predicts application scalability
- cd pipeline maturity → predicts deployment confidence
- cd pipeline maturity → predicts deployment frequency
- automated test coverage → predicts deployment confidence
- automated test coverage → predicts system reliability
- local dev environment quality → predicts developer productivity
- deployment confidence → predicts deployment frequency
- complexity manageability → predicts developer productivity
- developer productivity → predicts deployment frequency
- deployment frequency → predicts code evolvability
- microservice granularity → predicts application scalability
- team size and experience → moderates microservice granularity
- application growth trajectory → moderates microservice granularity
- single responsibility adherence → predicts complexity manageability
- automated test coverage → predicts code evolvability
The process
The book's operating playbook guides a developer through a microservices-first approach, starting from the creation of a single service and progressively building towards a complete, production-ready distributed application. The core methodology begins with local development, where individual microservices are built using Node.js, containerized with Docker, and orchestrated for multi-service development using Docker Compose. This local loop is optimized for rapid iteration with an application-wide live reload system. The playbook emphasizes robust data management strategies, including dedicated databases and abstracted cloud file storage, and details both direct (HTTP) and indirect (RabbitMQ) inter-service communication patterns. Once the local development workflow is established, the playbook transitions to production readiness. It introduces infrastructure as code using Terraform to define and provision all necessary cloud resources, such as a container registry and a Kubernetes cluster, in a repeatable and automated manner. The process culminates in the creation of a continuous deployment (CD) pipeline with GitHub Actions. This pipeline automates the building, testing, and deployment of each microservice to the Kubernetes cluster whenever code is updated. Underpinning this entire lifecycle is a multi-layered automated testing strategy. The book prescribes unit testing with Jest for isolated function validation, integration testing for verifying entire services with their real dependencies, and end-to-end testing with Playwright to validate user flows through the complete application. This comprehensive playbook equips a developer to build, test, deploy, and automate a scalable and maintainable microservices application from the ground up.
Create a Single Microservice
To build a foundational, runnable microservice that can serve as a building block for a larger application.
When to use: When starting a new microservices project or adding a new service to an existing application.
Step 1Create a new Node.js project directory and initialize it.
Entry: Node.js and npm are installed.
Exit: A `package.json` file exists in the project directory.
Out: package.json file
Step 2Install necessary dependencies.
Entry: A `package.json` file exists.
Exit: Dependencies are listed in `package.json` and installed in the `node_modules` directory.
In: List of required packages · Out: Updated package.json, package-lock.json, node_modules directory
Step 3Create the main source file and implement the HTTP server.
Entry: Express is installed.
Exit: A runnable `index.js` file exists that starts an HTTP server.
Out: index.js file
Step 4Configure the microservice using environment variables.
Entry: The server code is written.
Exit: The microservice's configuration is driven by environment variables instead of hardcoded values.
Out: Updated index.js
Step 5Set up npm scripts for development and production modes.
Entry: The microservice is runnable.
Exit: The microservice can be started with `npm start` (production) or `npm run start:dev` (development).
Out: Updated package.json
Package and Publish a Microservice with Docker
To containerize a microservice into a portable Docker image and publish it to a private registry, making it ready for deployment.
When to use: After a microservice has been developed and is ready to be prepared for deployment.
Step 1Create a Dockerfile for the microservice.
Entry: A complete microservice project exists.
Exit: A `Dockerfile` is present in the microservice's root directory.
In: Microservice source code · Out: Dockerfile
Step 2Build the Docker image.
Entry: A Dockerfile exists and Docker is installed.
Exit: A new Docker image is created and visible via `docker image list`.
In: Dockerfile · Out: Docker image
Step 3Create a private container registry.
Entry: A cloud provider account is available.
Exit: A private container registry is created and its credentials are known.
Out: Container Registry URL and credentials
Step 4Log in to the container registry.
Entry: Registry credentials are known.
Exit: The local Docker client is authenticated with the remote registry.
In: Registry URL, Username, Password
Step 5Tag the image for the registry.
Entry: A local Docker image exists and the Docker client is logged in.
Exit: A new tag for the image, including the registry URL, is visible via `docker image list`.
In: Local image name, Registry URL · Out: Tagged Docker image
Step 6Push the image to the registry.
Entry: The image is correctly tagged and the client is authenticated.
Exit: The image is available in the private container registry.
In: Tagged Docker image · Out: Published Docker image in registry
Develop a Multi-Service Application Locally with Docker Compose
To configure, build, and run an entire application consisting of multiple microservices and backing services (like databases) on a local development machine.
When to use: During the development phase of a microservices application to simulate the interaction of multiple services.
Step 1Organize the project with a subdirectory for each microservice.
Entry: At least one microservice project exists.
Exit: The project has a clear directory structure separating each service.
Out: Project directory structure
Step 2Create a `docker-compose.yaml` file in the project root.
Entry: Project directory is structured.
Exit: An empty `docker-compose.yaml` file exists.
Out: docker-compose.yaml
Step 3Define each microservice in the Docker Compose file.
Entry: Each microservice has a Dockerfile.
Exit: All custom microservices are defined as services in `docker-compose.yaml`.
In: Dockerfile paths, Port mappings, Environment variables · Out: Updated docker-compose.yaml
Step 4Define third-party backing services.
Entry: The application requires backing services.
Exit: All backing services are defined in `docker-compose.yaml`.
In: Public image names (e.g., 'mongo:7.0.0') · Out: Updated docker-compose.yaml
Step 5Boot the entire application.
Entry: The `docker-compose.yaml` file is complete.
Exit: All services are running in containers, and their aggregated logs are visible in the terminal.
In: docker-compose.yaml · Out: Running application stack
Step 6Shut down the application.
Entry: The application is running.
Exit: All application-related Docker resources are removed.
Set Up Application-Wide Live Reload
To enable a fast development workflow where code changes in any microservice automatically trigger a reload of only that service, without needing to rebuild images or restart the entire application.
When to use: When setting up the local development environment for a multi-service application to improve developer productivity.
Step 1Install `nodemon` and create a `start:dev` script in each microservice.
Entry: Each microservice is a Node.js project.
Exit: Each microservice has `nodemon` installed and a `start:dev` script.
Out: Updated package.json in each microservice
Step 2Create separate Dockerfiles for development and production.
Entry: Each microservice has a single Dockerfile.
Exit: Each microservice has two distinct Dockerfiles for dev and prod environments.
Out: Dockerfile-dev, Dockerfile-prod
Step 3Configure the development Dockerfile (`Dockerfile-dev`).
Entry: Two Dockerfiles exist.
Exit: The `Dockerfile-dev` is configured to install dependencies and run `nodemon` at container startup.
Out: Updated Dockerfile-dev
Step 4Update `docker-compose.yaml` to enable live reload.
Entry: A `docker-compose.yaml` file exists.
Exit: The Docker Compose file uses the development Dockerfile and mounts source code volumes for each service.
Out: Updated docker-compose.yaml
Step 5Start the application and test live reload.
Entry: All configurations are complete.
Exit: Code changes on the host are reflected in the running container without a manual restart.
In: A code change · Out: Automatic service restart
Create Production Infrastructure with Terraform
To define, provision, and manage cloud infrastructure as code, ensuring a repeatable, automated, and version-controlled setup for the production environment.
When to use: Before deploying an application to a new environment, or when managing the lifecycle of cloud infrastructure.
Step 1Install Terraform and authenticate with the cloud provider.
Entry: A cloud provider account is available.
Exit: Terraform is installed and the CLI is authenticated.
Step 2Create Terraform configuration files (`.tf`).
Entry: A directory for the infrastructure code is created.
Exit: Terraform files defining the desired infrastructure exist.
In: Infrastructure requirements (e.g., cluster size, region) · Out: Terraform configuration files
Step 3Initialize the Terraform project.
Entry: Provider definitions exist in `providers.tf`.
Exit: The `.terraform` directory is created and populated with provider plugins.
In: providers.tf · Out: .terraform directory
Step 4Apply the configuration to create the infrastructure.
Entry: The project is initialized and configuration files are complete.
Exit: The cloud resources defined in the `.tf` files are created in the cloud provider account.
- Approve or reject the execution plan.
In: Terraform configuration files · Out: Live cloud infrastructure, terraform.tfstate file
Step 5Destroy the infrastructure when no longer needed.
Entry: Infrastructure was previously created with `terraform apply`.
Exit: All managed cloud resources are deleted.
In: terraform.tfstate file
Create a Continuous Deployment Pipeline with GitHub Actions
To automate the process of building, publishing, and deploying a microservice to a Kubernetes cluster whenever code changes are pushed to the repository.
When to use: After establishing a manual deployment process, to automate it for reliability, speed, and consistency.
Step 1Create shell scripts for the deployment logic.
Entry: A manual deployment process using CLI commands is understood.
Exit: Runnable shell scripts for build, push, and deploy exist.
Out: Deployment shell scripts
Step 2Template the Kubernetes deployment configuration.
Entry: A static `deploy.yaml` file exists.
Exit: The `deploy.yaml` file is a template ready for value substitution.
Out: Templated deploy.yaml
Step 3Configure necessary secrets in the GitHub repository.
Entry: Credentials for the container registry and Kubernetes cluster are known.
Exit: All required secrets are stored in GitHub Actions.
In: Container registry credentials, Kube config
Step 4Create the GitHub Actions workflow YAML file.
Entry: The GitHub repository exists.
Exit: A workflow file with a trigger is created.
Out: Workflow YAML file
Step 5Define the deployment job and its steps.
Entry: The workflow file and shell scripts exist.
Exit: The workflow file contains a complete sequence of steps for deployment.
In: Deployment shell scripts · Out: Complete workflow YAML file
Step 6Trigger the workflow and monitor its execution.
Entry: The workflow is committed to the repository.
Exit: The microservice is successfully deployed to Kubernetes by the automated workflow.
In: A code commit · Out: A deployed microservice
Unit Test a Microservice
To test individual functions or units of code in complete isolation from their dependencies, ensuring that each small piece of logic works correctly.
When to use: During development, to verify the correctness of new or modified functions.
Step 1Set up the Jest testing framework.
Entry: A Node.js project exists.
Exit: Jest is installed and configured.
Out: jest.config.js
Step 2Create a test file for the code to be tested.
Entry: A source code file with functions to test exists.
Exit: An empty test file is created.
Out: Test file (e.g., index.test.js)
Step 3Mock all external dependencies.
Entry: The code under test has external dependencies.
Exit: When the test runs, the code under test will call the mock implementations instead of the real ones.
Out: Mock implementations
Step 4Write a test case for a specific function or behavior.
Entry: The test file is set up with mocks.
Exit: A test block is defined.
Step 5Invoke the code under test and assert the result.
Entry: A test block is defined.
Exit: The test case includes an assertion.
In: Test inputs for the function
Step 6Run the tests.
Entry: At least one test case is written.
Exit: Test results (pass or fail) are displayed in the terminal.
Out: Test report
Integration Test a Microservice
To test a complete microservice with its real, live dependencies (like a database) to verify that all its internal components work together correctly.
When to use: After unit tests are passing, to validate the service as a whole before deploying or running end-to-end tests.
Step 1Ensure backing services are available.
Entry: The microservice has external dependencies.
Exit: All backing services are running and accessible.
Step 2Set up the test file with setup and teardown logic.
Entry: A test file is created.
Exit: The test file has logic to manage the microservice's lifecycle.
Step 3Create a helper function to load database fixtures.
Entry: The test needs a specific database state.
Exit: A function to seed the database is available.
In: Test data
Step 4Write a test case that interacts with the microservice's API.
Entry: The microservice is running.
Exit: The test makes a real HTTP request to the service.
Out: HTTP response
Step 5Assert the HTTP response.
Entry: An HTTP response has been received.
Exit: The test includes assertions about the HTTP response.
In: HTTP response
Step 6Run the integration tests.
Entry: At least one integration test is written.
Exit: Test results are displayed.
Out: Test report
End-to-End Test the Application
To validate complete user flows through the application, from the user interface to the backend microservices and databases, simulating real user behavior.
When to use: Before a release, to verify that the integrated system meets business requirements and that critical user paths are working.
Step 1Set up the Playwright testing framework.
Entry: A project with a frontend exists.
Exit: Playwright is installed and configured.
Out: playwright.config.js
Step 2Prepare the application environment for testing.
Entry: A `docker-compose.yaml` file for development exists.
Exit: A runnable Docker Compose setup for the test environment is ready.
Out: Test-specific docker-compose.yaml
Step 3Start the application stack.
Entry: The Docker Compose file is ready.
Exit: The full application is running locally.
Out: Running application
Step 4Write an end-to-end test case.
Entry: The application is running.
Exit: A test case with data setup is defined.
Step 5Script browser interactions using Playwright's API.
Entry: A test case is defined.
Exit: The test case includes browser interactions and assertions.
Step 6Run the end-to-end tests.
Entry: The application is running and at least one test is written.
Exit: The test report is generated and displayed.
Out: Test report
The story
The reader A software developer—solo, on a small team, or at a startup—who wants to build a real, production-ready microservices application but feels intimidated by the steep learning curve, the sprawling toolchain, and the fear of getting the foundational decisions wrong.
External problem
The developer cannot get a microservices application from zero to production without mastering an overwhelming number of tools (Docker, Kubernetes, Terraform, CI/CD pipelines, messaging systems, databases) all at once, with no clear starting point.
Internal problem
They feel overwhelmed, stuck, and unsure whether they are even capable of succeeding with microservices as an individual or small team, leading to either paralysis or defaulting to a monolith they know they will regret later.
Philosophical problem
It is wrong that the most scalable, flexible, and evolvable application architecture should be gatekept behind months of painful self-directed learning, leaving developers trapped in monoliths they cannot escape.
The plan
- Create a single microservice (a Node.js HTTP video-streaming server) to understand the atomic unit of the architecture.
- Package and publish the microservice as a Docker image to a private container registry on Azure.
- Use Docker Compose to run multiple microservices, a database (MongoDB), and cloud file storage (Azure Storage) together locally.
- Wire microservices together using direct messaging (HTTP POST) and indirect messaging (RabbitMQ) and learn when to use each.
- Deploy a microservice to the local Kubernetes instance bundled with Docker Desktop to practice without cloud cost, then deploy to a managed Azure Kubernetes Service cluster.
- Automate infrastructure creation (container registry, Kubernetes cluster) using Terraform and infrastructure-as-code principles.
- Build a continuous deployment pipeline with GitHub Actions that automatically builds, publishes, and deploys microservices on every code push.
- Add automated testing at all three levels—unit (Jest), integration (Jest + real database), and end-to-end (Playwright + Docker Compose)—and wire tests into the CI pipeline.
- Deploy the full FlixTube application to production and learn to monitor, debug, and maintain healthy microservices.
- Understand pathways to scalability: multi-node clusters, service replication, blue-green deployment, and monolith decomposition strategies.
Success
- The developer can create, package, publish, and deploy individual microservices independently.
- The entire application is continuously and automatically deployed to production with a single git push, requiring no manual intervention.
- Infrastructure is created and destroyed reliably through code, enabling multiple parallel environments (development, testing, production) with minimal effort.
- The codebase is covered by a layered automated test suite that catches regressions before they reach customers.
- The developer feels confident decomposing any domain into bounded microservices and wiring them together with the right communication patterns.
- Scaling the application—more services, more nodes, larger teams—becomes a manageable and well-understood exercise rather than a crisis.
At stake
- The developer remains stuck with a monolith that grows into an unmanageable 'big ball of mud', accumulating deployment fear that slows innovation to a crawl.
- Manual deployments become a bottleneck that consumes developer time and introduces human error, making it impossible to scale the number of microservices.
- Without automated testing, broken code reaches production customers repeatedly, damaging the business and eroding trust.
- The developer remains locked into a single cloud vendor's proprietary container service and cannot migrate without rewriting deployment infrastructure.
- The opportunity to go microservices-first on a greenfield project is missed, and the team later faces the far more expensive and risky task of decomposing a large legacy monolith under production load.
Questions this book answers
- How do you build and deploy a microservices application from scratch without being overwhelmed by complexity?
- When should you choose microservices over a monolith, and what is the real spectrum of options?
- How do Docker, Kubernetes, and Terraform work together to make microservices cost-effective and deployable?
- How do you enable fast iterative development with live reload across a multi-container application?
- What are the right communication patterns—HTTP vs. RabbitMQ—between microservices and when should each be used?
Glossary
- Microservice Granularity
- The architectural choice of how finely to decompose application functionality into separate, independently deployable processes, ranging from a single monolithic process (zero granularity) to many small services each owning one bounded context (high granularity).
- Inter-Service Loose Coupling
- The degree to which microservices are designed to minimize direct dependencies on each other's internal implementations, using well-defined API boundaries, avoiding shared databases, and preferring indirect messaging where possible.
- Single Responsibility Adherence
- The extent to which each microservice is designed and maintained to address only one conceptual area of business domain responsibility, consistent with the single responsibility principle from software design and the bounded context concept from domain-driven design.
- Containerization Adoption (Docker)
- The extent to which every microservice is packaged as an immutable Docker image with a production and development Dockerfile, published to a private container registry, and deployed as containers consistently across local development, testing, and production environments.
- Infrastructure-as-Code Adoption (Terraform)
- The degree to which cloud infrastructure resources (container registries, Kubernetes clusters, networking, role assignments, storage accounts) are defined in version-controlled declarative Terraform HCL code and created by executing that code, rather than being provisioned manually through GUIs or ad-hoc CLI commands.
- Continuous Deployment Pipeline Maturity
- The sophistication, reliability, and completeness of automated pipelines that detect code changes (via GitHub Actions triggers), build and test service Docker images, publish them to a container registry, and deploy them to production Kubernetes clusters without manual intervention.
- Automated Test Coverage and Layering
- The breadth and depth of automated tests applied to the microservices application across all three tiers of the testing pyramid: unit tests testing isolated functions with mocks (Jest), integration tests exercising whole microservices against real dependencies (Jest + real database), and end-to-end tests exercising the full application through the browser UI (Playwright + Docker Compose).
- Local Development Environment Quality
- The degree to which the local development setup enables fast, low-friction iterative coding cycles with minimal rebuild latency, production environment parity, and easy project switching, achieved through Docker Compose multi-service orchestration, nodemon live reload, shared Docker volumes for code synchronization, and separate dev/prod Dockerfiles.
Related in the library
- Bootstrapping Microservices, Second Edition With Docker, Kubernetes, GitHub Actions, and Terraformshared: Systems
- Clean Architecture A Craftsmans Guide to Software Structure and Design (Robert C. Martin Series)shared: Systems
- Just Use Postgresshared: Systems
- Microservices Patternsshared: Systems
- Spring Microservices in Action, Second Editionshared: Systems
- The DevOps Handbook (2nd Edition)shared: Systems