Skip to content

Commit b9f15b3

Browse files
committed
ingress: Support for multple domains
1 parent 3d4e24b commit b9f15b3

9 files changed

Lines changed: 303 additions & 80 deletions

File tree

custom-domain/dstack-ingress/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ RUN set -e; \
2929
coreutils && \
3030
rm -rf /var/lib/apt/lists/* /var/log/* /var/cache/ldconfig/aux-cache /tmp/pinned-packages.txt
3131

32-
RUN mkdir -p /etc/letsencrypt /var/www/certbot /usr/share/nginx/html
32+
RUN mkdir -p \
33+
/etc/letsencrypt \
34+
/var/www/certbot \
35+
/usr/share/nginx/html \
36+
/etc/nginx/conf.d \
37+
/var/log/nginx
3338

3439
# Set up Python virtual environment and install certbot
3540
RUN set -e; \

custom-domain/dstack-ingress/README.md

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,26 @@ The dstack-ingress system provides a seamless way to set up custom domains for d
3333
3. **Certificate Management**:
3434

3535
- SSL certificates are automatically obtained during initial setup
36-
- A scheduled task runs twice daily to check for certificate renewal
36+
- A simple background daemon checks for certificate renewal every 12 hours
3737
- When certificates are renewed, Nginx is automatically reloaded to use the new certificates
38+
- Uses a simple sleep loop instead of cron for reliability and easier debugging in containers
3839

3940
4. **Evidence Generation**:
4041
- The system generates evidence files for verification purposes
4142
- These include the ACME account information and certificate data
4243
- Evidence files are accessible through a dedicated endpoint
4344

45+
## Features
46+
47+
### Multi-Domain Support (New!)
48+
49+
The dstack-ingress now supports multiple domains in a single container:
50+
51+
- **Single Domain Mode** (backward compatible): Use `DOMAIN` and `TARGET_ENDPOINT` environment variables
52+
- **Multi-Domain Mode**: Use `DOMAINS` environment variable with custom nginx configurations in `/etc/nginx/conf.d/`
53+
- Each domain gets its own SSL certificate
54+
- Flexible nginx configuration per domain
55+
4456
## Usage
4557

4658
### Prerequisites
@@ -87,15 +99,90 @@ volumes:
8799
cert-data: # Persistent volume for certificates
88100
```
89101
102+
### Multi-Domain Configuration
103+
104+
```yaml
105+
services:
106+
ingress:
107+
image: kvin/dstack-ingress:latest
108+
ports:
109+
- "443:443"
110+
environment:
111+
DNS_PROVIDER: cloudflare
112+
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
113+
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
114+
GATEWAY_DOMAIN: _.dstack-prod5.phala.network
115+
SET_CAA: true
116+
DOMAINS: |
117+
${APP_DOMAIN}
118+
${API_DOMAIN}
119+
120+
volumes:
121+
- /var/run/tappd.sock:/var/run/tappd.sock
122+
- letsencrypt:/etc/letsencrypt
123+
124+
configs:
125+
- source: app_conf
126+
target: /etc/nginx/conf.d/app.conf
127+
mode: 0444
128+
- source: api_conf
129+
target: /etc/nginx/conf.d/api.conf
130+
mode: 0444
131+
132+
restart: unless-stopped
133+
134+
app-main:
135+
image: nginx
136+
restart: unless-stopped
137+
138+
app-api:
139+
image: nginx
140+
restart: unless-stopped
141+
142+
volumes:
143+
letsencrypt:
144+
145+
configs:
146+
app_conf:
147+
content: |
148+
server {
149+
listen 443 ssl;
150+
server_name ${APP_DOMAIN};
151+
ssl_certificate /etc/letsencrypt/live/${APP_DOMAIN}/fullchain.pem;
152+
ssl_certificate_key /etc/letsencrypt/live/${APP_DOMAIN}/privkey.pem;
153+
location / {
154+
proxy_pass http://app-main:80;
155+
}
156+
}
157+
api_conf:
158+
content: |
159+
server {
160+
listen 443 ssl;
161+
server_name ${API_DOMAIN};
162+
ssl_certificate /etc/letsencrypt/live/${API_DOMAIN}/fullchain.pem;
163+
ssl_certificate_key /etc/letsencrypt/live/${API_DOMAIN}/privkey.pem;
164+
location / {
165+
proxy_pass http://app-api:80;
166+
}
167+
}
168+
```
169+
90170
**Core Environment Variables:**
91171
92172
- `DNS_PROVIDER`: DNS provider to use (cloudflare, linode)
93-
- `DOMAIN`: Your custom domain
173+
- `DOMAIN`: Your custom domain (for single domain mode)
174+
- `DOMAINS`: Multiple domains, one per line (supports environment variable substitution like `${APP_DOMAIN}`)
94175
- `GATEWAY_DOMAIN`: The dstack gateway domain (e.g. `_.dstack-prod5.phala.network` for Phala Cloud)
95176
- `CERTBOT_EMAIL`: Your email address used in Let's Encrypt certificate requests
96-
- `TARGET_ENDPOINT`: The plain HTTP endpoint of your dstack application
177+
- `TARGET_ENDPOINT`: The plain HTTP endpoint of your dstack application (for single domain mode)
97178
- `SET_CAA`: Set to `true` to enable CAA record setup
98179

180+
**Backward Compatibility:**
181+
182+
- If both `DOMAIN` and `TARGET_ENDPOINT` are set, the system operates in single-domain mode with auto-generated nginx config
183+
- If `DOMAINS` is set, the system operates in multi-domain mode and expects custom nginx configs in `/etc/nginx/conf.d/`
184+
- You can use both modes simultaneously
185+
99186
For provider-specific configuration details, see [DNS Provider Configuration](DNS_PROVIDERS.md).
100187

101188
#### Option 2: Build Your Own Image
@@ -126,7 +213,6 @@ docker push yourusername/dstack-ingress:tag
126213

127214
4. Update the docker-compose.yaml file with your image name and deploy
128215

129-
130216
#### gRPC Support
131217

132218
If your dstack application uses gRPC, you can set `TARGET_ENDPOINT` to `grpc://app:50051`.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
services:
2+
ingress:
3+
image: kvin/dstack-ingress@sha256:b61d50360c7a4e5ab7d22f5ce87677714f3f64a65db34ee5eebcc54683950c89
4+
ports:
5+
- "443:443"
6+
environment:
7+
DNS_PROVIDER: cloudflare
8+
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
9+
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
10+
GATEWAY_DOMAIN: _.dstack-prod5.phala.network
11+
SET_CAA: true
12+
DOMAINS: |
13+
${APP_DOMAIN}
14+
${API_DOMAIN}
15+
16+
volumes:
17+
- /var/run/tappd.sock:/var/run/tappd.sock
18+
- letsencrypt:/etc/letsencrypt
19+
20+
configs:
21+
- source: app_conf
22+
target: /etc/nginx/conf.d/app.conf
23+
mode: 0444
24+
- source: api_conf
25+
target: /etc/nginx/conf.d/api.conf
26+
mode: 0444
27+
28+
restart: unless-stopped
29+
30+
app-main:
31+
image: nginx
32+
restart: unless-stopped
33+
34+
app-api:
35+
image: nginx
36+
restart: unless-stopped
37+
38+
volumes:
39+
letsencrypt:
40+
41+
configs:
42+
app_conf:
43+
content: |
44+
server {
45+
listen 443 ssl;
46+
server_name ${APP_DOMAIN};
47+
48+
ssl_certificate /etc/letsencrypt/live/${APP_DOMAIN}/fullchain.pem;
49+
ssl_certificate_key /etc/letsencrypt/live/${APP_DOMAIN}/privkey.pem;
50+
51+
location / {
52+
proxy_pass http://app-main:80;
53+
}
54+
}
55+
api_conf:
56+
content: |
57+
server {
58+
listen 443 ssl;
59+
server_name ${API_DOMAIN};
60+
61+
ssl_certificate /etc/letsencrypt/live/${API_DOMAIN}/fullchain.pem;
62+
ssl_certificate_key /etc/letsencrypt/live/${API_DOMAIN}/privkey.pem;
63+
64+
location / {
65+
proxy_pass http://app-api:80;
66+
}
67+
}
File renamed without changes.

custom-domain/dstack-ingress/scripts/entrypoint.sh

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ if [[ -e /var/run/dstack.sock ]]; then
88
else
99
TXT_PREFIX=${TXT_PREFIX:-"_tapp-address"}
1010
fi
11+
PROXY_CMD="proxy"
12+
if [[ "${TARGET_ENDPOINT}" == grpc://* ]]; then
13+
PROXY_CMD="grpc"
14+
fi
1115

1216
echo "Setting up certbot environment"
1317

@@ -24,10 +28,7 @@ setup_certbot_env() {
2428
fi
2529
}
2630

27-
PROXY_CMD="proxy"
28-
if [[ "${TARGET_ENDPOINT}" == grpc://* ]]; then
29-
PROXY_CMD="grpc"
30-
fi
31+
setup_py_env
3132

3233
setup_nginx_conf() {
3334
cat <<EOF >/etc/nginx/conf.d/default.conf
@@ -84,29 +85,26 @@ server {
8485
}
8586
}
8687
EOF
87-
mkdir -p /var/log/nginx
8888
}
8989

90-
9190
set_alias_record() {
92-
# Use the unified DNS manager to set the alias record
93-
source /opt/app-venv/bin/activate
94-
echo "Setting alias record for $DOMAIN"
95-
dns_manager.py set_alias \
96-
--domain "$DOMAIN" \
91+
local domain="$1"
92+
echo "Setting alias record for $domain"
93+
dnsman.py set_alias \
94+
--domain "$domain" \
9795
--content "$GATEWAY_DOMAIN"
9896

9997
if [ $? -ne 0 ]; then
100-
echo "Error: Failed to set alias record for $DOMAIN"
98+
echo "Error: Failed to set alias record for $domain"
10199
exit 1
102100
fi
103-
echo "Alias record set for $DOMAIN"
101+
echo "Alias record set for $domain"
104102
}
105103

106104
set_txt_record() {
105+
local domain="$1"
107106
local APP_ID
108107

109-
# Generate a unique app ID if not provided
110108
if [[ -e /var/run/dstack.sock ]]; then
111109
DSTACK_APP_ID=$(curl -s --unix-socket /var/run/dstack.sock http://localhost/Info | jq -j .app_id)
112110
export DSTACK_APP_ID
@@ -116,48 +114,72 @@ set_txt_record() {
116114
fi
117115
APP_ID=${APP_ID:-"$DSTACK_APP_ID"}
118116

119-
# Use the unified DNS manager to set the TXT record
120-
source /opt/app-venv/bin/activate
121-
dns_manager.py set_txt \
122-
--domain "${TXT_PREFIX}.${DOMAIN}" \
117+
dnsman.py set_txt \
118+
--domain "${TXT_PREFIX}.${domain}" \
123119
--content "$APP_ID:$PORT"
124120

125121
if [ $? -ne 0 ]; then
126-
echo "Error: Failed to set TXT record for $DOMAIN"
122+
echo "Error: Failed to set TXT record for $domain"
127123
exit 1
128124
fi
129125
}
130126

131127
set_caa_record() {
128+
local domain="$1"
132129
if [ "$SET_CAA" != "true" ]; then
133130
echo "Skipping CAA record setup"
134131
return
135132
fi
136-
# Add CAA record for the domain
137133
local ACCOUNT_URI
138-
ACCOUNT_URI=$(jq -j '.uri' /evidences/acme-account.json)
139-
echo "Adding CAA record for $DOMAIN, accounturi=$ACCOUNT_URI"
140-
source /opt/app-venv/bin/activate
141-
dns_manager.py set_caa \
142-
--domain "$DOMAIN" \
134+
ACCOUNT_URI=$(jq -j '.uri' /etc/letsencrypt/accounts/acme-v02.api.letsencrypt.org/directory/*/regr.json)
135+
echo "Adding CAA record for $domain, accounturi=$ACCOUNT_URI"
136+
dnsman.py set_caa \
137+
--domain "$domain" \
143138
--caa-tag "issue" \
144139
--caa-value "letsencrypt.org;validationmethods=dns-01;accounturi=$ACCOUNT_URI"
145140

146141
if [ $? -ne 0 ]; then
147-
echo "Warning: Failed to set CAA record for $DOMAIN"
142+
echo "Warning: Failed to set CAA record for $domain"
148143
echo "This is not critical - certificates can still be issued without CAA records"
149144
echo "Consider disabling CAA records by setting SET_CAA=false if this continues to fail"
150145
# Don't exit - CAA records are optional for certificate generation
151146
fi
152147
}
153148

149+
process_domain() {
150+
local domain="$1"
151+
echo "Processing domain: $domain"
152+
153+
set_alias_record "$domain"
154+
set_txt_record "$domain"
155+
renew-certificate.sh "$domain" || echo "First certificate renewal failed for $domain, will retry after set CAA record"
156+
set_caa_record "$domain"
157+
renew-certificate.sh "$domain"
158+
}
159+
154160
bootstrap() {
155-
echo "Bootstrap: Setting up $DOMAIN"
156-
source /opt/app-venv/bin/activate
157-
renew-certificate.sh -n
158-
set_alias_record
159-
set_txt_record
160-
set_caa_record
161+
echo "Bootstrap: Setting up domains"
162+
163+
local all_domains
164+
all_domains=$(get-all-domains.sh)
165+
166+
if [ -z "$all_domains" ]; then
167+
echo "Error: No domains found. Set either DOMAIN or DOMAINS environment variable"
168+
exit 1
169+
fi
170+
171+
echo "Found domains:"
172+
echo "$all_domains"
173+
174+
while IFS= read -r domain; do
175+
[[ -n "$domain" ]] || continue
176+
process_domain "$domain"
177+
done <<<"$all_domains"
178+
179+
# Generate evidences after all certificates are obtained
180+
echo "Generating evidence files for all domains..."
181+
generate-evidences.sh
182+
161183
touch /.bootstrapped
162184
}
163185

@@ -175,6 +197,10 @@ fi
175197

176198
renewal-daemon.sh &
177199

178-
setup_nginx_conf
200+
mkdir -p /var/log/nginx
201+
202+
if [ -n "$DOMAIN" ] && [ -n "$TARGET_ENDPOINT" ]; then
203+
setup_nginx_conf
204+
fi
179205

180206
exec "$@"

0 commit comments

Comments
 (0)