Files
cloudron-box/setup/start.sh
Girish Ramakrishnan 804947f039 use dir mount instead of file mount
file mounting is fraught with problems wrt change notifications.

first, we must be carefule that the inode does not change.

second, changes outside container do not result in fs events inside the container.
haraka cache settings files and relies on fs events. So, even
though the file gets updated inside the container, haraka doesn't
see it.

https://github.com/docker/docker/issues/15793
2017-01-17 23:59:23 -08:00

306 lines
12 KiB
Bash
Executable File

#!/bin/bash
set -eu -o pipefail
echo "==> Cloudron Start"
readonly USER="yellowtent"
readonly DATA_FILE="/root/user_data.img"
readonly BOX_SRC_DIR="/home/${USER}/box"
readonly DATA_DIR="/home/${USER}/data"
readonly CONFIG_DIR="/home/${USER}/configs"
readonly SETUP_PROGRESS_JSON="/home/yellowtent/setup/website/progress.json"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
readonly is_update=$([[ -f "${CONFIG_DIR}/cloudron.conf" ]] && echo "true" || echo "false")
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_fqdn}"
echo "==> Setting up firewall"
iptables -t filter -N CLOUDRON || true
iptables -t filter -F CLOUDRON # empty any existing rules
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# caas has ssh on port 202
if [[ "${arg_provider}" == "caas" ]]; then
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
else
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,22,443,587,993,4190 -j ACCEPT
fi
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
iptables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
# log dropped incoming. keep this at the end of all the rules
iptables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
iptables -t filter -A CLOUDRON -j DROP
if ! iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null; then
iptables -t filter -I INPUT -j CLOUDRON
fi
# so it gets restored across reboot
mkdir -p /etc/iptables && iptables-save > /etc/iptables/rules.v4
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
systemctl restart apparmor
usermod yellowtent -a -G docker
temp_file=$(mktemp)
# some apps do not work with aufs
sed -e 's,^ExecStart=.*$,ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper --dns=172.18.0.1 --dns-search=.,' /lib/systemd/system/docker.service > "${temp_file}"
systemctl enable docker
if ! diff -q /lib/systemd/system/docker.service "${temp_file}" >/dev/null; then
mv "${temp_file}" /lib/systemd/system/docker.service
systemctl daemon-reload
systemctl restart docker
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
# caas has ssh on port 202 and we disable password login
if [[ "${arg_provider}" == "caas" ]]; then
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-e 's/^#\?Port .*/Port 202/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
echo "==> Setup btrfs data"
if ! grep -q loop.ko /lib/modules/`uname -r`/modules.builtin; then
# on scaleway loop is not built-in
echo "loop" >> /etc/modules
modprobe loop
fi
if [[ ! -d "${DATA_DIR}" ]]; then
echo "==> Mounting loopback btrfs"
truncate -s "8192m" "${DATA_FILE}" # 8gb start (this will get resized dynamically by cloudron-resize-fs.service)
mkfs.btrfs -L UserDataHome "${DATA_FILE}"
mkdir -p "${DATA_DIR}"
mount -t btrfs -o loop,nosuid "${DATA_FILE}" ${DATA_DIR}
fi
# keep these in sync with paths.js
echo "==> Ensuring directories"
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/acme" # acme keys
mkdir -p "${DATA_DIR}/graphite"
mkdir -p "${DATA_DIR}/box/mail/dkim"
if [[ -n "${arg_fqdn}" ]]; then
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
fi
mkdir -p "${DATA_DIR}/mysql"
mkdir -p "${DATA_DIR}/postgresql"
mkdir -p "${DATA_DIR}/mongodb"
mkdir -p "${DATA_DIR}/snapshots"
mkdir -p "${DATA_DIR}/addons/mail"
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${DATA_DIR}/acme"
echo "==> Configuring journald"
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
-i /etc/systemd/journald.conf
# When rotating logs, systemd kills journald too soon sometimes
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
-i /lib/systemd/system/systemd-journald.service
# Give user access to system logs
usermod -a -G systemd-journal yellowtent
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
chown root:systemd-journal /var/log/journal
systemctl restart systemd-journald
setfacl -n -m u:yellowtent:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl daemon-reload
systemctl enable cloudron.target
systemctl enable iptables-restore
# For logrotate
systemctl enable --now cron
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl enable unbound
systemctl restart unbound
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/yellowtent
cp "${script_dir}/start/sudoers" /etc/sudoers.d/yellowtent
echo "==> Configuring collectd"
rm -rf /etc/collectd
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
systemctl restart collectd
echo "==> Configuring nginx"
# link nginx config to system config
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
ln -s "${DATA_DIR}/nginx" /etc/nginx
mkdir -p "${DATA_DIR}/nginx/applications"
mkdir -p "${DATA_DIR}/nginx/cert"
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
# remove old snapshots. if we do want to keep this around, we will have to fix the chown -R below
# which currently fails because these are readonly fs
echo "==> Cleaning up snapshots"
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
# restart mysql to make sure it has latest config
# wait for all running mysql jobs
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
while true; do
if ! systemctl list-jobs | grep mysql; then break; fi
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
if [[ -n "${arg_restore_url}" ]]; then
set_progress "30" "Downloading restore data"
echo "==> Downloading backup: ${arg_restore_url} and key: ${arg_restore_key}"
while true; do
if $curl -L "${arg_restore_url}" | openssl aes-256-cbc -d -pass "pass:${arg_restore_key}" | tar -zxf - -C "${DATA_DIR}/box"; then break; fi
echo "Failed to download data, trying again"
done
set_progress "35" "Setting up MySQL"
if [[ -f "${DATA_DIR}/box/box.mysqldump" ]]; then
echo "==> Importing existing database into MySQL"
mysql -u root -p${mysql_root_password} box < "${DATA_DIR}/box/box.mysqldump"
fi
fi
set_progress "40" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"fqdn": "${arg_fqdn}",
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo},
"database": {
"hostname": "localhost",
"username": "root",
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
},
"appBundle": ${arg_app_bundle}
}
CONF_END
# pass these out-of-band because they have new lines which interfere with json
if [[ -n "${arg_tls_cert}" && -n "${arg_tls_key}" ]]; then
echo "${arg_tls_cert}" > "${CONFIG_DIR}/host.cert"
echo "${arg_tls_key}" > "${CONFIG_DIR}/host.key"
fi
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
CONF_END
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
# during updates, do not trample mail ownership behind the the mail container's back
find "${DATA_DIR}/box" -mindepth 1 -maxdepth 1 -not -path "${DATA_DIR}/box/mail" -print0 | xargs -0 chown -R "${USER}:${USER}"
chown "${USER}:${USER}" "${DATA_DIR}/box"
chown "${USER}:${USER}" -R "${DATA_DIR}/box/mail/dkim" # this is owned by box currently since it generates the keys
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${DATA_DIR}"
echo "==> Adding automated configs"
if [[ ! -z "${arg_backup_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
if [[ ! -z "${arg_dns_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
if [[ ! -z "${arg_update_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
fi
if [[ ! -z "${arg_tls_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
set_progress "100" "Done"