LocalCerts
Automatic HTTPS and local domain routing for Docker Compose services using Traefik, DPS and Step-CA.
Created by @Sajito
## Setup
1. Clone this repository and navigate into the directory.
2. Start the services:
```shell
docker compose up -d
```
3. Trust the Step-CA root certificate:
Download the `roots.pem` from [https://localhost:9000/roots.pem](https://localhost:9000/roots.pem) and trust it on your system.
This is necessary for the certificates to be trusted by your browser.
- Linux
```shell
sudo trust anchor --store roots.pem
```
- Windows
```powershell
Import-Certificate -FilePath "roots.pem" -CertStoreLocation "Cert:\LocalMachine\Root"
```
- macOS
```shell
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain roots.pem
```
## How to Use
1. Add a label to your docker compose service:
```yaml
labels:
serviceName: my-app
```
2. Your service will be accessible at `https://my-app.dev.local`.
## Troubleshooting
- If routes do not work you can open the traefik dashboard under [https://traefik.vm](https://traefik.vm) and check if the service and route exist.
- If your service is not reachable it could mean that traefik did not detect the correct port. You can set the port manually in your docker compose file:
```yaml
labels:
traefik.http.services.my-app.loadbalancer.server.port: 8080
```
> [!IMPORTANT]
> For each overwritten port you need to specify a custom `service-label` `traefik.http.services.[service-label]`. Otherwise the custom port will not be detected.
- If certificates are not renewed or have expired
```shell
docker compose up -d --force-recreate traefik
```
## Tipps
### Custom Domain Suffix
For example `dev.cool` 😎
Replace `.dev.local` with your custom domain suffix in the `config/traefik/traefik.yml` file:
```diff
docker:
defaultRule: |
- Host(`{{ trim (index .Labels "serviceName") }}.dev.local`) {{range $i, $domain := splitList "," (index .Labels "serviceDomains")}}{{if ne $domain ""}}|| Host(`{{$domain}}`){{end}}{{end}}
+ Host(`{{ trim (index .Labels "serviceName") }}.dev.cool`) {{range $i, $domain := splitList "," (index .Labels "serviceDomains")}}{{if ne $domain ""}}|| Host(`{{$domain}}`){{end}}{{end}}
constraints: LabelRegex(`serviceName`, `.+`) && !Label(`com.docker.compose.oneoff`, `True`)
```
Replace `.dev.local` with your custom domain suffix in the `config/dns/config.sample.json` file:
```diff
{
"id": 2,
- "hostname": ".dev.local",
+ "hostname": ".dev.cool",
"ip": "",
```
Restart the docker services to apply the changes:
```shell
docker compose up -d --force-recreate
```
### Custom Domain Per Service
If you want a single service to have a custom domain you can use the `serviceDomains` label. This will override the default domain suffix:
```yaml
labels:
serviceName: my-app
serviceDomains: my-app.very.cool
```
The service would now be reachable at `https://my-app.very.cool` and `https://my-app.dev.local`.
### Certificate Lifetime
To ensure Traefik has enough time to renew certificates, increase their duration:
```shell
docker compose exec step step ca provisioner update acme \
--x509-min-dur=20m \
--x509-max-dur=8760h \
--x509-default-dur=2160h
```
### Create a shell script to start/stop general services
I often use tools like dbgate to interact with databases. For such utilities, I maintain a services/compose.yml file in this directory, where each service is defined with a custom profile. This setup allows me to start and stop individual services as needed.
If you use the preconfigured services, you can add the following snippet to you `.bashrc`/`.zshrc` to easily start, stop, and manage the services. Make sure to update the `PROJECT_DIR` variable to point to the correct directory.
```shell
dev () {
PROJECT_DIR="$HOME/Projects/dev/services"
case "$1" in
(start) shift
docker compose -f "$PROJECT_DIR/compose.yml" --profile "$@" up -d ;;
(restart) shift
docker compose -f "$PROJECT_DIR/compose.yml" --profile "$@" restart ;;
(stop) shift
docker compose -f "$PROJECT_DIR/compose.yml" --profile "$@" down --remove-orphans ;;
(logs) shift
docker compose -f "$PROJECT_DIR/compose.yml" --profile "$@" logs -f ;;
(*) echo "Usage: dev {start|restart|stop|logs} [services...]" ;;
esac
}
```
## Fix `systemd-resolved` for `.dev.local`
If `getent hosts yourdomain.dev.local` returns nothing while
`dig yourdomain.dev.local @172.157.5.249` works, `systemd-resolved` is routing `.local` to mDNS instead of DNS.
```bash
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/localcerts.conf >/dev/null <