Published: Jan 16, 2025 by Isaac Johnson
Let’s talk about another hot AI Code Generation tool, Lovable. This is a relatively new tool using AI to build out full solutions with push-button deploys.
I’m interested to compare this with other tools like Vercel’s V0 and that which we can get with Gemini Code Assist and Copilot. Let’s dig in and see what it can do.
History
Founded in Stockholm Sweden, Lovable is basically a relaunch of gptengineer.app. It’s now, as of two weeks ago, officially branded after the URL and called Lovable.dev (read more on LinkedIn)
Anton Osika and Fabian Hedin are the co-founders of Lovable likely met at Depict where Fabien was a Frontend Lead and Anton was the CTO and Co-founder.
Lovable setup and pricing
We can create an account or sign in from the main page of Lovable.dev
For IdPs, I can create a unique account or use Google or Github
For the free or trial plan, we can see we get five messages a day
The starter plan, which is US$20/mo gives use 100 messages a month. But I’m confused by this as I get 5 a day with free. So does that mean 5 free a day (31x5=165 + 100 for 265?) or is it instead I get 100 but i could use 50 in a day?
I’m not thumbing my nose at this - I’m just saying the pricing is confusing to me.
Comparisons
Bolt.net has pricing based around “tokens per month”.
So the free tier gives me 1Million, the Pro 10M and so on
Vercel’s v0 pricing seems to just limit us on number of projects, chat speed and attachment size
(honestly, that attachment size will likely be the thing to get me to pay for it at some point)
Then cheapest, in my mind is Copilot, which is not the same thing, but gives me a lot of similar features and that is US$10/month
While Gemini Code Assist pricing does not have a free tier shown.
All I can say is it doesn’t have a free tier yet. No one has shared NDA things with me, however, I got a lot of hints lately they have a response in the works for Copilot Free.
Copilot setup
With “Copilot for free”, I get 2000 code completions and 50 chat messages per month
And as we see, for $10/month we move that to unlimited.
I’m not saying the two are equal - it’s just interesting in pricing compared to offering.
Again, let’s compare to Cursor which is 2000 completions (?ever ?month) and 50 “slow” requests but moving to US$20/month and we are at unlimited completions and 50 fast requests (and unlimited “slow”).
And lastly, best I can tell, v0 only limits me on total projects (which I can delete) - so ?unlimited.
And I can go to faster higher limits and attachments (actually that attachment size would be nice) with Premium at US$20/month
Now, we must consider a bit of motivations around pricing - Vercel likely gives us low prices on v0 since they really want us to host on Vercel which, if we build a full featured app, would be an obvious choice.
Usage
Let’s try and build on our last app by asking Lovable to make a Timeline view
Here we can see the image I’ll feed it for a basic wireframe of what i want
My hope was to feed it the JSON files we already had to make it easier to migrate to our existing structure, but sadly the attachments only allow JPG and PNG files
I really want to enable this to succeed with the fewest prompts possible. Here is what I ended up with for a prompt:
Create and implement a user interface for a Resume app that is as similar as possible to the attached image. This screenshot shows a layout issue on mobile. Adjust margins and padding to make it responsive while maintaining the same design structure. The data for the app should come from experiences in a JSON format with fields companyName, companyLogo, location, startDate, title, categories, description and user details in a JSON format with fields firstName, lastName, title. email, phone, addres, photo. The values are all strings. The values in companyLogo and photo are URLs to images. Include a Dockerfile that can build and expose the app on port 3000.
Let’s see how it handles it
While I keep getting errors, I don’t want to neccessarily attribute that to Lovable. I’m in a spotty wifi area as I write presently and it could be me at fault here
I tried other orgs but they also errored.
Follow-up: It just so happened I was trying this at the same time Github was having some serious issues which was the cause of the problem
Deploy
Let’s see where we can get with “Deploy”. In the free tier, we can only publish to Public
This built into https://cv-optimizer.lovable.app/
Firefox also errored. I moved to Google since that rarely has issues, at least for an IdP.
That suggested a malformed request
Being stuck
Here is where I am just stuck. I can view some of the files
But I cannot view just the code, or download it. There is no editor from what I can see. Everything about Edit requires Github and if that is down, we are stuck.
What if I am in a slow location? What if I chose to use Gitlab, or Forgejo or Gitea or Codeberg. I might even want to build and edit in Azure DevOps with Azure Repos or just use my own local git with GIT SSH.
The assumption that it’s Github (or nothing) is a real limiter for something that, at least at a first pass, looks really nice.
Try again
You’ll note that even if I could connect to Github, that App looked nothing like my image and certainly was not a timeline view.
Let’s try a simpler prompt
This did use Pins for icons, so I can see how that is similar but it is not what I wanted
I tried a new user, new Github Identity and the public wifi to take my hotspot out of the equation and it still errored on me
Now when I check Github Status it would appear things have gone to poo there at the moment
So this could just be bad timing on my part - but still, locking us in to only one provider and giving no editor or zip download really blocks us for doing anything about it.
I should say that coming back to ask about making it horizontal did seem to update the view a bit
Github
After a lot of tries and after Github Status showed all green, I finally got it to connect
I did a “Create in” my own area
which now has a URL to clone
Code
We can see the Tech stack from Readme.md
This project is built with .
- Vite
- TypeScript
- React
- shadcn-ui
- Tailwind CSS
By default, it is private, but I can change it to public so you can view it here.
I can see the files:
I cloned and built it locally
Let’s now test a local run
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/01/09 03:07:28 [notice] 1#1: using the "epoll" event method
2025/01/09 03:07:28 [notice] 1#1: nginx/1.27.3
2025/01/09 03:07:28 [notice] 1#1: built by gcc 13.2.1 20240309 (Alpine 13.2.1_git20240309)
2025/01/09 03:07:28 [notice] 1#1: OS: Linux 5.15.167.4-microsoft-standard-WSL2
2025/01/09 03:07:28 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2025/01/09 03:07:28 [notice] 1#1: start worker processes
2025/01/09 03:07:28 [notice] 1#1: start worker process 29
2025/01/09 03:07:28 [notice] 1#1: start worker process 30
2025/01/09 03:07:28 [notice] 1#1: start worker process 31
2025/01/09 03:07:28 [notice] 1#1: start worker process 32
2025/01/09 03:07:28 [notice] 1#1: start worker process 33
2025/01/09 03:07:28 [notice] 1#1: start worker process 34
2025/01/09 03:07:28 [notice] 1#1: start worker process 35
2025/01/09 03:07:28 [notice] 1#1: start worker process 36
2025/01/09 03:07:28 [notice] 1#1: start worker process 37
2025/01/09 03:07:28 [notice] 1#1: start worker process 38
2025/01/09 03:07:28 [notice] 1#1: start worker process 39
2025/01/09 03:07:28 [notice] 1#1: start worker process 40
2025/01/09 03:07:28 [notice] 1#1: start worker process 41
2025/01/09 03:07:28 [notice] 1#1: start worker process 42
2025/01/09 03:07:28 [notice] 1#1: start worker process 43
2025/01/09 03:07:28 [notice] 1#1: start worker process 44
172.17.0.1 - - [09/Jan/2025:03:07:30 +0000] "GET / HTTP/1.1" 200 647 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [09/Jan/2025:03:07:30 +0000] "GET /assets/index-Cc8Na4BH.js HTTP/1.1" 200 306841 "http://localhost:3033/"
which works great
Exposing in Kubernetes
Let’s first to dockerhub
builder@DESKTOP-QADGF36:~/Workspaces/cv-optimizer$ docker tag cv-optimizer:0.1 idjohnson/cv-optimizer:0.1
builder@DESKTOP-QADGF36:~/Workspaces/cv-optimizer$ docker push idjohnson/cv-optimizer:0.1
The push refers to repository [docker.io/idjohnson/cv-optimizer]
4cb6588057e6: Pushed
5ea0c9f16ce9: Pushed
5a760029e979: Mounted from library/nginx
23625999797d: Mounted from library/nginx
9aa22afcf27f: Mounted from library/nginx
59a5cb944b91: Mounted from library/nginx
598e577f3a23: Mounted from library/nginx
fd5f65a144ef: Mounted from library/nginx
a8903c9578e9: Mounted from library/nginx
ce5a8cde9eee: Mounted from library/nginx
0.1: digest: sha256:7252de46872656ff39da89a724c4f1ad488e5ce1ef0f8dde1c75fd44824190fe size: 2406
I’m going to use the Mistral LLM on a local Ollama to create a kubernetes manifest (i replaced the markdown backticks with dashes)
builder@bosgamerz9:~$ ollama run mistral
>>> create a kubernetes YAML manifest with a deployment of the image idjohnson/cv-optimizer:0.1 that exposes port 3000 and a service that exposes port
... 80 using ClusterIP
Here's a simple Kubernetes YAML manifest for a Deployment and Service to run the `idjohnson/cv-optimizer:0.1` image, exposing port 3000 on the
pod and creating a ClusterIP service to expose port 80.
'''yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cv-optimizer-deployment
spec:
replicas: 3
selector:
matchLabels:
app: cv-optimizer
template:
metadata:
labels:
app: cv-optimizer
spec:
containers:
- name: cv-optimizer
image: idjohnson/cv-optimizer:0.1
ports:
- containerPort: 3000
name: http
---
apiVersion: v1
kind: Service
metadata:
name: cv-optimizer-service
spec:
selector:
app: cv-optimizer
type: ClusterIP
ports:
- protocol: TCP
port: 80
targetPort: 3000
'''
This YAML file contains two resources, a Deployment and Service. The Deployment will create three pods with the `idjohnson/cv-optimizer:0.1`
image, listening on port 3000 within each pod. The Service, of type ClusterIP, routes traffic to these pods via port 80, allowing communication
between them only within the cluster.
You can apply this YAML file using `kubectl apply -f <filename>` in your terminal.
I can now test it locally
builder@DESKTOP-QADGF36:~$ kubectl apply -f ./test.yaml
deployment.apps/cv-optimizer-deployment created
service/cv-optimizer-service created
I can now test
builder@DESKTOP-QADGF36:~$ kubectl port-forward svc/cv-optimizer-service 3034:80
Forwarding from 127.0.0.1:3034 -> 3000
Forwarding from [::1]:3034 -> 3000
Handling connection for 3034
Handling connection for 3034
Exposing with Ingress and TLS
I’ll create an A Record in Azure DNS
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n cv-optimizer
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "b079c063-ad3d-49d2-9270-c47b73a68448",
"fqdn": "cv-optimizer.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/cv-optimizer",
"name": "cv-optimizer",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"trafficManagementProfile": {},
"type": "Microsoft.Network/dnszones/A"
}
Now let’s expose it
$ cat test.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
name: cv-optimizeringress
spec:
rules:
- host: cv-optimizer.tpk.pw
http:
paths:
- backend:
service:
name: cv-optimizer-service
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- cv-optimizer.tpk.pw
secretName: cv-optimizer-tls
$ kubectl apply -f ./test.ingress.yaml
ingress.networking.k8s.io/cv-optimizeringress created
When the cert is ready
$ kubectl get cert cv-optimizer-tls
NAME READY SECRET AGE
cv-optimizer-tls True cv-optimizer-tls 88s
We can now test at https://cv-optimizer.tpk.pw/
Using Copilot Free
I’ll next pull the code down locally and launch VS Code
builder@LuiGi:~/Workspaces$ git clone https://github.com/idjohnson/cv-optimizer
Cloning into 'cv-optimizer'...
remote: Enumerating objects: 106, done.
remote: Counting objects: 100% (106/106), done.
remote: Compressing objects: 100% (83/83), done.
remote: Total 106 (delta 16), reused 106 (delta 16), pack-reused 0 (from 0)
Receiving objects: 100% (106/106), 381.72 KiB | 3.15 MiB/s, done.
Resolving deltas: 100% (16/16), done.
builder@LuiGi:~/Workspaces$ cd cv-optimizer/
builder@LuiGi:~/Workspaces/cv-optimizer$ code .
The older VS Code’s had you install an extension to use Copilot but if you launched a current version, you’ll now see the Copilot icon up near the middle top. Clicking the drop-down will show “Use AI Features with Copilot for Free”.
I can then enable by clicking the button
In one case I had VS Code prompt me to continue
and in another it just prompted me to sign-in (with full MFA of course)
Updating
One of the issues I see right away is that this Dockerfile builds a static Vite app
If I hop on a pod, for instance we can see it is just a basic index.html hosted by Nginx and some very large Javascript files
/usr/share/nginx/html/assets # ls
index-C_nyZYQl.css index-Cc8Na4BH.js
/usr/share/nginx/html/assets # cd ..
/usr/share/nginx/html # cat index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>cv-optimizer</title>
<meta name="description" content="Lovable Generated Project" />
<meta name="author" content="Lovable" />
<meta property="og:image" content="/og-image.png" />
<script type="module" crossorigin src="/assets/index-Cc8Na4BH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C_nyZYQl.css">
</head>
<body>
<div id="root"></div>
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
</body>
</html>
/usr/share/nginx/html # cat assets/index-C
index-C_nyZYQl.css index-Cc8Na4BH.js
/usr/share/nginx/html # cat assets/index-C
index-C_nyZYQl.css index-Cc8Na4BH.js
/usr/share/nginx/html # cat assets/index-Cc8Na4BH.js
var ac=e=>{throw TypeError(e)};var qs=(e,t,n)=>t.has(e)||ac("Cannot "+n);var N=(e,t,n)=>(qs(e,t,"read from private field"),n?n.call(e):t.get(e)),q=(e,t,n)=>t.has(e)?ac("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),W=(e,t,n,r)=>(qs(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),Te=(e,t,n)=>(qs(e,t,"access private method"),n);var ni=(e,t,n,r)=>({set _(o){W(e,t,o,n)},get _(){return N(e,t,r)}});function hv(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&&!Array.isArray(r)){for(const o in r)if(o!=="default"&&!(o in e)){const i=Object.getOwnPropertyDescriptor(r,o);i&&Object.defineProperty(e,o,i.get?i:{enumerable:!0,get:()=>r[o]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(o){if(o.ep)return;o.ep=!0;const i=n(o);fetch(o.href,i)}})();function af(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var uf={exports:{}},ys={},cf={exports:{}},Y={};/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
...
If we scroll near the bottom of the logs we can see some of our address and work history details buried in there
The question becomes - can Copilot pull that out to runtime instead of compile time?
Let’s try together:
It was interesting there at the end that when I was about to nudge it to update the tsx file for the Dockerfile path, upon accepting the proposed change, it corrected it automatically
I tried to build and test
builder@LuiGi:~/Workspaces/cv-optimizer$ docker run -p 8034:3000 cv-optimizer:0.2
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/01/10 00:30:05 [notice] 1#1: using the "epoll" event method
2025/01/10 00:30:05 [notice] 1#1: nginx/1.27.3
2025/01/10 00:30:05 [notice] 1#1: built by gcc 13.2.1 20240309 (Alpine 13.2.1_git20240309)
2025/01/10 00:30:05 [notice] 1#1: OS: Linux 5.15.167.4-microsoft-standard-WSL2
2025/01/10 00:30:05 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2025/01/10 00:30:05 [notice] 1#1: start worker processes
2025/01/10 00:30:05 [notice] 1#1: start worker process 29
2025/01/10 00:30:05 [notice] 1#1: start worker process 30
2025/01/10 00:30:05 [notice] 1#1: start worker process 31
2025/01/10 00:30:05 [notice] 1#1: start worker process 32
2025/01/10 00:30:05 [notice] 1#1: start worker process 33
2025/01/10 00:30:05 [notice] 1#1: start worker process 34
2025/01/10 00:30:05 [notice] 1#1: start worker process 35
2025/01/10 00:30:05 [notice] 1#1: start worker process 36
2025/01/10 00:30:05 [notice] 1#1: start worker process 37
2025/01/10 00:30:05 [notice] 1#1: start worker process 38
2025/01/10 00:30:05 [notice] 1#1: start worker process 39
2025/01/10 00:30:05 [notice] 1#1: start worker process 40
2025/01/10 00:30:05 [notice] 1#1: start worker process 41
2025/01/10 00:30:05 [notice] 1#1: start worker process 42
2025/01/10 00:30:05 [notice] 1#1: start worker process 43
2025/01/10 00:30:05 [notice] 1#1: start worker process 44
172.17.0.1 - - [10/Jan/2025:00:30:18 +0000] "GET / HTTP/1.1" 200 647 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [10/Jan/2025:00:30:19 +0000] "GET /assets/index-TkCKc56C.js HTTP/1.1" 200 307027 "http://localhost:8034/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [10/Jan/2025:00:30:19 +0000] "GET /assets/index-C_nyZYQl.css HTTP/1.1" 200 56963 "http://localhost:8034/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [10/Jan/2025:00:30:19 +0000] "GET /usr/share/nginx/html/experiences.json HTTP/1.1" 200 647 "http://localhost:8034/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [10/Jan/2025:00:30:19 +0000] "GET /favicon.ico HTTP/1.1" 200 15086 "http://localhost:8034/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
172.17.0.1 - - [10/Jan/2025:00:30:35 +0000] "GET /assets/index-C_nyZYQl.css HTTP/1.1" 304 0 "http://localhost:8034/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" "-"
But clearly it is trying to pull from local path as a URL in the browser
I don’t need an AI to show me how that is off and what it should be
I fixed the line and built again
This now gives me a whole new errored
Copilot had some more suggestions
I worked this for a while, but in the end, the gptengineer.js
file, even when copied locally, has way too many hardcoded network paths
Try again
I went back to Lovable.dev to ask for another report, but this time I wanted to be exacting
I first uploaded some example files to a shared URL
Using the prompt:
Create a resume app with a horizontally displayed timeline. It should consume the users details from a public endpoint (https://freshbrewed.science/user.json) and user resume entries from a public endpoint (https://freshbrewed.science/experiences.json). There should be a Dockerfile to host the app that sets those endpoint URLs so that they are configurable with a passed in environment variable. However, for this example those values can be hardcoded in the Dockerfile for now. Thank you.
Interestingly, this fired up but showed a bit error in Lovable
I asked it to automatically correct the error
still no go…
I tried again..
Let’s try locally. I’ll copy over to Github (this time it worked so perhaps that first night was indeed Github’s fault)
I pulled it down but I didn’t see separate URLs for the JSON, but I did see a root API url in the Dockerfile
builder@LuiGi:~/Workspaces/timeline-resume-masterpiece$ ls
Dockerfile components.json nginx.conf postcss.config.js tailwind.config.ts tsconfig.node.json
README.md eslint.config.js package-lock.json public tsconfig.app.json vite.config.ts
bun.lockb index.html package.json src tsconfig.json
builder@LuiGi:~/Workspaces/timeline-resume-masterpiece$ cat Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
ENV API_URL=https://freshbrewed.science
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]b
But it seems it does not actually use that var
In fact, what it appears to have done is just use a Proxy Pass in the NGinx config to make the app “see” them on /user.json and /experiences.json
Compare to v0
I asked myself, “Am I being too hard?”. I took a moment to fire the same query at v0
It initially had an error
I followed the simple click to ‘Fix with v0’. I tried a few times but to no affect
I decided to download locally and build the Dockerfile as to me the code looked good
My first test to build with Docker failed due to a missing package lock file
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ docker build -t v0-resume-app:0.1 .
[+] Building 1.4s (8/14) docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 667B 0.0s
=> [internal] load metadata for docker.io/library/node:18-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [builder 1/6] FROM docker.io/library/node:18-alpine@sha256:a24108da7089c2d293ceaa61fb8969ec10821e8efe25572e5abb10b1841eb70b 0.0s
=> => resolve docker.io/library/node:18-alpine@sha256:a24108da7089c2d293ceaa61fb8969ec10821e8efe25572e5abb10b1841eb70b 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 3.32kB 0.2s
=> CACHED [builder 2/6] WORKDIR /app 0.0s
=> CACHED [builder 3/6] COPY package*.json ./ 0.0s
=> ERROR [builder 4/6] RUN npm ci 0.7s
------
> [builder 4/6] RUN npm ci:
0.647 npm error code EUSAGE
0.647 npm error
0.647 npm error The `npm ci` command can only install with an existing package-lock.json or
0.647 npm error npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or
0.647 npm error later to generate a package-lock.json file, then try again.
0.647 npm error
0.647 npm error Clean install a project
0.647 npm error
0.647 npm error Usage:
0.647 npm error npm ci
0.647 npm error
0.647 npm error Options:
0.647 npm error [--install-strategy <hoisted|nested|shallow|linked>] [--legacy-bundling]
0.647 npm error [--global-style] [--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]]
0.647 npm error [--include <prod|dev|optional|peer> [--include <prod|dev|optional|peer> ...]]
0.647 npm error [--strict-peer-deps] [--foreground-scripts] [--ignore-scripts] [--no-audit]
0.647 npm error [--no-bin-links] [--no-fund] [--dry-run]
0.647 npm error [-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
0.647 npm error [-ws|--workspaces] [--include-workspace-root] [--install-links]
0.647 npm error
0.647 npm error aliases: clean-install, ic, install-clean, isntall-clean
0.647 npm error
0.647 npm error Run "npm help ci" for more info
0.649 npm error A complete log of this run can be found in: /root/.npm/_logs/2025-01-12T22_26_24_036Z-debug-0.log
------
1 warning found (use docker --debug to expand):
- LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format (line 17)
Dockerfile:7
--------------------
5 |
6 | COPY package*.json ./
7 | >>> RUN npm ci
8 |
9 | COPY . .
--------------------
ERROR: failed to solve: process "/bin/sh -c npm ci" did not complete successfully: exit code: 1np
I double checked the Dockerfile for with version of NodeJS, the set that with nvm
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ cat Dockerfile
# Stage 1: Building the app
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Running the app
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Set default values for environment variables
ENV USER_API_URL=https://freshbrewed.science/user.json
ENV EXPERIENCES_API_URL=https://freshbrewed.science/experiences.json
EXPOSE 3000
CMD ["node", "server.js"]
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ nvm list
-> v10.22.1
v12.22.11
v14.18.1
v16.14.2
v17.6.0
v18.17.1
v18.18.2
v20.9.0
default -> 10.22.1 (-> v10.22.1)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v20.9.0) (default)
stable -> 20.9 (-> v20.9.0) (default)
lts/* -> lts/iron (-> N/A)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.2 (-> N/A)
lts/hydrogen -> v18.20.3 (-> N/A)
lts/iron -> v20.15.0 (-> N/A)
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ nvm use 18.18.2
Now using node v18.18.2 (npm v9.8.1)
I can now install and save
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ npm install --legacy-peer-deps --save
up to date, audited 269 packages in 1s
41 packages are looking for funding
run `npm fund` for details
1 moderate severity vulnerability
To address all issues, run:
npm audit fix --force
Run `npm audit` for details.
This time with docker build
(and adding the same –legacy-peer-deps to the build step), I got farther but still no go
builder@DESKTOP-QADGF36:/mnt/c/Users/isaac/Downloads/resume-timeline$ docker build -t v0-resume-app:0.1 .
[+] Building 197.3s (13/14) docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 686B 0.0s
=> [internal] load metadata for docker.io/library/node:18-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [builder 1/6] FROM docker.io/library/node:18-alpine@sha256:a24108da7089c2d293ceaa61fb8969ec10821e8efe25572e5abb10b1841eb70b 0.0s
=> => resolve docker.io/library/node:18-alpine@sha256:a24108da7089c2d293ceaa61fb8969ec10821e8efe25572e5abb10b1841eb70b 0.0s
=> [internal] load build context 159.1s
=> => transferring context: 1.90MB 159.1s
=> CACHED [builder 2/6] WORKDIR /app 0.0s
=> CACHED [builder 3/6] COPY package*.json ./ 0.0s
=> [builder 4/6] RUN npm ci --legacy-peer-deps 17.9s
=> [builder 5/6] COPY . . 5.1s
=> [builder 6/6] RUN npm run build 14.2s
=> ERROR [runner 3/6] COPY --from=builder /app/next.config.js ./ 0.0s
=> CACHED [runner 4/6] COPY --from=builder /app/public ./public 0.0s
=> ERROR [runner 5/6] COPY --from=builder /app/.next/standalone ./ 0.0s
------
> [runner 3/6] COPY --from=builder /app/next.config.js ./:
------
------
> [runner 5/6] COPY --from=builder /app/.next/standalone ./:
------
1 warning found (use docker --debug to expand):
- LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format (line 17)
Dockerfile:21
--------------------
19 | COPY --from=builder /app/next.config.js ./
20 | COPY --from=builder /app/public ./public
21 | >>> COPY --from=builder /app/.next/standalone ./
22 | COPY --from=builder /app/.next/static ./.next/static
23 |
--------------------
ERROR: failed to solve: failed to compute cache key: failed to calculate checksum of ref 2f2440cb-8c3f-42a0-839d-7aaabcdbcd04::4no1o4wk93v5u06okw66uds18: "/app/.next/standalone": not found
I tried a local test only to reproduce what the v0 preview showed
I tried pulling from S3 bucket URLs
and even test bucket URLs
Copilot and Gemini
I had to stop. This was nuts. I was trying everything I could think of to fight through to pull these files from an endpoint.
I wanted to see some kind of basic app before this writeup was completed.
Gemini Code Assist
First up, let’s use Gemini Code Assist
create a basic python flask app that fetches a user.json file from a REST endpoint (https://s3.us-east-1.amazonaws.com/freshbrewed.science/user.json) and resume experiences from a REST endpoint (https://s3.us-east-1.amazonaws.com/freshbrewed.science/experiences.json) then displays them in a simple resume page with a horizontal timeline.
Here you can see in about 8 minutes (counting build time that I paused recording), you can see me whip up an ugly, but functional, version using Gemini Code Assist and Python Flask
Copilot Free
Let’s do similar with Copilot. I will give it the same prompt as Gemini.
As you can see, that tighter integration with VS Code meant it could create all the files for me. The only mistake it made was where it created the Dockerfile when prompted. Otherwise, it worked just dandy (and the page was a bit more stylish out of the box).
Summary
We looked at Lovable.dev and found for making a self-contained app, especially one they host, it works quite well. I liked the styling and it was pretty good at self-fixing errors.
However, it really didn’t do so hot outside of their ecosystem. There is lots of hardcoded references to ‘gptengineer.app’ (which was their original name) and I could not get the Javascript to fetch from external sources. Moreover, it really wanted to bake in the files exploded into the minimized code. This would make it hard to fetch externally.
I’ll own that perhaps there are better prompts so it could be user error. But I really did try (far more than I generally do). On the same hand, I had similar issues with reports from v0, however the v0 app did, at the least, externalized the JSON files so I imagine I could have worked through fetching them with in a init container or at the very least a startup script that did a wget first.
Circling back to pricing, I’m not keen on lovable.dev’s model. Unless I’m mistaken, you really pay a lot more than all the others and I wasn’t wowed enough to choose this over Bolt.new or v0.dev
Lastly, I wanted to just sanity check my proposal. Was I asking too much? By leveraging Gemini Code Assist and Copilot, I very quickly had my answer - both tools did exactly as I asked and in less than 10 minutes I had a working dockerized app that would have been much closer to my ask than Lovable and v0 did (albeit not as pretty).
LAPP
During the writing of this article, I saw this video from Ras Mic ‘How to Build a SaaS using Bolt.net’. It’s a good example of using Bolt.net, but more importantly he really hammers this idea of using a framework he’s dubbed “LAPP”.
Ras Mic defines LAPP as:
Tab 1 | Tab 2 |
---|---|
1. Landing Page | 1. Product |
2. Authentication | |
3. Payments |
The idea here is that the AIs do a lot better if you separate out your workloads into two separate ‘asks’ (tabs, projects, chats, threads.. whatever you call your session with the AI).
By just focusing on the Landing Page with Authentication (and Payments if needed) totally separate from building out the product, you’ll be far more successful.
I’m almost thinking of building on this concept and suggesting using these AI tools like CursorAI, v0 and Lovable for the “Tab 1” side of the house then tools like Gemini Code Assist, Copilot and Claude for the “Tab 2” side of the house.
Either way, I highly recommend checking out his 16m YT video