Charlesworth AI, 15 Sept 2026
If you have read the About section, you already know why this web app came about. I had been thinking about building it for a few years, but never really had the time to make it a reality. Fortunately, now I do, so I have built it as part of my portfolio.
I am using pre-trained open-source models and MongoDB Labs' Full Stack FARM application as the starting point for the web stack, with AWS Free Tier services for hosting and storage.
Having recently acquired an old Dell PowerEdge T610 server, I am also hoping to experiment with migrating some of the storage and processing away from AWS and onto my own hardware. The server is old, only takes DDR3 RAM and certainly isn't an AI workstation, so I don't expect self-hosting all of the ML workflows to be particularly practical. Nevertheless, part of the fun is finding out where its limits actually are.
Below is an explanation of the stack. It is based on FARM, with several additions to handle background processing, routing, monitoring and the machine learning workflows. I've also included some of my own experience with each technology and why I chose to use it.
I was originally a Flask user. I liked using it and had a lot of fun creating applications with Jinja2 templates, while documenting them with Sphinx and the classic Read the Docs theme.
Then FastAPI came along. It was fast, supported async from the outset, made good use of Python type hints and generated interactive OpenAPI documentation automatically. It quickly became my go-to framework for building APIs.
That said, Flask still has its place. In fact, I use a Flask service within the wider Vanlife Stories architecture to host the anonymisation model. The model is an older component that depends on older versions of Python and its machine learning libraries, so isolating it behind its own small service is considerably cleaner than forcing those dependencies into the main application.
This is one of the things I like about building applications as separate services. Not every part of the system has to use the newest technology, or even the same technology. It just needs a clean interface with the rest of the application.
I was encouraged to start using React by an ex-colleague and friend who is a very strong Python engineer.
Where we worked, I would build POCs and use Python for almost everything, including the front end. Initially that meant Plotly Dash and later Streamlit, once it had matured enough for the applications I wanted to build.
Dash was particularly useful because customising applications properly required me to learn HTML and CSS rather than hiding them completely. That knowledge turned out to be useful elsewhere, particularly when building scrapers and bots with Scrapy, and eventually gave me a much better foundation for moving into JavaScript, React and TypeScript.
The actual transition from Python front ends to React and TypeScript happened after I left that job, but the experience there, and particularly the encouragement from my former colleague, pointed me in that direction.
For Vanlife Stories, React also makes sense because the front end is much more than a dashboard. Maps, image browsing, filtering and interactive components are central to the application, and I wanted the front end and API to remain clearly separated.
I started using MongoDB in my last non-contract role. Before that, I had almost always worked with SQL databases.
SQL absolutely has its place, and I would choose a relational database when the data and application call for one. But during exploratory data science and application development, I often prefer the flexibility of a document database.
Vanlife Stories deals with photos, locations, metadata and outputs generated by different models. Those records can evolve as new capabilities are added, which makes MongoDB a natural fit. I can add new metadata without having to redesign a relational schema every time an experiment introduces another field.
That flexibility shouldn't be confused with having no structure at all. Once an application matures, schemas and validation still matter. FastAPI and Pydantic provide a useful boundary here, allowing the API to enforce the structure it expects while MongoDB retains flexibility underneath.
The wider MongoDB ecosystem was also part of the attraction. MongoDB Labs' Full Stack FastAPI MongoDB project gave me a useful starting architecture rather than having to assemble every part of the application from scratch.
FARM provides the core of the application, but Vanlife Stories has workloads that shouldn't happen inside a normal API request.
Uploading a photograph is one thing. Running models against it, extracting information and processing potentially large batches of images is another.
For that reason, the application adds several components around the core FARM stack.
The entire project is containerised with Docker.
I use Docker extensively because I want an application to behave consistently regardless of whether I am running it locally, deploying it to AWS or experimenting with running parts of it on my old PowerEdge server.
It is particularly useful for this project because the application isn't really one application. There is the FastAPI backend, React front end, MongoDB, RabbitMQ, Celery workers and supporting services. Some components also have very different dependencies, particularly the older anonymisation model.
Containers give each of those components its own environment while Docker Compose allows me to bring the wider application up as a single system.
It also means I can experiment. If I decide that a particular workload belongs on the PowerEdge while the public-facing application remains in AWS, I don't have to redesign the application to make that possible.
Some of the work Vanlife Stories performs can take considerably longer than a normal web request should.
When a user uploads images, I don't want the FastAPI application sitting there waiting while machine learning models process them. The API should accept the work, respond quickly and allow that processing to happen elsewhere.
RabbitMQ acts as the message broker between the application and the workers doing that processing.
Instead of asking FastAPI to perform a long-running task directly, the application places a message onto a queue. A worker can then pick up that task and process it independently.
This also creates a useful separation between the web application and the data science workloads. If image processing becomes more demanding, I can add or move workers without necessarily changing the API itself.
Celery provides the worker system behind those background jobs.
This is where the web development and data science sides of the project really start to meet. FastAPI handles the application and user requests, while Celery handles work that is slower, computationally expensive or simply doesn't need to happen synchronously.
For example, an uploaded batch of photographs can be queued for processing rather than forcing the user to wait for every model to finish before the API responds.
It also gives me options for where that work happens. A Celery worker doesn't necessarily have to run on the same machine as FastAPI. That becomes particularly useful as I experiment with the PowerEdge server and potentially other hardware for running data science workloads.
Flower sits alongside Celery and gives me a web interface for seeing what the workers are doing. I can see active and completed tasks, inspect failures and monitor workers without having to work everything out from logs.
Celery does the work. Flower lets me see what Celery is doing.
With several services running independently, something needs to sit in front of them and decide where incoming traffic should go.
That is Traefik's job.
Traefik acts as the reverse proxy and router for the application. Requests can be directed to the appropriate service without exposing every container directly to the outside world.
One of the reasons I like Traefik in a Docker-based environment is that routing can be configured alongside the services themselves. As containers are added or changed, the routing configuration can evolve with them rather than becoming a completely separate piece of infrastructure.
It also handles things such as HTTPS termination, allowing the individual application services to remain focused on what they are supposed to do.
There is a big difference between an application working on my laptop and knowing whether it is still working once somebody else starts using it.
For that, I use Sentry.
Sentry provides application monitoring and error tracking across the deployed application. If something fails, I want more than a generic 500 error and a user telling me that "it doesn't work". I want the exception, stack trace and enough context to understand what actually happened.
This becomes even more important with background processing. Failures don't necessarily happen while somebody is looking at a page. A Celery task might fail several minutes after an image was uploaded, so having proper error reporting makes those failures much easier to find and diagnose.
There are probably simpler ways to build Vanlife Stories.
I could have built the entire application in Streamlit, put everything into one Python application and had something working much sooner. For a POC, that might even have been the right decision.
But this project has another purpose.
It is partly something I wanted for myself, partly something I wanted to share, and partly an opportunity to build a complete data science application rather than stopping when the model works.
That distinction matters to me.
Data science doesn't end with a Jupyter notebook and a model file. Somebody eventually has to use what you have built. That means APIs, interfaces, infrastructure, monitoring, background processing, deployment and all the less glamorous engineering that turns an experiment into an actual product.
Vanlife Stories gives me somewhere to bring all of those pieces together.
And, at the end of it, I can finally find that photograph I was looking for before the conversation moves on.