z-docker-esy
This example wraps a very simple Web app in a Docker container, and runs it persistently, as a daemon.
let () =
Dream.run ~interface:"0.0.0.0"
@@ Dream.logger
@@ Dream.router [
Dream.get "/" (fun _ ->
Dream.html "Dream started by Docker Compose, built with esy!");
]
It uses Docker Compose, so that you can quickly expand it by adding databases and other services.
version: "3"
services:
web:
build: .
ports:
- "80:8080"
restart: always
logging:
driver: ${LOGGING_DRIVER:-json-file}
The example app is running live at http://docker-esy.dream.as.
The setup can be run locally or on any server provider. We will use a Digital Ocean "droplet" (virtual machine). The server binary is built by Docker.
The
Dockerfile
has two stages: one for building our application, and one for the runtime that
only contains the final binary and its run-time dependencies.
Droplet setup
Visit Digital Ocean and create an account, then create a droplet. Simple Dream apps can be built on the smallest and cheapest droplets as of May 2021, but you may eventually need more memory. Be sure to enable monitoring and add your public SSH key.
Once the droplet starts, Digital Ocean will display its IP. We will use
my-droplet
as a stand-in in the example.
SSH into your droplet:
$ ssh root@my-droplet
Then, update the droplet:
$ apt update
$ apt upgrade -y
There was likely a kernel update, so restart the droplet:
$ init 6
$ ssh root@my-droplet
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
$ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
$ apt update
$ apt install docker-ce -y
Install Docker Compose. Check here for the latest available release.
$ curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64 -o /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose
At this point, you may want to create a non-root user for running your builds,
We add this user to the docker
group, so that it can build and start Docker
containers, and give it the same SSH public key:
$ adduser build --disabled-password
$ usermod build --append --groups docker
$ usermod build --append --groups systemd-journal
$ mkdir /home/build/.ssh -m 700
$ cp .ssh/authorized_keys /home/build/.ssh/
$ chown -R build:build /home/build/.ssh
Droplet setup is now complete:
$ exit
Deploy
To deploy to the droplet, we send the sources over, and trigger the commands
in
deploy.sh
remotely:
$ rsync -rlv . build@my-droplet:app --exclude _esy --exclude node_modules
$ ssh build@my-droplet "cd app && bash deploy.sh"
deploy.sh
looks like this:
#!/bin/bash
set -e
set -x
docker-compose build
docker-compose down
docker-compose up --detach
The app should now be publicly accessible at the droplet's IP. Logs can be viewed with
$ ssh build@my-droplet "journalctl -f"
Automation
The Dream repo has a GitHub action that deploys this example to docker-esy.dream.as on every push. It runs the two commands above.
The action needs SSH access to the droplet. See
Automation in
z-systemd
for discussion. The only difference
is that we need don't need to upload the SSH key to user root
, because we
don't need to log in as root
to start a daemon:
$ ssh-keygen -t rsa -b 4096 -f github-actions
$ ssh build@my-droplet "cat - >> .ssh/authorized_keys" < github-actions.pub
And this example uses a known_hosts
secret named
DIGITALOCEAN_DOCKER_ESY_KNOWN_HOSTS
rather than
DIGITALOCEAN_SYSTEMD_KNOWN_HOSTS
, but you can pick any name you like for your
version of the deploy script.
See also:
z-docker-opam
is a variant of this example that uses opam instead of esy.z-systemd
packages the app as a systemd daemon, outside of a Docker container.z-heroku
deploys the app to Heroku.