Domain + HTTPS setup for Docker services (Nginx + Certbot)
Project description
🚀 DomainUp 0.1.2 just released – turn your Docker services into HTTPS domains in 1 minute.
DomainUp
Config-driven reverse proxy for Docker apps that ships production-ready HTTPS Nginx automation from a single YAML.
Table of Contents
- Why DomainUp
- Features
- Quickstart
- Requirements
- Installation
- Example: domainup.yaml → rendered Nginx
- Configuration
- Configuration Options
- Usage Examples
- Files Generated
- DomainUp vs Hetzner DNS – Complementary Tools
- How It Works
- Roadmap
- Contributing
- Security
- Star & Support
- License
Why DomainUp
Every self-hosted DevOps sprint looked the same: log into a Hetzner box, wire up a reverse proxy, massage Nginx blocks, request Let’s Encrypt certs, and cross fingers that Docker endpoints still spoke HTTPS.
DomainUp turns that loop into a repeatable automation by reading one YAML map and emitting trusted templates for proxies and DNS orchestration.
- Save late-night pager time with https-ready domains launched in under a minute.
- Keep self-hosted Docker fleets tidy with versioned yaml and repeatable proxy steps.
- Give devops teammates confidence that Let’s Encrypt renewals, security headers, and monitoring hooks run on schedule.
Features
- ⚡ Single YAML manifest defines every edge domain across Docker upstreams.
- 🔐 Built-in HTTPS termination with Let’s Encrypt webroot renewals and optional HSTS.
- 🔁 Smart automation for DevOps teams with templated Nginx and Traefik renderers.
- 🧰 Self-hosted friendly: headers, websockets, basic auth, rate limits, sticky cookies, and path routing.
- 📦 Works with multiple Docker services, health checks, and per-domain overrides.
- FinOps-friendly: YAML-as-source-of-truth, predictable HTTPS automation, reproducible across environments.
Quickstart
⭐ If this project saves you time, please consider starring the repo — it helps more self-hosters find it.
Get up and running on a fresh self-hosted node with the CLI in minutes.
Install the CLI (see also Installation):
pipx install domainup # or: pip install domainup
If you haven’t installed the CLI yet, see Installation)
Run this minimal flow to validate the yaml stack, render Nginx, bring up the reverse proxy, and request Let’s Encrypt certs for https endpoints:
domainup init --email contact@cirrondly.com # creates domainup.yaml skeleton
domainup plan # validate + print plan
domainup render # generate Nginx configs from yaml
domainup up # start Nginx gateway
domainup cert # obtain certs (webroot)
domainup reload # reload Nginx
domainup deploy # render -> up -> cert -> reload
domainup check --domain api.example.com # quick diagnostics
This automation works equally well on local Docker Compose or remote hosts.
Local testing tips:
- If ports 80/443 are busy, either:
- Override at runtime:
domainup up --http-port 8080 --https-port 8443(no file edits), or - Make it permanent: set
runtime.http_port/runtime.https_portindomainup.yaml, thendomainup up.
- Override at runtime:
- For HTTPS locally, use
mkcertand place certs under./letsencrypt/live/<host>/.
Example: domainup.yaml → rendered Nginx
Here’s the central YAML manifest that DomainUp consumes to manage multiple domains:
version: 1
email: contact@cirrondly.com
engine: nginx # nginx | traefik (poc)
cert:
method: webroot # webroot | dns01 (todo)
webroot_dir: ./www/certbot
staging: false # true to test with LE staging
network: proxy_net
runtime:
http_port: 80
https_port: 443
domains:
- host: api.example.com
upstreams:
- name: app1
target: app:8000
weight: 1
paths:
- path: /
upstream: app1
websocket: true
strip_prefix: false
headers:
hsts: true
extra:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
security:
basic_auth:
enabled: false
users: []
allow_ips: []
rate_limit:
enabled: false
requests_per_minute: 600
tls: { enabled: true }
gzip: true
cors_passthrough: false
- host: console.example.com
upstreams:
- name: console
target: console:3000
paths:
- path: "/"
upstream: console
security:
basic_auth:
enabled: true
users: ["admin:{SHA}..."]
tls: { enabled: true }
- host: data.example.com
upstreams:
- name: otel
target: otel:4318
paths:
- path: "~* ^/(v1/|otlp/v1/)(traces|logs|metrics)"
upstream: otel
body_size: 20m
tls: { enabled: true }
The renderer outputs an Nginx server block with upstreams and https wiring:
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
include snippets/headers.conf;
location / {
proxy_pass http://app1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Requirements
- Linux VM or local machine with Docker and Docker Compose v2
- Ports 80 and 443 available (for HTTP-01 / HTTPS)
- A writable directory for Let’s Encrypt assets (e.g.
./letsencrypt/)
Installation
From PyPI
# user-wide (pipx) – recommandé pour les CLIs
pipx install domainup
# ou via pip (virtualenv/venv)
pip install domainup
With Docker (no Python needed)
docker run --rm -it \
-v $PWD:/work \
-v $PWD/letsencrypt:/work/letsencrypt \
-p 80:80 -p 443:443 \
ghcr.io/cirrondly/domainup:latest domainup --help
Configuration
DomainUp reads the domainup.yaml file for these options:
version: 1
email: contact@cirrondly.com
engine: nginx # Nginx | traefik (poc)
cert:
method: webroot # webroot | dns01 (todo)
webroot_dir: ./www/certbot
staging: false # true to test with LE staging
network: proxy_net
runtime:
http_port: 80
https_port: 443
domains:
- host: api.example.com
upstreams:
- name: app1
target: app:8000
weight: 1
paths:
- path: /
upstream: app1
websocket: true
strip_prefix: false
headers:
hsts: true
extra:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
security:
basic_auth:
enabled: false
users: []
allow_ips: []
rate_limit:
enabled: false
requests_per_minute: 600
tls: { enabled: true }
gzip: true
cors_passthrough: false
- host: console.example.com
upstreams:
- name: console
target: console:3000
paths:
- path: "/"
upstream: console
security:
basic_auth:
enabled: true
users: ["admin:{SHA}..."]
tls: { enabled: true }
- host: data.example.com
upstreams:
- name: otel
target: otel:4318
paths:
- path: "~* ^/(v1/|otlp/v1/)(traces|logs|metrics)"
upstream: otel
body_size: 20m
tls: { enabled: true }
Configuration Options
| Option | Description | Default |
|---|---|---|
engine |
Selects the renderer (Nginx or traefik) for the edge gateway. |
Nginx |
cert.method |
Chooses certificate strategy (webroot today, dns01 planned) via Let’s Encrypt. |
webroot |
network |
Docker network name used to wire containers behind the gateway container. | proxy_net |
runtime.http_port / runtime.https_port |
Host ports exposed for http/https listeners. | 80 / 443 |
domains[].paths[].websocket |
Enables websocket upgrade support per route. | false |
domains[].security.basic_auth |
Configures htpasswd or inline users for protected paths. | false |
domains[].security.rate_limit |
Simple rate limiting (requests per minute) for DevOps safeguards. | 600 |
Usage Examples
Generate configs for the running proxy stack:
domainup render && domainup up
Obtain certificates through Let’s Encrypt and reload the edge:
domainup cert && domainup reload
Check DNS and TLS quickly during automation runs:
domainup check --domain api.example.com
Auto-discovery mode (zero-config)
domainup up can scan running Docker containers, list published ports, and guide you to map each service to a domain without touching YAML.
domainup up # discover → ask domains → write domainup.yaml → render+up+cert+reload
# Or run discovery alone:
domainup discover
# Typical guided flow
Found 4 containers with published ports:
[1] back_web_1 → 8000/tcp → 0.0.0.0:8000
[2] otel → 4318/tcp → 0.0.0.0:4318
[3] grafana → 3000/tcp → 0.0.0.0:3000
[4] stripe_worker → 8080/tcp → 0.0.0.0:8080
Choose domain for back_web_1 (suggest: back_web_1.example.com): monitoring.cirrondly.com
Enable websockets? [y/N]: y
Choose domain for otel (suggest: otel.example.com): otlp.cirrondly.com
Large body (20m) for OTLP? [Y/n]: y
Choose domain for grafana (suggest: grafana.example.com): grafana.cirrondly.com
Protect with Basic Auth? [y/N]: n
Choose domain for stripe_worker (suggest: stripe_worker.example.com): webhooks.cirrondly.com
This will:
- Detect containers with published TCP ports.
- Let you pick a FQDN per service (with smart defaults).
- Write/update domainup.yaml (idempotent).
- Optionally start Nginx, issue certs, and reload.
Print DNS records for your provider (Hetzner, Cloudflare, Vercel) before you flip traffic:
domainup dns --ipv4 203.0.113.10 --ipv6 2001:db8::10
Files Generated
Nginx engine
nginx/nginx.confnginx/conf.d/00-redirect.conf(http→https + ACME webroot for all TLS hosts)nginx/conf.d/<host>.confper domainruntime/docker-compose.nginx.ymlto run the proxy
Traefik engine
traefik/traefik.yml(static)traefik/dynamic/<host>.ymlper domain with middlewarestraefik/htpasswd/<host>.htpasswdwhen basic auth enabledruntime/docker-compose.traefik.ymlto run the proxy
DomainUp vs Hetzner DNS – Complementary Tools
Developers sometimes ask: “If Hetzner DNS already exists, why would I need DomainUp?” Good question — they serve two different layers of the stack.
| Purpose | Hetzner DNS | DomainUp |
|---|---|---|
| Manage DNS zones & records | ✅ Yes – creates A/AAAA/CNAME, etc. | ✅ Yes (via provider APIs, e.g. Hetzner, Cloudflare, Vercel) |
| Configure reverse proxy (Nginx / Traefik) | ❌ | ✅ Generates and reloads configs automatically |
| Obtain & renew Let's Encrypt certificates | ❌ | ✅ Full automation (HTTP-01 webroot today, DNS-01 soon) |
| Deploy and reload Dockerized edge | ❌ | ✅ domainup up, domainup reload, domainup deploy |
| Handle websockets, headers, auth, rate-limit, gzip | ❌ | ✅ Config-driven per domain |
| Provide a single CLI to set up new domains | ❌ | ✅ One-command automation |
How they fit together
• Hetzner DNS is the authoritative DNS service that tells browsers “where to go”. It maps *.example.com → 91.98.141.137 (your server).
• DomainUp runs on that server and makes sure that, once traffic arrives, it’s routed to the right container, secured with HTTPS, and kept alive.
You can (and should) use both:
- Keep Hetzner DNS as your DNS provider (fast, reliable, free API).
- Use DomainUp to automate everything after DNS — proxy, certs, reloads.
- Or let DomainUp call Hetzner’s API directly to create/update A/AAAA records automatically:
domainup dns --provider hetzner --token $HETZNER_DNS_TOKEN \
--record monitoring A 91.98.141.137 \
--record monitoring AAAA 2a01:4f8:1c1c:5d0e::1
Override ports at runtime (no file edits)
If your machine already uses 80/443, you can override host ports just for this run:
domainup up --http-port 8080 --https-port 8443
You can still make it permanent by editing domainup.yaml under runtime: and re-running domainup up.
Troubleshooting
Ports 80/443 already in use
Symptoms:
Failed: ports 80/443 already in use on host.
Fix it in one of these ways:
- Quick (one-off):
domainup up --http-port 8080 --https-port 8443
- Permanent (edit config): in
domainup.yamlset:
runtime:
http_port: 8080
https_port: 8443
Then run:
domainup up
Or free the default ports by stopping whatever binds to 80/443 and try again.
Docker daemon is not running
Symptoms:
Cannot connect to the Docker daemon ... Is the docker daemon running?
Start your Docker engine on macOS, then retry:
open -a Docker # Docker Desktop
# or
colima start # Colima
# or
open -a OrbStack # OrbStack
The CLI detects this scenario and prints a helpful hint if Docker isn’t up.
nginx: host not found in upstream
Symptoms in logs:
nginx: [emerg] host not found in upstream "back_web_1:8000" in /etc/nginx/conf.d/<host>.conf:2
What it means:
- Nginx tried to resolve the upstream host at startup and couldn’t find it via Docker DNS.
- Common causes: the backend container isn’t on the same Docker network as the proxy, or the target uses a container instance name instead of the Compose service name.
How to fix:
- Ensure the backend service joins the same network as DomainUp (default
proxy_net). In your backend compose file:
services:
app:
image: your/image
networks: [proxy_net]
networks:
proxy_net:
external: true
- Use the Compose service name, not a container instance name. For a service named
applistening on 8000:
upstreams:
- name: app1
target: app:8000
- If the backend is running on the host (not in Docker), you can use
host.docker.internal:PORTon macOS.
Verify connectivity:
docker network inspect proxy_net | jq '.[0].Containers | keys'
You should see both nginx_proxy and your backend service listed on proxy_net.
🔧 Typical setup
- Create your DNS zone on Hetzner DNS or keep it on Vercel — both work.
- Add A/AAAA records for each subdomain pointing to your Hetzner server.
- On the server, run this chained deployment:
domainup render && domainup up && domainup cert && domainup reload
- Optionally:
domainup dns hetzner --token …to automate record creation next time.
How It Works
- CLI parses the YAML spec and builds an in-memory model of domains, upstreams, and security rules.
- Template engine renders Nginx or Traefik configs, staging Let’s Encrypt assets where needed.
- Docker Compose files spin up the proxy containers with mounted certificates.
- Scheduled automation handles renewals, reloads, and optional DNS provider updates for DevOps teams.
Roadmap
- DNS provider API integration: Vercel
- ACME DNS-01 support
Delivered from roadmap in this release:
- Hetzner DNS automation (A/AAAA upsert) via
domainup dns --provider hetzner --token ... - Cloudflare DNS automation (A/AAAA upsert) via
domainup dns --provider cloudflare --token ... - Optional htpasswd file generation for basic auth (render-time)
- Better CORS passthrough controls
- Traefik middlewares: BasicAuth + CORS + RateLimit + Sticky cookie
- Traefik advanced headers: HSTS + custom response headers (from
headers.hstsandheaders.extra)
Contributing
Set up the dev environment with editable dependencies and tests:
pip install -e .[dev]
pytest -q
Coding standards:
- Format: black, lint: ruff, types: mypy
- Tests: pytest; add unit tests for new behaviors
- PRs: include a brief description, motivation (what problem you solved), and tests
Security
- Don’t expose Basic Auth user/passwords in the repo; use htpasswd files or safe secret storage.
- HTTP-01 requires port 80. If you can’t open it, prefer DNS-01 (on roadmap).
- Review Nginx config before going to production; adjust rate limits and headers as needed for your threat model.
Star & Support
If DomainUp saves you time with proxy automation across Docker and Let’s Encrypt, ⭐ star the repo if it helped you! Share it with fellow self-hosted devops teams rolling out https services on yaml-first stacks.
License
MIT License. See LICENSE for details.
Created with ❤️ by Cirrondly — a tiny startup by José MARIN.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file domainup-0.1.3.tar.gz.
File metadata
- Download URL: domainup-0.1.3.tar.gz
- Upload date:
- Size: 39.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
129b3273a5515778b2bad28b26fc13bb90866ca3f313b52ae31e84dc5c864e80
|
|
| MD5 |
49651c192ff4fcd3794a1ec727fed452
|
|
| BLAKE2b-256 |
bd111a9205b96fb2579c17be005263bccb61ab87cb60fdb335437ee81963f32b
|
File details
Details for the file domainup-0.1.3-py3-none-any.whl.
File metadata
- Download URL: domainup-0.1.3-py3-none-any.whl
- Upload date:
- Size: 38.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1a78bbd6dcfee884a161d17a95a348b93a88c5fc74d308c457a509e6bad1bcc
|
|
| MD5 |
b81eca994dd2583912275a52dbfcad76
|
|
| BLAKE2b-256 |
67e8709caf544db94cfc05391a257dd554f07d0021491840476e49a97a224935
|