This site is a blogging platform. It only shows my posts by default, but if you're signed in, you can post as well. I wrote this site from scratch in Python and TypeScript using Django and Vue. It's free software, and you can see the code on GitHub (that link is the current version of the code, see here for the version from when this post was written).
Before going further, I should note that for most use cases, I would not write a site like this from scratch. There are countless off-the-shelf solutions (even free ones) for this kind of site, which would take far less time and effort, would probably work better, and could be extended as needed.
I wrote this site because I'm between jobs and wanted to have some recent project to show off in line with my work (I'm a web developer, if you couldn't guess). It's also a fun way to experiment with different technologies that interest me. In short, I just wanted to have a website.
I'm a firm believer in test-driven development. I think that as much as possible, development should proceed by
To enable this, I set up an e2e
project in the repository which uses
Playwright to make browsers interact with the site and verify that
features work, from the user perspective, with as little knowledge as
possible of the code.
Once you have a failing e2e test, why is it failing? Is it trying to use a button that doesn't exist, is the data not being saved to the database? Whatever it is, once you figure it out, you can proceed down the stack and test more specifically. If a component needs to render a button, you can add a unit test that it does so, make that test pass, and then run the e2e test again to see what your next step is.
This process keeps you focused on real user requirements. Everything you are doing is in service of specified, user-facing features. As these tests accumulate, they are an executable specification that says whether all the features you designed still work. They will tell you if you accidentally break something, and if you're not sure why some code is there or if it's safe to remove, you can ideally just try removing it and seeing what tests break.
The cloud services the site uses are all provisioned via the Terraform
code in the infra
directory. This makes it so that that running
terraform apply
will provision resources for everything needed to
run the app and monitor it in production.
The site is hosted on AWS. I configured a VPC to contain a publically accessible EC2 instance connected to a private RDS instance running Postgres. The EC2 instance is available at a static Elastic IP address, and we configure DNS records in Cloudflare to point at it. Cloudflare also provides some protection against bots and DDoS attacks.
Terraform also sets up an SSH key for the EC2 instance and saves it to the local filesystem for easy access.
The EC2 instance just runs a 1-node Docker Swarm, and deploys our
production Docker images with docker stack
. In production, the
frontend container runs Nginx. It serves the compiled Vue app and
proxies API calls to the backend container.
We also provision a Grafana Cloud stack. This gives us a centralised place to monitor logs and metrics from across our services.
The Docker daemon on the EC2 instance is configured to push its logs to Amazon CloudWatch, where an AWS Lambda function then pushes them to Loki in Grafana Cloud.
The code is also instrumented with
OpenTelemetry which pushes traces to
Tempo in Grafana Cloud, which gives us an easy way to see how user
actions look across our whole system:
In the spirit of automation, I wrote a couple GitHub Actions workflows.
The first runs when code is pushed to any branch. It builds the Docker images, checks that the code lints without warnings, and runs all of the unit tests and e2e tests.
The second runs when the first one succeeds on main. It builds
production Docker images (which are smaller and do not have debugging
features, but also don't have the linter or unit test runners) and
pushes them to DockerHub. After this, it uses a script to generate our
updated Docker Stack config, and copies it and a .env
file to our
EC2 instance. Finally, it ssh
es into the EC2 instance to update the
Docker services and run migrations. We run containers for each service
on prod, so that we can do a rolling update to avoid downtime. At this
point, all updates are live on this site.
Hopefully some of this was enlightening to you in terms of technology or methodology. Feel free to comment and ask for clarification on anything.
This site remains under active development, so I don't know how well this description reflects the current state of things. The tagged git release linked at the start should be fully functional if you want to clone it and play with it, or even deploy it yourself.