skip to content
Aaron Becker

Deploying Astro with Docker Compose and Traefik

/ 4 min read

In my last post, I outlined how I optimized my Astro blog’s build process to compress static files with brotli compression. This post outlines how I serve my blog from a docker container at aaronjbecker.com using docker compose and traefik, as well as how I deploy it using a bare-bones bash script with SSH agent forwarding.

This post assumes you already have a server running docker compose, as well as a Dockerfile to build and serve your Astro site. I’ll be using my own configuration for this site as an example.

step 1: configure container infrastructure

I use docker compose to build and run the container for this blog, which allows me to attach configuration labels for traefik. Traefik is an open-source tool that makes it possible to host arbitrarily many websites on a single server, with each running as a separate container. Traefik routes incoming requests to the appropriate container and automatically generates and renews SSL certificates so the sites are served over HTTPS1.

Here’s my docker-compose.yml file, which lives in the blog’s root directory:

docker-compose.yml
# blog container is only exposed on the `traefik` network,
# which is created in conjunction with the traefik container.
networks:
traefik:
external: true
services:
ajb_blog:
container_name: ajb_blog
restart: unless-stopped
build:
context: .
networks:
- traefik
labels:
- traefik.enable=true
- "traefik.http.routers.ajb_blog.rule=Host(`aaronjbecker.com`) || Host(`www.aaronjbecker.com`)"
# redirect http to https (configured in traefik.yml)
- "traefik.http.routers.ajb_blog.entrypoints=websecure"
# generate and renew HTTPS certificates with traefik
- "traefik.http.routers.ajb_blog.tls.certResolver=myResolver"
# redirect www to non-www
- "traefik.http.routers.ajb_blog.middlewares=redirect-www@file"

step 2: deploy!

First, some background on my codebase: since I’m a solo developer at the moment, I can get away with keeping all of the projects I’m working on in a single git monorepo. Being a solo developer also means that I don’t have much to gain from using a CI/CD pipeline, so I deploy directly from my git repository by manually running bash scripts when I have changes I want to deploy.

Since I don’t want to keep my GitHub SSH key on my server, I use SSH agent forwarding, which allows me to pull changes from my repository on my server using keys stored on my local machine. I use a separate SSH key to connect to my server, which provides some degree of security isolation.

Here’s the bash script I use to deploy my blog, which lives in a directory with other scripts I use to deploy other projects:

deployBlog.sh
#!/bin/bash
# Script to deploy blog updates to the server automatically
# Usage: ./deployBlog.sh or `bash deployBlog.sh`
# Get current branch name
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Deploying branch: $CURRENT_BRANCH"
# Load SSH keys (git and docker server)
ssh-add ~/.ssh/id_rsa
ssh-add ~/.ssh/id_ed25519_ovh
# Remote commands to execute on the server
ssh -A SERVER_USERNAME@SERVER_IP_ADDRESS << EOF
# navigate to directory with monorepo
cd ~/aaronjbecker.com
# fetch and checkout the current branch
git fetch
git checkout $CURRENT_BRANCH
git pull
# rebuild the docker stack for the blog; explicit down is not needed.
cd astro_blog
docker compose up -d --build --remove-orphans
EOF
echo "Deployment complete!"

Some details like my server’s IP address and root username are redacted for obvious reasons. I have the script configured to checkout and pull whichever branch I’m working on locally so that I don’t have to create a pull request to my main branch if I’m just testing something out.

closing thoughts

Although this may mark me as crazy, I genuinely like working with docker, even if it’s overkill for this blog. My setup makes it possible to securely and efficiently host multiple websites on a single low-cost server, with each running as its own docker-compose stack.

Traefik is very good at SSL termination and routing, and docker-compose makes it easy to insert additional components to address gaps in Traefik’s capabilities, like a varnish proxy to handle caching backend responses.

As for bash scripts, they can often be illegible, but that’s less of a problem in this new age of AI-assisted coding. I like that bash scripts work on both of my development machines, a MacBook Pro and an Ubuntu desktop, without any additional setup. And I can take comfort in the fact that shell scripting isn’t going anywhere anytime soon.

Footnotes

  1. I also run the traefik reverse proxy container with docker-compose. Configuration for that is beyond the scope of this post but here’s a tutorial from the traefik team if you’re interested.