Simplifying Local Development with Docker, mkcert, DNSMasq and Traefik.
Tags: devops, tutorial, ssl, webdev
Published: 2024-06-10 22:10:55

Hello dev.to community!

I've created a project called [wayofdev/docker-shared-services](https://github.com/wayofdev/docker-shared-services) that my team and I use to streamline our local development.

It simplifies the setup for Dockerized projects on macOS and Linux, and I’m excited to share it with the community. Let's dive into how it can help you and your team.

<br>

## 🗂️ Table of Contents

- [Key Features](#key-features)
- [Problem](#problem)
- [The Solution: Docker Shared Services](#the-solution-docker-shared-services)
- [Requirements](#requirements)
- [Quick Start Guide (macOS)](#quick-start-guide-macos)
- [Connecting Your Projects to Shared Services](#connecting-your-projects-to-shared-services)
- [Example: Spin-up Laravel Sail Project](#example-spinup-laravel-sail-project)
- [Example: Want to See a Ready-Made Template?](#example-want-to-see-a-readymade-template)
- [Linux](#linux)
- [Conclusion](#conclusion)

<br>

## Key Features 🌟

By implementing this approach, you will have:

- **Automated Local DNS and SSL Setup**: No more manual edits to `/etc/hosts` or dealing with self-signed certificate warnings. You'll have an automated solution for DNS and SSL.
- **Consistent Development Environment**: All team members will have the same setup, reducing environment-related bugs and making onboarding new developers faster and easier.
- **Elimination of Port Conflicts**: Using Traefik, you can avoid port conflicts entirely, allowing multiple dockerized projects to run concurrently without issues.
- **User-Friendly Local URLs**: Access your projects via custom local domains like `project.docker` instead of `localhost:8000`, improving the overall development experience.
- **Simplified CORS and Cookie Management**: With SSL in place for local domains, configuring CORS and Cookies will mirror production settings, reducing debugging time.
- **Enhanced Testing Environment**: Test OAuth, Secure Cookies, and HTTPS APIs locally, ensuring they work exactly as they will in production.
- **Improved Service Discovery and Routing**: Traefik provides automatic service discovery and routing, making it easier to manage and access various services within your Docker network.
- **Ease of Integration with Existing Projects**: Quickly connect your existing Docker projects to the `docker-shared-services` setup, leveraging DNSMasq and Traefik for better service management.

<br>

## Problem 🤔

### → Common Challenges in Local Development

1. **Manual DNS Configuration**:
   Developers often need to update their `/etc/hosts` file to direct traffic for local domains like `yourproject.local` or `yourproject.domain.local` to `127.0.0.1`. This is tedious and requires admin access.

2. **Lack of SSL Support**:
   Local domains often lack SSL, making it hard to test secure connections and leading to issues with OAuth providers, CORS, and cookies.

3. **Port Conflicts**:
   Forwarding Docker service ports to the host machine can cause conflicts, especially when multiple services use common ports like 80 or 443.

4. **Cumbersome Hostnames**:
   Using hostnames like `localhost:8000` for local projects is not user-friendly and complicates development.

5. **Complex CORS and Cookie Configuration**:
   Without SSL and proper DNS setup, configuring CORS and Cookies becomes more challenging.

6. **Consistency Across Team Members**:
   When a team is working on the same project, maintaining consistency in the local development environment across different machines is challenging. Differences in setup can lead to issues that are hard to debug and resolve, slowing down the development process.

### → The Hard Way

Traditionally, setting up a local development environment with SSL involves a series of manual steps:

- Generating and trusting self-signed certificates in the system
- Editing the `/etc/hosts` file
- Setting up and configuring Dockerized projects with custom ports
- Solving CORS and SSL related problems between local services

These steps are time-consuming and prone to errors, leading to a frustrating development experience.

<br>

## The Solution: Docker Shared Services 🐳

The [wayofdev/docker-shared-services](https://github.com/wayofdev/docker-shared-services) simplifies local development by providing a Docker-powered environment that integrates Traefik, mkcert, and DNSMasq. This setup offers:

- Automated DNS and SSL support
- A consistent development environment across different machines
- Simplified service discovery and routing using Traefik
- Elimination of port conflicts on the host network
- The ability to test your Dockerized projects in an environment that closely mimics real-world scenarios, ensuring that your applications behave as expected before deployment

### → Key Components

1. **Traefik**:
   Traefik acts as a reverse proxy and load balancer, providing routing for your Docker services. It integrates with Docker via docker-compose labeling functionality to automatically discover and route traffic to your services.

2. **mkcert**:
   [mkcert](https://github.com/FiloSottile/mkcert) generates locally-trusted development certificates, enabling SSL support for your local domains.

3. **DNSMasq**:
   [Dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) provides lightweight DNS and DHCP services, allowing you to use custom local domains without editing the `/etc/hosts` file.

### → Going Further

You can go further and pack this project into an Ansible setup. This way, you can automate the provisioning of macOS or Linux hosts, ensuring that all necessary tools and configurations are in place. This not only saves time but also ensures consistency across different environments.

<br>

## Requirements 🚩

- **macOS** Monterey+ or **Linux**
    - Tested on Ubuntu 22.04, but should also work on Debian or Arch variants
- **Docker** 26.0 or newer
    - [How To Install and Use Docker on Ubuntu 22.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04)
    - [How To Install Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/)
- **Homebrew** (macOS only): Install via [brew.sh](https://brew.sh/)
- Installed [mkcert](https://github.com/FiloSottile/mkcert) binary in system
    - See full installation instructions in their official [README.md](https://github.com/FiloSottile/mkcert)
    - Quick installation on macOS: `brew install mkcert nss`

<br>

## Quick Start Guide (macOS) 🚀

1. **Install Homebrew** (if not installed):
    If [Homebrew](https://brew.sh) is not already installed, run the following command:

    ```bash
   /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    ```

2. **Install Docker** (if not installed):
    Set up Docker Desktop via Homebrew:

    ```bash
    brew install --cask docker

    ```

3. **Install `mkcert` and `nss`:**
    `mkcert` is a tool that creates locally-trusted development certificates, and `nss` provides support for mkcert certificates in Firefox.

    ```bash
    brew install mkcert nss
    ```

4. **Create shared project directory:**
    This repository should be run once per machine, so let's create a shared directory for this project:

    ```bash
    mkdir -p ~/projects/infra && cd ~/projects/infra
    ```

5. **Clone this repository:**
    
    ```bash
    git clone \
      git@github.com:wayofdev/docker-shared-services.git \
      ~/projects/infra/docker-shared-services && \
    cd ~/projects/infra/docker-shared-services
    ```

6. **Create `.env` file:**
    Generate a default `.env` file, which contains configuration settings for the project.

    ```bash
    make env
    ```

    Open this file and read the notes inside to make any necessary changes to fit your setup.

7. **Install root certificate** and generate default project certs:
    This step installs the root certificate into your system's trust store and generates default SSL certificates for your local domains, which are listed in the `.env` file, under the variable `TLS_DOMAINS`.

    ```bash
    make cert-install
    ```
 
    > **Note:**
    >
    > Currently, on macOS, you may need to enter your password several times to allow `mkcert` to install the root certificate.
    > **This is a one-time operation** and details can be found in this upstream GitHub [issue](https://github.com/FiloSottile/mkcert/issues/415).

8. **Run this project:**
    Start the Docker services defined in the repository.
 
    ```bash
    make up
    ```
9. **Check that all Docker services are running:**
    Ensure Docker is running and services are up by using the `make ps` and `make logs` commands.
 
    ```bash
    make ps
    make logs
    ```

10. **Add custom DNS resolver to your system:**
    This allows macOS to understand that `*.docker` domains should be resolved by a custom resolver via `127.0.0.1`, where our DNSMasq, which runs inside Docker, will handle all DNS requests.

    ```bash
    sudo sh -c 'echo "nameserver 127.0.0.1" > /etc/resolver/docker'
    sudo dscacheutil -flushcache
    sudo killall -HUP mDNSResponder
    ```

    You can check that DNS was added by running:

    ```bash
    scutil --dns
    ```

    Example output:

    ```bash
    resolver #8
      domain   : docker
      nameserver[0] : 127.0.0.1
      flags    : Request A records, Request AAAA records
      reach    : 0x00030002 (Reachable,Local Address,Directly Reachable Address)
    ```

    > **Note:**
    >
    > Instead of creating the `/etc/resolver/docker` file, you can add `127.0.0.1` to your macOS DNS Servers in your Ethernet or Wi-Fi settings.
    >
    > Go to System Preferences → Network → Wi-Fi → Details → DNS and add `127.0.0.1` as the first DNS server.
    >
    > This allows you to do it one time, and if you need to create a new local domain, for example `*.mac`, in the future, it will be automatically resolved without creating a separate `/etc/resolver/mac` file.

11. **Ping `router.docker` to check if DNS is working:**
    Ensure that the DNS setup is functioning correctly.

    ```bash
    ping router.docker -c 3
    ping any-domain.docker -c 3
    ```

12. **Access Traefik dashboard:**
    Open [https://router.docker](https://router.docker).

    You should see the Traefik Dashboard:

    ![Traefik dashboard](https://raw.githubusercontent.com/wayofdev/docker-shared-services/master/.github/assets/traefik.png?raw=true "Traefik dashboard example")

### → Outcome

At this point, you should have a working local development environment with DNS and SSL support ready to be used with your projects.

Services will be running under a shared Docker network called `network.ss`, and all projects or microservices that will share the same [Docker network](https://docs.docker.com/network/) will be visible to Traefik. The local DNS, served by DNSMasq, will be available on `*.docker` domains.

<br>

## Connecting Your Projects to Shared Services 🔌

To connect your projects to the shared services, configure your project's `docker-compose.yaml` file to connect to the shared network and Traefik.

This project comes with an example Portainer service, which also starts by default with the `make up` command. You can check the [`docker-compose.yaml`](https://github.com/wayofdev/docker-shared-services/blob/master/docker-compose.yaml) to see how Traefik labels and the shared network are used to spin up Portainer on the <https://ui.docker> host, which supports SSL by default.

### → Sample Configuration

Your project should use the shared Docker network `network.ss` and Traefik labels to expose services to the outside world.

1. **Change your project's `docker-compose.yaml` file:**
    ```diff
    ---
 
    services:
      web:
        image: wayofdev/nginx:k8s-alpine-latest
        restart: on-failure
    +   networks:
    +     - default
    +     - shared
        volumes:
          - ./app:/app:rw,cached
    +   labels:
    +     - traefik.enable=true
    +     - traefik.http.routers.api-my-project-secure.rule=Host(`api.my-project.docker`)
    +     - traefik.http.routers.api-my-project-secure.entrypoints=websecure
    +     - traefik.http.routers.api-my-project-secure.tls=true
    +     - traefik.http.services.api-my-project-secure.loadbalancer.server.port=8880
    +     - traefik.docker.network=network.ss
 
    networks:
    + shared:
    +   external: true
    +   name: network.ss
    + default:
    +   name: project.my-project
    ```

    In this configuration, we added the shared network and Traefik labels to the web service. These labels help Traefik route the traffic to the service based on the specified rules.

2. **Generate SSL certs for your project:**
    Go to the `docker-shared-services` directory:
 
    ```bash
    cd ~/projects/infra/docker-shared-services
    ```
 
    Edit the `.env` file to add your custom domain:
 
    ```bash
    nano .env
    ```
 
    It should look something like this:
 
    ```bash
    TLS_DOMAINS="ui.docker router.docker *.my-project.docker"
    ```
 
    Generate SSL certificates:
 
    ```bash
    make cert-install restart
    ```

<br>

## Example: Spin-up Laravel Sail Project 🚀

Let's walk through an example of setting up a Laravel project using Sail and integrating it with the `docker-shared-services`.

1. Create an example Laravel project based on Sail:
    ```bash
    curl -s "https://laravel.build/example-app" | bash
    ```

2. Open the `docker-compose.yaml` file of the `example-app` project and make adjustments:
    ```diff
    services:
      laravel.test:
        build:
          context: ./vendor/laravel/sail/runtimes/8.3
          dockerfile: Dockerfile
          args:
            WWWGROUP: '${WWWGROUP}'
        image: sail-8.3/app
    -   extra_hosts:
    -     - 'host.docker.internal:host-gateway'
        ports:
    -     - '${APP_PORT:-80}:80'
    -     - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        networks:
          - sail
    +     - shared
        depends_on:
          - ...
    +   labels:
    +     - traefik.enable=true
    +     - traefik.http.routers.test-laravel-app-secure.rule=Host(`api.laravel-app.docker`)
    +     - traefik.http.routers.test-laravel-app-secure.entrypoints=websecure
    +     - traefik.http.routers.test-laravel-app-secure.tls=true
    +     - traefik.http.services.test-laravel-app-secure.loadbalancer.server.port=80
    +     - traefik.docker.network=network.ss
 
      mailpit:
        image: 'axllent/mailpit:latest'
        networks:
          - sail
    +     - shared
        ports:
    -     - '${FORWARD_MAILPIT_PORT:-1025}:1025'
    -     - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
    +   labels:
    +     - traefik.enable=true
    +     - traefik.http.routers.mail-laravel-app-secure.rule=Host(`mail.laravel-app.docker`)
    +     - traefik.http.routers.mail-laravel-app-secure.entrypoints=websecure
    +     - traefik.http.routers.mail-laravel-app-secure.tls=true
    +     - traefik.http.services.mail-laravel-app-secure.loadbalancer.server.port=8025
    +     - traefik.docker.network=network.ss
 
    networks:
      sail:
        driver: bridge
    + shared:
    +   external: true
    +   name: network.ss
    ```

    These changes connect the Laravel app and Mailpit docker services to the shared network and expose them via Traefik.

3. **Run the Laravel project:**
    Navigate to the `example-app` directory and start the services using Sail.
 
    ```bash
    ./vendor/bin/sail up -d
    ```

4. **Check Traefik routers**:
    Open <https://router.docker/dashboard/#/http/routers> and check that there are two routers:
      * Host(api.laravel-app.docker)  → test-laravel-app-secure@docker
      * Host(mail.laravel-app.docker)` → mail-laravel-app-secure@docker

    ![Traefik Routers](https://raw.githubusercontent.com/wayofdev/docker-shared-services/master/.github/assets/traefik-routers.png?raw=true "Traefik Routers Example")

5. **Check the setup**:
    Ensure that your Laravel application and Mailpit services are running correctly by accessing their respective domains:

      * **Laravel app** — https://api.laravel-app.docker
      * **Mailpit** — https://mail.laravel-app.docker

At this point, your Laravel project is integrated with the `wayofdev/docker-shared-services`, utilizing DNS and SSL support for local development.

<br>

## Example: Want to See a Ready-Made Template? 👀

If you come from the PHP or Laravel world, or if you want to see how a complete project can be integrated with `docker-shared-services`, check out [wayofdev/laravel-starter-tpl](https://github.com/wayofdev/laravel-starter-tpl).

This Dockerized Laravel starter template works seamlessly with [wayofdev/docker-shared-services](https://github.com/wayofdev/docker-shared-services), providing a foundation with integrated DNS and SSL support, and can show you `the way` to implement patterns, stated in this article, in your projects.

<br>

## Linux 🐧

Linux Users, you are also covered!

If you're using Ubuntu or another Linux distribution, I've included a [Linux Quick Start Guide](https://github.com/wayofdev/docker-shared-services?tab=readme-ov-file#-quick-start-guide-linux) in the README.md of the `wayofdev/docker-shared-services` project.

This guide provides step-by-step instructions to help you set up the environment on a Linux system, ensuring you can have the same streamlined development experience as described in the [macOS Quick Start Guide](#-quick-start-guide-macos) provided in this article.

<br>

## Conclusion ✨

In conclusion, the [wayofdev/docker-shared-services](https://github.com/wayofdev/docker-shared-services) project offers a streamlined and automated solution for managing local development environments using Docker, mkcert, DNSMasq, and Traefik.

By integrating these tools into your workflow, you can ensure that your local environment closely mirrors production, leading to more reliable and predictable outcomes when deploying applications.

### → What do you think? Would you approach this differently?

I hope this guide has been helpful in demonstrating the potential benefits of using this approach for your local development needs. If you have any suggestions or a better way of achieving this setup, please feel free to share your thoughts.

Feel free to fork this repository and adjust it to your needs, or use it as a template.

Thank you for reading, and I hope you find this setup as beneficial as I have.

Happy coding!

<br>