~$ mkdir isocta
~$ cd isocta
~/isocta$ cat > docker-compose.yml << EOF
services:
nginx:
image: nginx
container_name: isocta-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/snippets:/etc/nginx/snippets:ro
- ./nginx/auth:/etc/nginx/auth:ro
- ./nginx/logs:/var/log/nginx
- ./nginx/www:/var/www
- ./certbot/www:/var/www/certbot:ro
- ./certbot/conf:/etc/letsencrypt:ro
networks:
- front
- back
certbot:
image: certbot/certbot:latest
container_name: isocta-certbot
restart: "no"
volumes:
- ./certbot/www:/var/www/certbot
- ./certbot/conf:/etc/letsencrypt
networks:
- front
nextcloud:
image: nextcloud
container_name: isocta-nextcloud
restart: unless-stopped
environment:
- POSTGRES_HOST=nextcloud-db
- POSTGRES_DB=${NC_DB_NAME}
- POSTGRES_USER=${NC_DB_USER}
- POSTGRES_PASSWORD=${NC_DB_PASS}
- REDIS_HOST=nextcloud-redis
- NEXTCLOUD_ADMIN_USER=${NC_ADMIN_USER}
- NEXTCLOUD_ADMIN_PASSWORD=${NC_ADMIN_PASS}
- NEXTCLOUD_TRUSTED_DOMAINS=${NC_TRUSTED_DOMAINS}
- OVERWRITEPROTOCOL=https
volumes:
- ./nextcloud/app:/var/www/html
depends_on:
- nextcloud-db
- nextcloud-redis
networks:
- back
nextcloud-db:
image: postgres
container_name: isocta-nextcloud-db
restart: unless-stopped
environment:
- POSTGRES_DB=${NC_DB_NAME}
- POSTGRES_USER=${NC_DB_USER}
- POSTGRES_PASSWORD=${NC_DB_PASS}
volumes:
- ./nextcloud/db:/var/lib/postgresql
networks:
- back
nextcloud-redis:
image: redis:7-alpine
container_name: isocta-nextcloud-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./nextcloud/redis/data:/data
networks:
- back
EOF
~/isocta$ cat .env
LE_DOMAINS="cloud.isocta.com isocta.com www.isocta.com jiri.isocta.com"
# Email for Let's Encrypt
LE_EMAIL="jiri@isocta.com"
# Nextcloud DB settings
NC_DB_NAME="nextcloud"
NC_DB_USER="nextcloud"
NC_DB_PASS="B66106000151952B963D99E3D1ADBF396C2FBA215188BF749A206BB9EC465C72"
NC_ADMIN_USER="jiri"
NC_ADMIN_PASS="XHRRyhiXF4cQ@PxtrXZXDYM9"
NC_TRUSTED_DOMAINS="cloud.isocta.com"
EOF
~/isocta$ mkdir -p certbot/{conf,www} nextcloud/{app,db,redis} nginx/{auth,conf.d/sites,logs,snippets,www} redis/data scripts
/isocta$ cat > nginx/nginx.conf << EOF
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
# Reasonable defaults
server_tokens off;
client_max_body_size 2g;
# Load per-site configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.d/sites/*.conf;
}
EOF
~/isocta$ cat > nginx/conf.d/00-acme-redirect.conf << EOF
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type "text/plain";
}
location / {
return 301 https://$host$request_uri;
}
}
EOF
~/isocta$ cat > nginx/conf.d/sites/cloud.isocta.com.conf << EOF
upstream nextcloud_upstream {
server nextcloud:80;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name cloud.isocta.com;
ssl_certificate /etc/letsencrypt/live/cloud.isocta.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cloud.isocta.com/privkey.pem;
client_max_body_size 32g;
client_body_timeout 3600s;
fastcgi_read_timeout 3600s;
include /etc/nginx/snippets/ssl.conf;
location / {
include /etc/nginx/snippets/proxy.conf;
# Nextcloud behind reverse proxy wants this:
proxy_set_header X-Forwarded-Port 443;
proxy_pass http://nextcloud_upstream;
}
}
EOF
~/isocta$ cat > nginx/snippets/proxy.conf << EOF
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
EOF
~/isocta$ cat > nginx/snippets/ssl.conf << EOF
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy no-referrer-when-downgrade always;
EOF
~/isocta$ cat > scripts/certs.sh << EOF
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
echo "Loading env vars"
# Load env vars safely (supports spaces + quotes)
if [ -f .env ]; then
set -a # automatically export all variables
# shellcheck disable=SC1091
. ./.env
set +a
fi
: "${LE_EMAIL:?Set LE_EMAIL in .env}"
: "${LE_DOMAINS:?Set LE_DOMAINS in .env}"
cmd="${1:-}"
usage() {
echo "Usage:"
echo " $0 init # issue certs for all domains in LE_DOMAINS"
echo " $0 renew # renew any expiring certs"
echo " $0 reload # reload nginx"
exit 1
}
reload_nginx() {
docker compose exec -T nginx nginx -s reload
}
init_certs() {
# Ensure nginx is up serving /.well-known/acme-challenge on port 80
docker compose up -d nginx
for d in $LE_DOMAINS; do
echo "$d"
if [ -d "certbot/conf/live/$d" ]; then
echo "Cert already exists for $d, skipping."
continue
fi
echo "Issuing cert for $d..."
docker compose run --rm certbot certonly \
--webroot -w /var/www/certbot \
--email "$LE_EMAIL" --agree-tos --no-eff-email \
-d "$d"
echo "Done: $d"
done
reload_nginx
echo "All certs issued and nginx reloaded."
}
renew_certs() {
echo "Renewing certs if needed..."
docker compose run --rm certbot renew --webroot -w /var/www/certbot
reload_nginx
echo "Renew complete; nginx reloaded."
}
case "$cmd" in
init) init_certs ;;
renew) renew_certs ;;
reload) reload_nginx ;;
*) usage ;;
esac
EOF
~/isocta$ chmod +x scripts/certs.sh