One of the major benefits of containerization is that it provides a standard interface that others can use to design their systems. Because of this, TONS of different options are available for deploying containers to the cloud.
Within AWS alone, Corey Quinn (from the Duckbill Group) noted there are 17 unique ways to run containers (https://www.lastweekinaws.com/blog/the-17-ways-to-run-containers-on-aws/). That was in 2021... the number has probably gone up by now!
We will be deploying in 3 different ways:
Railway is a relatively new infrastructure company focused on making it as easy as possible for users to deploy applications. They offer automated deployments from GitHub, an intuitive user interface, and even created a technology called Nixpacks (https://nixpacks.com/) which enable you to create a container image automagically (without the need for a Dockerfile). For some situations the nixpacks work seamlessly (for the golang api it worked), for others it can be necessary to specify your own Dockerfile so that you have full control.
Unfortunately there is no way (in Feb 2023) to specify a Docker build context other than the location of the Dockerfile. Also, their build environment didn't support using --mount=type=cache
. Because of this I created a Dockerfile.railway
in the 06-building-container-images
directory. To prevent this cluttering the repo, I only included this on the railway
git branch.
git fetch && git checkout railway # fetch and checkout the railway branch
The deployment configuration is specified via railway.toml
files, also included on the railway
git branch.
A specific nginx-railway.conf
is also used in order to specify public domains of the apis. Within docker compose we use Docker's internal DNS on our bridge network, but Railway doesn't (yet) support private networking so we use the public domains.
The only other consideration is setting the PORT
environment variable for each service (80 for nginx, 3000 for node, and 8080 for golang).
When we provision a PostgresDB within the project, Railway automatically sets the necessary DATABASE_URL
environment variable for the other services.
At this point, we already have a docker compose
file which allows us to specify our application using a single command. There are a few limitations with docker compose that make it less than ideal to run a production application with docker compose directly:
- No support for secrets (we are passing our DB credentials as environment variables)
- No support for zero downtime re-deployments
Luckily, docker swarm does provide those things and setting up a single node cluster can be done with one command docker swarm init
. Making just a few modifications to the docker compose file (adding deploy
configurations, passing sensitive info as secrets and reading those data as files within the applications) it is ready to deploy.
- Create a virtual machine with your favorite cloud provider. Make sure to set up the firewall to listen on ports 80, 443, and 22.
- Use the script at https://get.docker.com/ to install docker engine. If managing docker as a non-root user, you may need to follow these steps: https://docs.docker.com/engine/install/linux-postinstall/ after installing.
- Set the
DOCKER_HOST
environment variable in the Makefile toUSERNAME@IP_ADDRESS
of your virtual machine (this will allow your local docker client to use the remote docker daemon!) - Build and push the container images to a registry
- Populate the secrets by running
make create-secrets
- Deploy the application by running
make swarm-deploy-stack
(uses thedocker stack deploy
command under the hood) - Set up a DNS A record to route traffic to your VM (or access using the IP address)
I was planning to only include the Railway + Swarm examples, but figured a course about containers wouldn't be complete without at least mentioning Kubernetes, the most popular container orchestrator today. Like Docker Swarm, Kubernetes is designed to schedule and run your containers, but has more maturity when it comes to cloud provider support.
I created the necessary resource yaml files in ./kubernetes
to deploy the application. You will notice that it is somewhat more verbose than the swarm specification.
- Create a kubernetes cluster with your favorite cloud provider.
- Set up kubectl to connect to the cluster (using the cloud provider instructions)
- Install Traefik ingress controller by running
make install-traefik
(uses this helm chart https://github.com/traefik/traefik-helm-chart) - Install Postgres by running
make install-postgres
(uses this helm chart https://github.com/bitnami/charts/tree/main/bitnami/postgresql) - Deploy the application by running
make deploy-app
- Set up a DNS A record to route traffic to the IP address of the load balancer that traefik provisions