DEV Community

lotyp
lotyp

Posted on • Edited on

Simplifying Local Development with Docker, mkcert, DNSMasq and Traefik.

Hello dev.to community!

I've created a project called 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.

πŸ—‚οΈ Table of Contents

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.

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.

The Solution: Docker Shared Services 🐳

The 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 generates locally-trusted development certificates, enabling SSL support for your local domains.

  3. DNSMasq:
    Dnsmasq 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.

Requirements 🚩

Quick Start Guide (macOS) πŸš€

  1. Install Homebrew (if not installed):
    If Homebrew is not already installed, run the following command:

    /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:

    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.

    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:

    mkdir -p ~/projects/infra && cd ~/projects/infra
    
  5. Clone this repository:

    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.

    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.

    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.

  8. Run this project:
    Start the Docker services defined in the repository.

    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.

    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.

    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:

    scutil --dns
    

    Example output:

    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.

    ping router.docker -c 3
    ping any-domain.docker -c 3
    
  12. Access Traefik dashboard:
    Open https://router.docker.

    You should see the Traefik Dashboard:

    Traefik dashboard

β†’ 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 will be visible to Traefik. The local DNS, served by DNSMasq, will be available on *.docker domains.

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 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:

    ---
    
    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:

    cd ~/projects/infra/docker-shared-services
    

    Edit the .env file to add your custom domain:

    nano .env
    

    It should look something like this:

    TLS_DOMAINS="ui.docker router.docker *.my-project.docker"
    

    Generate SSL certificates:

    make cert-install restart
    

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:

    curl -s "https://laravel.build/example-app" | bash
    
  2. Open the docker-compose.yaml file of the example-app project and make adjustments:

    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.

    ./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

  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
Enter fullscreen mode Exit fullscreen mode

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

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.

This Dockerized Laravel starter template works seamlessly with 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.

Linux 🐧

Linux Users, you are also covered!

If you're using Ubuntu or another Linux distribution, I've included a Linux Quick Start Guide 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 provided in this article.

Conclusion ✨

In conclusion, the 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!

Top comments (0)