Skip to content
This repository was archived by the owner on Jul 14, 2024. It is now read-only.

Commit a86ce1b

Browse files
authoredMar 14, 2023
Add cli.sh (#46)
1 parent c01f42a commit a86ce1b

13 files changed

+286
-45
lines changed
 

‎README.md

+101-7
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Nginx and Let’s Encrypt with Docker Compose in less than 3 minutes.
3636

3737
This example automatically obtains and renews [Let's Encrypt](https://letsencrypt.org/) free SSL/TLS certificates and sets up HTTPS in Nginx for multiple domain names using Docker Compose.
3838

39-
You can run Nginx with IPv4, IPv6, HTTP/1.1, and HTTP/2 support and set up HTTPS with Let's Encrypt TLS certificates for your domain names and get an A+ rating in [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/) using Docker Compose and _letsencrypt-docker-compose_ interactive CLI tool.
39+
You can run Nginx and set up HTTPS (`https://`) and WebSocket Secure (`wss://`) with Let's Encrypt TLS certificates for your domain names and get an A+ rating in [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/) using Docker Compose and _letsencrypt-docker-compose_ interactive CLI tool.
40+
Nginx is configured to support IPv4, IPv6, HTTP/1.1, HTTP/2, and optionally, WebSocket.
4041

4142
Let's Encrypt is a certificate authority that provides free X.509 certificates for TLS encryption.
4243
The certificates are valid for 90 days and can be renewed. Both initial creation and renewal can be automated using [Certbot](https://certbot.eff.org/).
@@ -116,6 +117,8 @@ cp -R ./examples/html/ ./html/a.evgeniy-khyst.com
116117

117118
The [`docker-compose.yml`](docker-compose.yml) contains the `example-backend` service.
118119
It's a simple Node.js web app listening on port 8080.
120+
It has `/hello?name={name}` REST endpoint and WebSocket echo server sending back the request sent by the client.
121+
119122
Replace it with your backend service or remove it.
120123

121124
```yaml
@@ -155,6 +158,12 @@ networks:
155158

156159
Run the CLI tool and follow the instructions to perform an initial setup.
157160

161+
```bash
162+
./cli.sh config
163+
```
164+
165+
or
166+
158167
```bash
159168
docker compose run --rm cli
160169
```
@@ -166,14 +175,26 @@ We will switch to a Let's Encrypt production environment after verifying that HT
166175

167176
### <a id="2-5"></a>Step 4 - Start the services
168177

169-
On the first run, build the services.
178+
If you've made any changes to the Docker images, rebuild the services.
179+
180+
```bash
181+
./cli.sh build
182+
```
183+
184+
or
170185

171186
```bash
172187
docker compose build
173188
```
174189

175190
Start the services.
176191

192+
```bash
193+
./cli.sh up
194+
```
195+
196+
or
197+
177198
```bash
178199
docker compose up -d
179200
```
@@ -193,7 +214,7 @@ Reloading Nginx configuration
193214

194215
### <a id="2-6"></a>Step 5 - Verify that HTTPS works with the test certificates
195216

196-
For each domain, check `https://${domain}` and `https://www.${domain}` if you configured the `www` subdomain.
217+
For each domain, check `https://${domain}` and `https://www.${domain}` if you've configured the `www` subdomain.
197218
Certificates issued by `(STAGING) Let's Encrypt` are considered not secure by browsers and cURL.
198219

199220
```bash
@@ -203,10 +224,22 @@ curl --insecure https://b.evgeniy-khyst.com/hello?name=Eugene
203224
curl --insecure https://www.b.evgeniy-khyst.com/hello?name=Eugene
204225
```
205226

227+
If you've set up WebSocket, check it using the [wscat](https://github.com/websockets/wscat) tool.
228+
229+
```bash
230+
wscat --no-check --connect wss://b.evgeniy-khyst.com/echo
231+
```
232+
206233
### <a id="2-7"></a>Step 6 - Switch to a Let's Encrypt production environment
207234

208235
Run the CLI tool, choose `Switch to a Let's Encrypt production environment` and follow the instructions.
209236

237+
```bash
238+
./cli.sh config
239+
```
240+
241+
or
242+
210243
```bash
211244
docker compose run --rm cli
212245
```
@@ -215,7 +248,7 @@ docker compose run --rm cli
215248

216249
### <a id="2-8"></a>Step 7 - Verify that HTTPS works with the production certificates
217250

218-
For each domain, check `https://${domain}` and `https://www.${domain}` if you configured the `www` subdomain.
251+
For each domain, check `https://${domain}` and `https://www.${domain}` if you've configured the `www` subdomain.
219252
Certificates issued by `Let's Encrypt` are considered secure by browsers and cURL.
220253

221254
```bash
@@ -225,6 +258,12 @@ curl https://b.evgeniy-khyst.com/hello?name=Eugene
225258
curl https://www.b.evgeniy-khyst.com/hello?name=Eugene
226259
```
227260

261+
If you've set up WebSocket, check it using the [wscat](https://github.com/websockets/wscat) tool.
262+
263+
```bash
264+
wscat --connect wss://b.evgeniy-khyst.com/echo
265+
```
266+
228267
Optionally check your domains with [SSL Labs SSL Server Test](https://www.ssllabs.com/ssltest/) and review the SSL Reports.
229268

230269
The `cron` service will automatically renew the Let's Encrypt production certificates when the time comes.
@@ -254,20 +293,32 @@ Repeat the actions described in [the subsection of the same name in the "Initial
254293

255294
Run the CLI tool, choose `Add new domains` and follow the instructions.
256295

296+
```bash
297+
./cli.sh config
298+
```
299+
300+
or
301+
257302
```bash
258303
docker compose run --rm cli
259304
```
260305

261306
### <a id="3-4"></a>Step 4 - Verify that HTTPS works
262307

263-
For each new domain, check `https://${domain}` and `https://www.${domain}` if you configured the `www` subdomain.
308+
For each new domain, check `https://${domain}` and `https://www.${domain}` if you've configured the `www` subdomain.
264309

265310
[Back to top](#0)
266311

267312
## <a id="4"></a>Removing existing domains without downtime
268313

269314
Run the CLI tool, choose `Remove existing domains` and follow the instructions.
270315

316+
```bash
317+
./cli.sh config
318+
```
319+
320+
or
321+
271322
```bash
272323
docker compose run --rm cli
273324
```
@@ -284,6 +335,12 @@ This operation is not appropriate to run daily because each certificate will be
284335

285336
Run the CLI tool, choose `Manually renew all Let's Encrypt certificates (force renewal)` and follow the instructions.
286337

338+
```bash
339+
./cli.sh config
340+
```
341+
342+
or
343+
287344
```bash
288345
docker compose run --rm cli
289346
```
@@ -300,13 +357,23 @@ It is possible in dry run mode.
300357

301358
### <a id="6-1"></a>Step 1 - Perform an initial setup using the CLI tool
302359

360+
```bash
361+
./cli.sh config
362+
```
363+
364+
or
365+
303366
```bash
304367
docker compose run --rm cli
305368
```
306369

307370
### <a id="6-2"></a>Step 2 - Start the services in dry run mode
308371

309-
Enable dry run mode by setting the environment variable `DRY_RUN=true`.
372+
```bash
373+
./cli.sh up --dry-run
374+
```
375+
376+
Alternatively, you can enable dry run mode using the environment variable `DRY_RUN=true`.
310377

311378
```bash
312379
DRY_RUN=true docker compose up -d
@@ -332,9 +399,16 @@ upstream backend {
332399
```
333400
334401
After editing the Nginx configuration, do a hot reload of the Nginx configuration.
402+
Run the CLI tool and choose `Reload Nginx configuration without downtime`.
403+
404+
```bash
405+
./cli.sh config
406+
```
407+
408+
or
335409

336410
```bash
337-
docker compose exec --no-TTY nginx nginx -s reload
411+
docker compose run --rm cli
338412
```
339413

340414
Manual edits of the `nginx-conf/nginx.conf` and `nginx-conf/conf.d/${domain}.conf` are lost after running the CLI tool
@@ -346,6 +420,14 @@ To make Nginx configuration changes persistent, also edit the Handlebars templat
346420
- [`templates/nginx.conf.hbs`](templates/nginx.conf.hbs),
347421
- [`templates/servers.conf.hbs`](templates/servers.conf.hbs).
348422

423+
To add domain-specific configuration to a template use the [`ifEquals` Handlebars helper](cli/src/handlebars-helpers.js).
424+
425+
```hbs
426+
{{#ifEquals domain "a.evgeniy-khyst.com"}}
427+
# Configuration for a specific domain
428+
{{/ifEquals}}
429+
```
430+
349431
[Back to top](#0)
350432

351433
## <a id="8"></a>Running Docker containers as a non-root user
@@ -377,6 +459,18 @@ Run the CLI tool specifying the current user and `docker` group to make it creat
377459
CURRENT_USER="$(id -u):$(id -g)" DOCKER_GROUP="$(getent group docker | cut -d: -f3)" docker compose run --rm cli
378460
```
379461

462+
The convenience script `cli.sh` runs the CLI tool as the current user by default.
463+
464+
```bash
465+
./cli.sh config
466+
```
467+
468+
You can run the CLI tool as UID/GID 0 instead of the current user with the option `--no-current-user`.
469+
470+
```bash
471+
./cli.sh config --no-current-user
472+
```
473+
380474
[Back to top](#0)
381475

382476
## <a id="9"></a>SSL configuration for A+ rating

‎cli.sh

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/bin/bash
2+
3+
usage() {
4+
cat << EOF
5+
Usage: $(basename $0) COMMAND
6+
7+
config [OPTIONS] Run the CLI tool
8+
--no-current-user Run as root (Docker default) instead of the current user
9+
10+
up [OPTIONS] Build, (re)create, and start all services in the background
11+
--dry-run Disable Certbot to run all services locally
12+
13+
build [OPTIONS] Build or rebuild all Docker images
14+
--no-cache Do not use cache when building the images
15+
EOF
16+
exit 1
17+
}
18+
19+
case $1 in
20+
config)
21+
CMD=(docker compose run --rm cli)
22+
export CURRENT_USER="$(id -u):$(id -g)"
23+
export DOCKER_GROUP="$(getent group docker | cut -d: -f3)"
24+
shift
25+
while [[ $# -gt 0 ]]; do
26+
case $1 in
27+
--no-current-user)
28+
unset CURRENT_USER
29+
unset DOCKER_GROUP
30+
shift
31+
;;
32+
*)
33+
echo "Unknown option $1"
34+
usage
35+
;;
36+
esac
37+
done
38+
;;
39+
up)
40+
CMD=(docker compose up -d)
41+
shift
42+
while [[ $# -gt 0 ]]; do
43+
case $1 in
44+
--dry-run)
45+
export DRY_RUN=true
46+
shift
47+
;;
48+
*)
49+
echo "Unknown option $1"
50+
usage
51+
;;
52+
esac
53+
done
54+
;;
55+
build)
56+
CMD=(docker compose --profile config build)
57+
shift
58+
while [[ $# -gt 0 ]]; do
59+
case $1 in
60+
--no-cache)
61+
CMD+=(--no-cache)
62+
shift
63+
;;
64+
*)
65+
echo "Unknown option $1"
66+
usage
67+
;;
68+
esac
69+
done
70+
;;
71+
*)
72+
echo "Unknown command $1"
73+
usage
74+
;;
75+
esac
76+
77+
"${CMD[@]}"

‎cli/src/cli.js

+29-16
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,26 @@ const askDomain = async (config, domainName) => {
107107
{
108108
type: 'confirm',
109109
name: 'websockets',
110-
message:
111-
'Do you want to proxy websocket connections as well?',
112-
default: true
110+
message: 'Enable WebSocket proxying?',
111+
default: domainConfig.websockets ?? false,
113112
},
114113
];
115114

116-
const { upstream, dockerDns, websockets } = await inquirer.prompt(
115+
const { dockerDns, upstream, websockets } = await inquirer.prompt(
117116
reverseProxyQuestions
118117
);
119118

120-
Object.assign(domainConfig, {
121-
upstream,
122-
...(dockerDns ? { dnsResolver: dockerDnsResolver } : {}),
123-
websockets,
124-
});
119+
Object.assign(
120+
domainConfig,
121+
{
122+
upstream,
123+
websockets,
124+
},
125+
dockerDns ? { dnsResolver: dockerDnsResolver } : {}
126+
);
125127
} else {
126-
delete domainConfig.upstream;
127128
delete domainConfig.dnsResolver;
129+
delete domainConfig.upstream;
128130
delete domainConfig.websockets;
129131
}
130132

@@ -192,7 +194,7 @@ const askNginxConfig = async (config) => {
192194
Object.assign(config, answers);
193195
};
194196

195-
const askConfim = async () => {
197+
const askConfirm = async () => {
196198
const questions = [
197199
{
198200
type: 'confirm',
@@ -214,7 +216,7 @@ const writeConfigFiles = async (config) => {
214216
const initConfig = async (config) => {
215217
await askDomain(config);
216218
await askNginxConfig(config);
217-
if (await askConfim()) {
219+
if (await askConfirm()) {
218220
await writeConfigFiles(config);
219221
}
220222
};
@@ -228,7 +230,7 @@ const obtainProductionCertificates = async (config) => {
228230

229231
domainConfig.testCert = false;
230232

231-
if (await askConfim()) {
233+
if (await askConfirm()) {
232234
await writeConfigFiles(config);
233235
await execDeleteCertbotCertificate(domainName);
234236
await execConfigNginx(); // Use dummy certificate
@@ -239,7 +241,7 @@ const obtainProductionCertificates = async (config) => {
239241

240242
const addDomains = async (config) => {
241243
await askDomain(config);
242-
if (await askConfim()) {
244+
if (await askConfirm()) {
243245
await writeConfigFiles(config);
244246
await execConfigNginx(); // Use dummy certificate
245247
await execCertbotCertonly(); // Obtain Let's Encrypt certificate
@@ -255,7 +257,7 @@ const removeDomains = async (config) => {
255257

256258
config.domains.splice(index, 1);
257259

258-
if (await askConfim()) {
260+
if (await askConfirm()) {
259261
await writeConfigFiles(config);
260262
await deleteNginxConfigFile(domainName);
261263
await execNginxReload();
@@ -264,12 +266,18 @@ const removeDomains = async (config) => {
264266
};
265267

266268
const forceRenewCertificates = async () => {
267-
if (await askConfim()) {
269+
if (await askConfirm()) {
268270
await execForceRenewCertbotCertificate();
269271
await execNginxReload();
270272
}
271273
};
272274

275+
const reloadNginxConfiguration = async () => {
276+
if (await askConfirm()) {
277+
await execNginxReload();
278+
}
279+
};
280+
273281
const askConfig = async () => {
274282
const config = await readConfig();
275283

@@ -312,6 +320,10 @@ const askConfig = async () => {
312320
name: "Manually renew all Let's Encrypt certificates (force renewal)",
313321
value: 'forceRenewCertificates',
314322
},
323+
{
324+
name: 'Reload Nginx configuration without downtime',
325+
value: 'reloadNginxConfiguration',
326+
},
315327
],
316328
},
317329
];
@@ -323,6 +335,7 @@ const askConfig = async () => {
323335
addDomains,
324336
removeDomains,
325337
forceRenewCertificates,
338+
reloadNginxConfiguration,
326339
};
327340

328341
await commands[command](config);

‎cli/src/config-processor.js

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as fs from 'fs/promises';
22
import Handlebars from 'handlebars';
3+
import registerHelpers from './handlebars-helpers.js';
4+
5+
registerHelpers(Handlebars);
36

47
const configPath = './config.json';
58
const templatesDir = './templates';

‎cli/src/handlebars-helpers.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default (Handlebars) => {
2+
Handlebars.registerHelper('ifEquals', function (arg1, arg2, options) {
3+
return arg1 === arg2 ? options.fn(this) : options.inverse(this);
4+
});
5+
};

‎examples/nodejs-backend/package-lock.json

+50-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎examples/nodejs-backend/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"license": "Apache-2.0",
1010
"dependencies": {
11-
"express": "^4.18.1"
11+
"express": "^4.18.1",
12+
"express-ws": "^5.0.2"
1213
}
1314
}

‎examples/nodejs-backend/server.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const express = require("express");
2+
const app = express();
3+
const expressWs = require("express-ws")(app);
24

35
const host = "0.0.0.0";
46
const port = 8080;
57

6-
const app = express();
78
app.get("/", (req, res) => {
89
res.send("Hello, world!");
910
});
@@ -12,5 +13,12 @@ app.get("/hello", (req, res) => {
1213
res.send(`Hello, ${req.query.name || "world"}!`);
1314
});
1415

16+
app.ws("/echo", function (ws, req) {
17+
ws.on("message", function (msg) {
18+
console.log("Received message", msg);
19+
ws.send(msg);
20+
});
21+
});
22+
1523
app.listen(port, host);
1624
console.log(`Running on http://${host}:${port}`);

‎scripts/rebuild-all-images.sh

-9
This file was deleted.

‎scripts/reload-nginx-conf.sh

-4
This file was deleted.

‎scripts/run-cli.sh

-3
This file was deleted.

‎templates/nginx.conf.hbs

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,10 @@ http {
3131
include /etc/nginx/conf.d/includes/gzip.conf;
3232
{{/if}}
3333

34+
map $http_upgrade $connection_upgrade {
35+
default upgrade;
36+
'' close;
37+
}
38+
3439
include /etc/nginx/conf.d/*.conf;
35-
}
40+
}

‎templates/servers.conf.hbs

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,20 @@ location / {
3535
proxy_set_header X-Real-IP $remote_addr;
3636
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
3737
proxy_set_header X-Forwarded-Proto $scheme;
38+
3839
{{#if dnsResolver}}
3940
# Let Nginx start if upstream host is unreachable
4041
set $upstream {{upstream}};
4142
proxy_pass http://$upstream;
4243
{{else}}
4344
proxy_pass http://{{upstream}};
4445
{{/if}}
46+
4547
{{#if websockets}}
4648
proxy_http_version 1.1;
4749
proxy_set_header Upgrade $http_upgrade;
48-
proxy_set_header Connection "upgrade";
49-
proxy_read_timeout 86400;
50+
proxy_set_header Connection $connection_upgrade;
51+
proxy_read_timeout 3600;
5052
{{/if}}
5153
}
5254
{{/inline}}

0 commit comments

Comments
 (0)
This repository has been archived.