Skip to content

Instantly share code, notes, and snippets.

@c-harding
Last active December 14, 2023 14:43
Show Gist options
  • Save c-harding/59686990f42f4dddae01dffa56618ea4 to your computer and use it in GitHub Desktop.
Save c-harding/59686990f42f4dddae01dffa56618ea4 to your computer and use it in GitHub Desktop.
EC2 for hosting multiple sites

EC2 for hosting multiple sites

This configuration can be used for hosting multiple sites on a single server, using docker and docker-compose for each site. Each site needs a unique port, and will be served on a unique domain. For SSL, use Cloudflare as a proxy.

Use EC2 to create a new instance. The image should be Amazon Linux 2 on ARM, running on a t4g.nano/t4g.micro/t4g.small. n.b. t4g.small is free for 750h/mo until the end of 2023. These cost $0.0042, $0.0084 and $0.0168 per hour respectively, or $3.066, $6.132 and $12.264 per month respectively. Use an Elastic IP address to maintain a constant IP address. You should expose only the ports 80 and 443 to the world (HTTP/S), and the port 22 (SSH) to yourself. The script can then be copied directly in to the shell when you are ssh’ed in.

Once a site has been started with docker-compose, exposing a port internally, use the command add-site domain-name port (e.g. add-site my.website.com 8080) to register this with Apache. This means that any requests to my.website.com will be forwarded to your container running on the given port (e.g. 8080). Note that the docker-compose file should include restart: always to ensure that it runs when the instance is restarted.

Use the command list-sites to list all registered sites. If you attempt to start two Docker projects with the same IP address, docker-compose will probably complain. However, add-site can be used to bind arbitrarily many domains to the same internal port.

In order to change the port for a given domain, simply call add-site again, and it will be overwritten.

To delete a site, delete the relevant file in /etc/httpd/conf.d/. E.g. to delete my.website.com, run sudo rm /etc/httpd/conf.d/domain-name.conf.

Web sockets are also supported, but please let me know if anything doesn’t work.

Deploying

Run the attached deploy script, with the following environment variables set. These variables can also be read from a .ec2-deploy file in the current directory (if not already set in the environment).

  • DOMAIN: The domain name that will be used to access the site
  • DEPLOY_HOST: The address used for connecting to the server push the changes
  • DEPLOY_USER: The user of the server. Optional
  • DEPLOY_KEYPAIR: The PEM file (ssh -i flag) for connecting to the server. Optional
  • DEPLOY_DIR: The path on the remote server to deploy under
  • SERVER_PORT: The port exposed by docker-compose. This must be unique among VMs on the server.
  • PORT_VARIABLE: The name of the environment variable used for passing $SERVER_PORT to the docker-compose command. E.g. If PORT_VARIABLE=APP_PORT, docker-compose.yaml could contain the entry services.app.ports of "${APP_PORT:-5000}:5000".
  • SOURCE_DIR: The local directory to deploy.
# Install Apache and Docker, and start their respoective processes
sudo yum install -y httpd docker perl rsync
sudo systemctl start httpd
sudo systemctl enable httpd
sudo usermod -a -G apache ec2-user
sudo chown -R ec2-user:apache /var/www
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker ec2-user
sudo systemctl restart docker
sudo rm /etc/httpd/conf.d/autoindex.conf /etc/httpd/conf.d/welcome.conf
sudo apachectl graceful
# Disable directory indexes from the webserver
sudo perl -pe 's/\s*\+?Indexes//g if /^\s*Options .*Indexes/' -i /etc/httpd/conf/httpd.conf
# Prevent a specific site from being served when the server is requested directly
sudo tee /etc/httpd/conf.d/\ default.conf > /dev/null << EOF
<VirtualHost *:80>
Options -Indexes
DocumentRoot /var/www/html
<Location />
Deny from all
Allow from none
</Location>
</VirtualHost>
EOF
sudo rm /etc/httpd/conf.d/autoindex.conf /etc/httpd/conf.d/welcome.conf
sudo apachectl graceful
# Compile and install docker compose
sudo curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Define the scripts
sudo tee /bin/add-site > /dev/null << 'EOS'
#!/bin/bash
DEPLOY_HOSTNAME="$1"
DEPLOY_PORT="$2"
if [[ $# -ne 2 ]]; then
echo "Usage: add-site hostname port" >&2
echo "e.g. add-site test.com 8080" >&2
exit 1
fi
CONFIG_FILENAME="/etc/httpd/conf.d/$DEPLOY_HOSTNAME.conf"
SAME_PATH='$1'
NEW_CONF=$(cat <<EOF
# add-site $DEPLOY_HOSTNAME $DEPLOY_PORT
<VirtualHost *:80>
ServerName $DEPLOY_HOSTNAME
RewriteEngine On
RewriteCond %{HTTP:Upgrade} =websocket [NC]
RewriteRule /(.*) ws://localhost:$DEPLOY_PORT/$SAME_PATH [P,L]
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule /(.*) http://localhost:$DEPLOY_PORT/$SAME_PATH [P,L]
ProxyPass / http://localhost:$2/
ProxyPassReverse / http://localhost:$2/
</VirtualHost>
EOF
)
if [ -f "$CONFIG_FILENAME" ] && [ "$NEW_CONF" == "$(cat "$CONFIG_FILENAME")" ]; then
echo "No change needed for Apache configuration"
elif sudo -n true 2>/dev/null; then
sudo tee "/etc/httpd/conf.d/$DEPLOY_HOSTNAME.conf" > /dev/null <<< "$NEW_CONF"
sudo apachectl graceful
echo "Updated Apache configuration"
else
echo "Permission denied, cannot update apache configuration"
exit 1
fi
EOS
sudo tee /bin/list-sites > /dev/null << 'EOS'
#!/bin/bash
head -n1 -q /etc/httpd/conf.d/*.conf | grep '^# add-site'
EOS
sudo chmod ugo+rx /bin/add-site
sudo chmod ugo+rx /bin/list-sites
#!/usr/bin/env bash
set -e
if [ -f .ec2-deploy ]; then
set -a
# Use .ec2-deploy file (but only if the values aren’t already set)
. /dev/stdin <<< "$(sed -n 's/^\([^#][^=]*\)=\(.*\)$/\1=${\1:-\2}/p' .ec2-deploy)"
set +a
fi
if [ -z "$DEPLOY_HOST" ]; then
echo "DEPLOY_HOST is not set, please add this to .env or provide it on the command line" >&2
exit 1
fi
if [ -z "$DEPLOY_USER" ]; then
DEPLOY_TARGET="$DEPLOY_HOST"
else
DEPLOY_TARGET="$DEPLOY_USER@$DEPLOY_HOST"
fi
if [ -z "$DEPLOY_KEYPAIR" ]; then
DEPLOY_FLAG=""
else
DEPLOY_FLAG="$(printf -- "-i%q" "$DEPLOY_KEYPAIR")"
fi
if [ "$0" = "$BASH_SOURCE" ]; then
ssh "$DEPLOY_FLAG" "$DEPLOY_TARGET" "$@"
fi
#!/usr/bin/env bash
set -e
. "$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"/connect
case $SERVER_PORT in
'') echo "SERVER_PORT is missing" >&2; exit 1 ;;
*[^0-9]*) echo "PORT_VARIABLE is set, but is not a valid bash identifier" >&2; exit 1 ;;
esac
case $PORT_VARIABLE in
'') PORT_ASSIGNMENT="" ;;
*[^A-Za-z0-9_]*) echo "PORT_VARIABLE is set, but is not a valid bash identifier" >&2; exit 1 ;;
*) PORT_ASSIGNMENT="$PORT_VARIABLE=$SERVER_PORT" ;;
esac
# On the server, start the docker process, clear old docker images and then set up port forwarding
# (assuming the server is set up as in this gist:
# https://gist.github.com/c-harding/59686990f42f4dddae01dffa56618ea4, if not then this stage is
# simply skipped).
(
RAW_DOMAIN="${DOMAIN/#*\/\//}"
DEPLOY_COMMAND="$(printf "
%s docker-compose -f%q/docker-compose.yml up --build -d &&
docker system prune -f &&
(command -v add-site > /dev/null && add-site %q %d; true)
" "$PORT_ASSIGNMENT" "${DEPLOY_DIR:?missing, this must be provided to specify where on the server to deploy to}" "$RAW_DOMAIN" "$SERVER_PORT")"
rsync --exclude=".ec2-deploy" -re"ssh $DEPLOY_FLAG" "${SOURCE_DIR:-.}" "$DEPLOY_TARGET":"$DEPLOY_DIR" &&
ssh "$DEPLOY_FLAG" "$DEPLOY_TARGET" "$DEPLOY_COMMAND"
)
@c-harding
Copy link
Author

TODO: add support for Cloudflare certificates, and disable HTTP connections. https://developers.cloudflare.com/ssl/origin-configuration/origin-ca

@c-harding
Copy link
Author

Todo: add notes about creating new users. Fairly simple:

sudo adduser bivouacker
sudo usermod -aG docker bivouacker
mkdir .ssh
chmod 700 .ssh
touch .ssh/authorized_keys
chmod 600 .ssh/authorized_keys
vim .ssh/authorized_keys

Add new keypair

@c-harding
Copy link
Author

For CentOS: sudo setsebool -P httpd_can_network_connect 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment