Add automated TLS renewal and deployment documentation
Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
10
.env
10
.env
@ -1,10 +0,0 @@
|
||||
# .env
|
||||
# Database
|
||||
NEXTCLOUD_DB_NAME=nextcloud
|
||||
NEXTCLOUD_DB_USER=nextcloud
|
||||
NEXTCLOUD_DB_PASSWORD=u@ZFN8y8WzwVPgE
|
||||
NEXTCLOUD_DB_ROOT_PASSWORD=FNEyZG6pzPv*uf9
|
||||
|
||||
# Nextcloud admin
|
||||
NEXTCLOUD_ADMIN_USER=admin
|
||||
NEXTCLOUD_ADMIN_PASSWORD=7VZxFLJd-8Lb*pp
|
||||
9
.env.example
Normal file
9
.env.example
Normal file
@ -0,0 +1,9 @@
|
||||
# Database
|
||||
NEXTCLOUD_DB_NAME=nextcloud
|
||||
NEXTCLOUD_DB_USER=nextcloud
|
||||
NEXTCLOUD_DB_PASSWORD=change-me
|
||||
NEXTCLOUD_DB_ROOT_PASSWORD=change-me
|
||||
|
||||
# Nextcloud admin
|
||||
NEXTCLOUD_ADMIN_USER=admin
|
||||
NEXTCLOUD_ADMIN_PASSWORD=change-me
|
||||
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Local runtime configuration
|
||||
.env
|
||||
.tls-renewal.env
|
||||
|
||||
# Runtime TLS artifacts (never commit private keys/certs)
|
||||
nginx/ssl/*.crt
|
||||
nginx/ssl/*.key
|
||||
nginx/ssl/*.pem
|
||||
nginx/ssl/letsencrypt/
|
||||
|
||||
# Runtime logs
|
||||
logs/
|
||||
5
.tls-renewal.env.example
Normal file
5
.tls-renewal.env.example
Normal file
@ -0,0 +1,5 @@
|
||||
# Used by scripts/renew-production-tls.sh in non-interactive contexts (e.g. cron)
|
||||
# Copy to .tls-renewal.env and adjust values.
|
||||
LETSENCRYPT_EMAIL=you@example.com
|
||||
CLOUDFLARE_TOKEN_SCRIPT=$HOME/bin/cloudflare-api-usertoken.sh
|
||||
CF_DNS_PROPAGATION_SECONDS=60
|
||||
114
README.md
114
README.md
@ -1,34 +1,86 @@
|
||||
# Nextcloud on Ubuntu via Docker for nxt.bhatfamily.in
|
||||
|
||||
This repo deploys Nextcloud using Docker on Ubuntu, fronted by Nginx on:
|
||||
|
||||
- HTTP: 8082
|
||||
- HTTPS: 8446
|
||||
|
||||
It is intended to serve the hostname **nxt.bhatfamily.in**.
|
||||
|
||||
This repository deploys Nextcloud behind Nginx using Docker Compose.
|
||||
Exposed ports:
|
||||
- HTTP: `8082`
|
||||
- HTTPS: `8446`
|
||||
Target hostname:
|
||||
- `nxt.bhatfamily.in`
|
||||
## What changed
|
||||
The stack now includes:
|
||||
- Fixed Nginx mount path (`nginx/nginx.conf` mapped correctly)
|
||||
- Fixed MariaDB command (`mariadbd`)
|
||||
- Nginx reverse proxy mode for `nextcloud:apache` (no FastCGI mismatch)
|
||||
- Production TLS provisioning using Let's Encrypt DNS-01 with Cloudflare
|
||||
- Automated TLS renewal job support (cron)
|
||||
## Prerequisites
|
||||
|
||||
- Ubuntu host with Docker and Docker Compose installed
|
||||
- Static public IP (or Cloudflare Tunnel)
|
||||
- Domain `bhatfamily.in` managed in Cloudflare
|
||||
- Basic familiarity with UFW and router port forwarding
|
||||
|
||||
## DNS (Cloudflare)
|
||||
|
||||
1. In Cloudflare DNS for `bhatfamily.in`, create:
|
||||
|
||||
- Type: A
|
||||
- Name: `nxt`
|
||||
- IPv4: your Ubuntu server public IP
|
||||
- Proxy: DNS only (grey cloud)
|
||||
|
||||
2. Forward ports 8082 and 8446 from your router to the Ubuntu host.
|
||||
|
||||
## First-time setup
|
||||
|
||||
1. Clone this repo and enter directory:
|
||||
|
||||
- Ubuntu host with Docker + Docker Compose plugin (or `docker-compose`)
|
||||
- Domain `nxt.bhatfamily.in` in Cloudflare DNS
|
||||
- DNS A record for `nxt` pointing to your server public IP (DNS-only)
|
||||
- Router/firewall forwarding for ports `8082` and `8446`
|
||||
## Initial setup
|
||||
1. Create runtime env file:
|
||||
```bash
|
||||
git clone <your-repo-url> nextcloud-docker-nxt.bhatfamily.in
|
||||
cd nextcloud-docker-nxt.bhatfamily.in
|
||||
cp .env.example .env
|
||||
```
|
||||
2. Edit `.env` with strong values.
|
||||
3. Start stack with bootstrap TLS:
|
||||
```bash
|
||||
./scripts/install.sh
|
||||
```
|
||||
4. Validate:
|
||||
```bash
|
||||
./scripts/test.sh
|
||||
```
|
||||
## Production TLS (Let's Encrypt + Cloudflare DNS-01)
|
||||
1. Export credentials in shell:
|
||||
```bash
|
||||
export CF_DNS_API_TOKEN={{CF_DNS_API_TOKEN}}
|
||||
export LETSENCRYPT_EMAIL={{LETSENCRYPT_EMAIL}}
|
||||
```
|
||||
2. Issue/renew and install production cert:
|
||||
```bash
|
||||
./scripts/provision-production-tls.sh
|
||||
```
|
||||
3. Reload Nginx container:
|
||||
```bash
|
||||
docker compose restart web
|
||||
```
|
||||
4. Verify cert:
|
||||
```bash
|
||||
echo | openssl s_client -connect nxt.bhatfamily.in:8446 -servername nxt.bhatfamily.in 2>/dev/null | openssl x509 -noout -subject -issuer -dates
|
||||
```
|
||||
## Automated renewal job (cron)
|
||||
1. Ensure your Cloudflare token export script exists (default path used by renewal wrapper):
|
||||
- `~/bin/cloudflare-api-usertoken.sh`
|
||||
2. Install/update renewal cron entry:
|
||||
```bash
|
||||
./scripts/setup-renewal-cron.sh
|
||||
```
|
||||
This script will:
|
||||
- create/update `.tls-renewal.env` (local only, not committed)
|
||||
- install a daily cron job (`03:17` by default)
|
||||
- write logs to `logs/tls-renew.log`
|
||||
3. Manual renewal run (same path cron uses):
|
||||
```bash
|
||||
./scripts/renew-production-tls.sh
|
||||
```
|
||||
## Useful commands
|
||||
Start/update containers:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
Restart all services:
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
Restart web only:
|
||||
```bash
|
||||
docker compose restart web
|
||||
```
|
||||
Stop and remove containers/volumes:
|
||||
```bash
|
||||
./scripts/uninstall.sh
|
||||
```
|
||||
## Security notes
|
||||
- `.env`, `.tls-renewal.env`, and runtime cert material under `nginx/ssl` are intentionally ignored by Git.
|
||||
- If secrets were ever committed earlier, rotate them.
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
# docker-compose.yml
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:11
|
||||
|
||||
0
nginx/ssl/.gitkeep
Normal file
0
nginx/ssl/.gitkeep
Normal file
@ -1,8 +0,0 @@
|
||||
-----BEGIN DH PARAMETERS-----
|
||||
MIIBCAKCAQEA6URKKHp177eP2+UP//DCEmAP63DpZNtVQeC0PO3x14Jomg6bicVa
|
||||
ySBkEGtSHw2WMtcGe/4P0AyrPuVL8VgIUvFXzLO2j8XeHZ/Gu5LWZw3bAwONBUC2
|
||||
N4l2msyNMRoPBnu/BjjlYEkDEsqo5VX38ZdaCSa7ZgseYAWV524jW2VewB58ox6s
|
||||
c0iw5aoq1XrJp5+mgj/XG2i0xO5DvdLfDa/Yvslhi8MT7GLvfoHziLcybB949ZK4
|
||||
PSCPvvZE17P5oQhRgLhCWe2VW7TchRLy4FTk8Iv2HV7ndDzZEsXIGjy8MgyDQNCd
|
||||
YxIIa1Fkw90Bzw8htyNX3AcakTlWy8d/ZwIBAg==
|
||||
-----END DH PARAMETERS-----
|
||||
@ -1,30 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFGTCCAwGgAwIBAgIUHAA348XpRrN8g/F5MM2n1PRwGOQwDQYJKoZIhvcNAQEL
|
||||
BQAwHDEaMBgGA1UEAwwRbnh0LmJoYXRmYW1pbHkuaW4wHhcNMjYwNDE3MDIzMDU4
|
||||
WhcNMjcwNDE3MDIzMDU4WjAcMRowGAYDVQQDDBFueHQuYmhhdGZhbWlseS5pbjCC
|
||||
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKygVD9ln901VJIhiq7cDu5Q
|
||||
mxvCN+jlfMzSDeIri7SDqBKFgLh5l+c0SddNpKmRqnSLWAFPrJbKRXuwmE5sAD6t
|
||||
wPDKiwxpX09IP6lhBojjKAPRpHNCWw0JRJJgOYixm7/dsDIenTK55AuQkIeM9Iic
|
||||
R6ohzoeJ+zDF+etGedh0O+UI7XDhZJECUOGUJBxSPepeETww436FNJfoLx7VxS+4
|
||||
VF65Zq7uqv18FhqO1gab4q/ydwZ9AbIILfUdZXWbd6z3DbmKug0FPyFOASwfNCFD
|
||||
mJ7dIFBsOu6mhRpV881Gjitn3O5RwrIC0QJ4R2FQ/SKhat1GUlmNNx9ELt+VH6VB
|
||||
+jyNk9/e3pEHKlweKki6HW8tB/MT7ZZkk0/qD0Kg/SEqLsxblmnLv5s6GXLPfMNR
|
||||
yqV7Gs0ZlXbCkFSYrMSh5A4NACJUtGpq5rTZNKtn9taMHGuO3l0h8K88Su0t2nRM
|
||||
yxBgL6WizvdsQvqHowzjQVnBds6cTUwR7QLxUUbKr4uuZoo0lleUlHGvubNDdOIE
|
||||
6NKuJBB1rcENO5B6tITPRrOYrurDwVPVvHBfLlYHEQvvaypWQtDpcf39GTHCIekK
|
||||
C4NysXrDGbshcK9fH30VwiR78kkvXLShbytGL4aq6yf9Gp77iMSt69GUCR4wIGMu
|
||||
ck7H96LmA82vx8Q+bZhDAgMBAAGjUzBRMB0GA1UdDgQWBBTV67Dyn+iaubohmUDq
|
||||
OBp78vgKJTAfBgNVHSMEGDAWgBTV67Dyn+iaubohmUDqOBp78vgKJTAPBgNVHRMB
|
||||
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA4t0pSJViW66Ue+KX5YD3GWW5Z
|
||||
RQy8XwrPqHqFHU8yjf5jeK2bsBXQC5Ovr/dQnlLs2GdGvHFmthcWFlpzF4zRzVFK
|
||||
jy2ZgqDYV+ET0rPqpGkz3iThTRLWAEm7q6gwZtmGQSdpuKK5Ei47TqfOtq/r0Juy
|
||||
cg12PQs3wUuavNXWVz/uRis+RZiWZhn9+xz6XRhEPZjTOyIoS65frm1uvFv96Sw+
|
||||
0Lrxn2e/oFRuV0yrnaIV/bJjPvf0/VGVrQ0DmWfrJ1ywXwl+Ys/Ib9LmVTU0tdlL
|
||||
vVR5a/6Lg5iSNiQqm8VrLspFj5iv6x65ygb8klbUxHz7zPmwGRrG8UxscEg9pF5u
|
||||
TaZfCk+V6VSp+VjdLUUdmn4UGSN2tPMBdIwQjZAsBHN6pPaTE9EpxOqBQG64rUzt
|
||||
jR8damZ3X+qHaS8Lz3wvRFYr1NNVC2oTx5+1neM0zfSEcGkmNlkONcmkWghzggI/
|
||||
g7xTm3oSlrUKqw+cSoksMaa8bCuXMR0CtM8x/sPZgwiDwQVQOGBVQKmnMbMq9eGh
|
||||
QkGHAERUeGUNArdYeYxzjFWQ1EmWIy9kjmVIePt4qNpGQWQ/lk2XTkScsG4GrQ1q
|
||||
JVqFP1RwTkZfgo1KPTT1FrxVawuIiSemaqmTw8y08zKYw3UJxwVhgo50cmiSiCh7
|
||||
UunNLvfJ2QgFnV0yYw==
|
||||
-----END CERTIFICATE-----
|
||||
@ -1,52 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
6RSKsmjGRIZba+3ia3R8g8Z2rwhv
|
||||
-----END PRIVATE KEY-----
|
||||
@ -6,60 +6,68 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SSL_DIR="${REPO_DIR}/nginx/ssl"
|
||||
DOMAIN="nxt.bhatfamily.in"
|
||||
|
||||
echo "==> Ensuring required packages are installed (docker, docker-compose, ufw, openssl)..."
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Docker is not installed. Install Docker and rerun this script."
|
||||
echo "ERROR: Docker is not installed. Install Docker and rerun this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker compose >/dev/null 2>&1 && ! command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "docker compose / docker-compose is not installed. Install Docker Compose and rerun."
|
||||
if ! docker compose version >/dev/null 2>&1 && ! command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "ERROR: docker compose / docker-compose is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "ERROR: openssl is not installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ufw >/dev/null 2>&1; then
|
||||
echo "ufw not found. Installing ufw requires root and internet access."
|
||||
echo "INFO: ufw not found. Ensure ports 8082 and 8446 are open in your firewall/router."
|
||||
fi
|
||||
|
||||
compose() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
docker compose "$@"
|
||||
else
|
||||
docker-compose "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "${SSL_DIR}"
|
||||
|
||||
echo "==> Generating self-signed TLS certificate for ${DOMAIN} (valid 365 days)..."
|
||||
echo "==> Ensuring TLS files exist for nginx startup..."
|
||||
if [ ! -f "${SSL_DIR}/${DOMAIN}.crt" ] || [ ! -f "${SSL_DIR}/${DOMAIN}.key" ]; then
|
||||
echo "==> Generating bootstrap self-signed certificate for ${DOMAIN} (valid 365 days)..."
|
||||
openssl req -x509 -nodes -newkey rsa:4096 \
|
||||
-keyout "${SSL_DIR}/${DOMAIN}.key" \
|
||||
-out "${SSL_DIR}/${DOMAIN}.crt" \
|
||||
-days 365 \
|
||||
-subj "/CN=${DOMAIN}"
|
||||
else
|
||||
echo "Certificate already exists, skipping generation."
|
||||
echo "==> Existing certificate/key found; skipping bootstrap certificate generation."
|
||||
fi
|
||||
|
||||
if [ ! -f "${SSL_DIR}/dhparam.pem" ]; then
|
||||
echo "==> Generating dhparam (this may take a while)..."
|
||||
echo "==> Generating dhparam (one-time, may take a while)..."
|
||||
openssl dhparam -out "${SSL_DIR}/dhparam.pem" 2048
|
||||
fi
|
||||
|
||||
echo "==> Configuring UFW firewall rules (allow 8082/tcp and 8446/tcp)..."
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
echo "==> Configuring UFW firewall rules (8082/tcp, 8446/tcp)..."
|
||||
sudo ufw allow 8082/tcp comment "Nextcloud HTTP"
|
||||
sudo ufw allow 8446/tcp comment "Nextcloud HTTPS"
|
||||
else
|
||||
echo "ufw not installed; ensure ports 8082 and 8446 are open in your firewall/router."
|
||||
fi
|
||||
|
||||
echo "==> Starting Nextcloud stack via Docker Compose..."
|
||||
cd "${REPO_DIR}"
|
||||
echo "==> Pulling and starting containers..."
|
||||
compose -f "${REPO_DIR}/docker-compose.yml" pull
|
||||
compose -f "${REPO_DIR}/docker-compose.yml" up -d
|
||||
|
||||
if command -v docker compose >/dev/null 2>&1; then
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
else
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
echo "==> Nextcloud should now be reachable at:"
|
||||
echo "==> Stack started"
|
||||
echo " http://${DOMAIN}:8082 (redirects to HTTPS)"
|
||||
echo " https://${DOMAIN}:8446"
|
||||
echo ""
|
||||
echo "NOTE: Browser will warn about self-signed certificate. Replace with a valid cert for production."
|
||||
echo
|
||||
echo "For production TLS with Let's Encrypt DNS-01 (Cloudflare):"
|
||||
echo " 1) export CF_DNS_API_TOKEN=<token>"
|
||||
echo " 2) export LETSENCRYPT_EMAIL=<email>"
|
||||
echo " 3) ./scripts/provision-production-tls.sh"
|
||||
echo " 4) docker compose restart web"
|
||||
|
||||
73
scripts/provision-production-tls.sh
Executable file
73
scripts/provision-production-tls.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/provision-production-tls.sh
|
||||
# Issue/renew Let's Encrypt certificate via DNS-01 (Cloudflare), then install
|
||||
# cert/key into nginx/ssl paths used by the existing Nginx config.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DOMAIN="${DOMAIN:-nxt.bhatfamily.in}"
|
||||
SSL_DIR="${REPO_DIR}/nginx/ssl"
|
||||
LE_DIR="${SSL_DIR}/letsencrypt"
|
||||
PROPAGATION_SECONDS="${CF_DNS_PROPAGATION_SECONDS:-60}"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ERROR: docker is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${CF_DNS_API_TOKEN:-}" ]; then
|
||||
echo "ERROR: CF_DNS_API_TOKEN is not set."
|
||||
echo "Export it first, then rerun this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
|
||||
echo "ERROR: LETSENCRYPT_EMAIL is not set."
|
||||
echo "Export it first, then rerun this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${LE_DIR}" "${SSL_DIR}"
|
||||
|
||||
CF_CREDS_FILE="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "${CF_CREDS_FILE}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
chmod 600 "${CF_CREDS_FILE}"
|
||||
printf 'dns_cloudflare_api_token = %s\n' "${CF_DNS_API_TOKEN}" > "${CF_CREDS_FILE}"
|
||||
|
||||
echo "==> Requesting/renewing Let's Encrypt cert for ${DOMAIN} via Cloudflare DNS-01..."
|
||||
docker run --rm \
|
||||
-v "${LE_DIR}:/etc/letsencrypt" \
|
||||
-v "${CF_CREDS_FILE}:/run/secrets/cloudflare.ini:ro" \
|
||||
certbot/dns-cloudflare:latest certonly \
|
||||
--dns-cloudflare \
|
||||
--dns-cloudflare-credentials /run/secrets/cloudflare.ini \
|
||||
--dns-cloudflare-propagation-seconds "${PROPAGATION_SECONDS}" \
|
||||
--non-interactive \
|
||||
--agree-tos \
|
||||
--email "${LETSENCRYPT_EMAIL}" \
|
||||
--keep-until-expiring \
|
||||
--preferred-challenges dns-01 \
|
||||
--rsa-key-size 4096 \
|
||||
-d "${DOMAIN}"
|
||||
|
||||
echo "==> Installing production certificate into nginx/ssl..."
|
||||
docker run --rm \
|
||||
-v "${LE_DIR}:/etc/letsencrypt:ro" \
|
||||
-v "${SSL_DIR}:/work" \
|
||||
alpine:3.20 sh -lc " test -f /etc/letsencrypt/live/${DOMAIN}/fullchain.pem && \
|
||||
test -f /etc/letsencrypt/live/${DOMAIN}/privkey.pem && \
|
||||
cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem /work/${DOMAIN}.crt && \
|
||||
cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem /work/${DOMAIN}.key && \
|
||||
chmod 0644 /work/${DOMAIN}.crt && \
|
||||
chmod 0600 /work/${DOMAIN}.key "
|
||||
|
||||
echo "==> Production TLS material installed:"
|
||||
echo " ${SSL_DIR}/${DOMAIN}.crt"
|
||||
echo " ${SSL_DIR}/${DOMAIN}.key"
|
||||
echo ""
|
||||
echo "Next step: restart web service (or full stack) to pick up the new certificate."
|
||||
69
scripts/renew-production-tls.sh
Executable file
69
scripts/renew-production-tls.sh
Executable file
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/renew-production-tls.sh
|
||||
# Non-interactive renewal wrapper:
|
||||
# - loads renewal env defaults
|
||||
# - loads Cloudflare token export script
|
||||
# - runs certificate provisioning
|
||||
# - restarts only nextcloud-web when certificate changed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DOMAIN="${DOMAIN:-nxt.bhatfamily.in}"
|
||||
SSL_DIR="${REPO_DIR}/nginx/ssl"
|
||||
TLS_ENV_FILE="${TLS_ENV_FILE:-${REPO_DIR}/.tls-renewal.env}"
|
||||
CLOUDFLARE_TOKEN_SCRIPT_DEFAULT="${HOME}/bin/cloudflare-api-usertoken.sh"
|
||||
|
||||
if [ -f "${TLS_ENV_FILE}" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "${TLS_ENV_FILE}"
|
||||
set +a
|
||||
fi
|
||||
|
||||
CLOUDFLARE_TOKEN_SCRIPT="${CLOUDFLARE_TOKEN_SCRIPT:-${CLOUDFLARE_TOKEN_SCRIPT_DEFAULT}}"
|
||||
|
||||
if [ -z "${CF_DNS_API_TOKEN:-}" ] && [ -f "${CLOUDFLARE_TOKEN_SCRIPT}" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${CLOUDFLARE_TOKEN_SCRIPT}"
|
||||
fi
|
||||
|
||||
if [ -z "${CF_DNS_API_TOKEN:-}" ]; then
|
||||
echo "ERROR: CF_DNS_API_TOKEN is not set and could not be loaded from ${CLOUDFLARE_TOKEN_SCRIPT}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
|
||||
echo "ERROR: LETSENCRYPT_EMAIL is not set."
|
||||
echo "Set it in ${TLS_ENV_FILE} or export it in the shell before running this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${REPO_DIR}/logs"
|
||||
|
||||
CERT_FILE="${SSL_DIR}/${DOMAIN}.crt"
|
||||
BEFORE_SHA=""
|
||||
if [ -f "${CERT_FILE}" ]; then
|
||||
BEFORE_SHA="$(sha256sum "${CERT_FILE}" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
echo "==> Running production TLS provisioning/renewal..."
|
||||
"${REPO_DIR}/scripts/provision-production-tls.sh"
|
||||
|
||||
AFTER_SHA=""
|
||||
if [ -f "${CERT_FILE}" ]; then
|
||||
AFTER_SHA="$(sha256sum "${CERT_FILE}" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
if [ "${BEFORE_SHA}" != "${AFTER_SHA}" ]; then
|
||||
echo "==> Certificate changed; restarting nextcloud-web..."
|
||||
if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
docker compose -f "${REPO_DIR}/docker-compose.yml" restart web
|
||||
else
|
||||
docker-compose -f "${REPO_DIR}/docker-compose.yml" restart web
|
||||
fi
|
||||
else
|
||||
echo "==> Certificate unchanged; no container restart required."
|
||||
fi
|
||||
|
||||
echo "==> Renewal workflow complete."
|
||||
52
scripts/setup-renewal-cron.sh
Executable file
52
scripts/setup-renewal-cron.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/setup-renewal-cron.sh
|
||||
# Installs/updates a daily cron entry for automated TLS renewal.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TLS_ENV_FILE="${REPO_DIR}/.tls-renewal.env"
|
||||
LOG_DIR="${REPO_DIR}/logs"
|
||||
LOG_FILE="${LOG_DIR}/tls-renew.log"
|
||||
CRON_MARKER="# nextcloud-docker tls renewal"
|
||||
CRON_SCHEDULE="${RENEW_CRON_SCHEDULE:-17 3 * * *}"
|
||||
DEFAULT_TOKEN_SCRIPT="${HOME}/bin/cloudflare-api-usertoken.sh"
|
||||
|
||||
if [ -f "${TLS_ENV_FILE}" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
set -a
|
||||
source "${TLS_ENV_FILE}"
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ -z "${LETSENCRYPT_EMAIL:-}" ]; then
|
||||
echo "ERROR: LETSENCRYPT_EMAIL is not set."
|
||||
echo "Export it in your shell before running this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLOUDFLARE_TOKEN_SCRIPT="${CLOUDFLARE_TOKEN_SCRIPT:-${DEFAULT_TOKEN_SCRIPT}}"
|
||||
CF_DNS_PROPAGATION_SECONDS="${CF_DNS_PROPAGATION_SECONDS:-60}"
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
chmod 700 "${LOG_DIR}"
|
||||
|
||||
cat > "${TLS_ENV_FILE}" <<ENVFILE
|
||||
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
|
||||
CLOUDFLARE_TOKEN_SCRIPT=${CLOUDFLARE_TOKEN_SCRIPT}
|
||||
CF_DNS_PROPAGATION_SECONDS=${CF_DNS_PROPAGATION_SECONDS}
|
||||
ENVFILE
|
||||
chmod 600 "${TLS_ENV_FILE}"
|
||||
|
||||
CRON_COMMAND="cd ${REPO_DIR} && /usr/bin/env bash ${REPO_DIR}/scripts/renew-production-tls.sh >> ${LOG_FILE} 2>&1"
|
||||
CRON_LINE="${CRON_SCHEDULE} ${CRON_COMMAND} ${CRON_MARKER}"
|
||||
|
||||
{
|
||||
crontab -l 2>/dev/null | grep -v "${CRON_MARKER}" || true
|
||||
echo "${CRON_LINE}"
|
||||
} | crontab -
|
||||
|
||||
echo "==> Installed cron renewal job:"
|
||||
echo " ${CRON_LINE}"
|
||||
echo "==> Stored renewal defaults in ${TLS_ENV_FILE}"
|
||||
echo "==> Logs will be written to ${LOG_FILE}"
|
||||
@ -5,23 +5,20 @@ set -euo pipefail
|
||||
DOMAIN="nxt.bhatfamily.in"
|
||||
HTTP_PORT=8082
|
||||
HTTPS_PORT=8446
|
||||
STRICT_TLS="${STRICT_TLS:-0}"
|
||||
|
||||
CURL_TLS_ARGS=("-k")
|
||||
if [ "${STRICT_TLS}" = "1" ]; then
|
||||
CURL_TLS_ARGS=()
|
||||
fi
|
||||
|
||||
echo "==> Testing HTTP redirect..."
|
||||
curl -I "http://${DOMAIN}:${HTTP_PORT}" || {
|
||||
echo "HTTP test failed."
|
||||
exit 1
|
||||
}
|
||||
curl -I "http://${DOMAIN}:${HTTP_PORT}" >/dev/null
|
||||
|
||||
echo "==> Testing HTTPS endpoint (ignoring self-signed cert errors)..."
|
||||
curl -k -I "https://${DOMAIN}:${HTTPS_PORT}" || {
|
||||
echo "HTTPS test failed."
|
||||
exit 1
|
||||
}
|
||||
echo "==> Testing HTTPS endpoint..."
|
||||
curl "${CURL_TLS_ARGS[@]}" -I "https://${DOMAIN}:${HTTPS_PORT}" >/dev/null
|
||||
|
||||
echo "==> Quick application-level check (Nextcloud status.php)..."
|
||||
curl -k "https://${DOMAIN}:${HTTPS_PORT}/status.php" || {
|
||||
echo "status.php test failed."
|
||||
exit 1
|
||||
}
|
||||
echo "==> Testing Nextcloud status.php..."
|
||||
curl "${CURL_TLS_ARGS[@]}" "https://${DOMAIN}:${HTTPS_PORT}/status.php" >/dev/null
|
||||
|
||||
echo "All tests passed."
|
||||
|
||||
Reference in New Issue
Block a user