AI Code Generation: Lovable.dev and Copilot Free

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

/content/images/2025/01/lovable-01.png

For IdPs, I can create a unique account or use Google or Github

/content/images/2025/01/lovable-02.png

For the free or trial plan, we can see we get five messages a day

/content/images/2025/01/lovable-03.png

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?

/content/images/2025/01/lovable-04.png

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

/content/images/2025/01/lovable-59.png

Vercel’s v0 pricing seems to just limit us on number of projects, chat speed and attachment size

/content/images/2025/01/lovable-60.png

(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

/content/images/2025/01/lovable-61.png

While Gemini Code Assist pricing does not have a free tier shown.

/content/images/2025/01/lovable-62.png

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

/content/images/2025/01/lovable-05.png

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”).

/content/images/2025/01/lovable-06.png

And lastly, best I can tell, v0 only limits me on total projects (which I can delete) - so ?unlimited.

/content/images/2025/01/lovable-07.png

And I can go to faster higher limits and attachments (actually that attachment size would be nice) with Premium at US$20/month

/content/images/2025/01/lovable-08.png

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

/content/images/2025/01/Notes_241230_111347.jpg

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

/content/images/2025/01/lovable-09.png

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.

/content/images/2025/01/lovable-10.png

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

/content/images/2025/01/lovable-11.png

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

/content/images/2025/01/lovable-12.png

This built into https://cv-optimizer.lovable.app/

/content/images/2025/01/lovable-13.png

Firefox also errored. I moved to Google since that rarely has issues, at least for an IdP.

That suggested a malformed request

/content/images/2025/01/lovable-14.png

Being stuck

Here is where I am just stuck. I can view some of the files

/content/images/2025/01/lovable-15.png

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.

/content/images/2025/01/lovable-16.png

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

/content/images/2025/01/lovable-17.png

This did use Pins for icons, so I can see how that is similar but it is not what I wanted

/content/images/2025/01/lovable-18.png

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

/content/images/2025/01/lovable-19.png

Now when I check Github Status it would appear things have gone to poo there at the moment

/content/images/2025/01/lovable-20.png

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

/content/images/2025/01/lovable-21.png

Github

After a lot of tries and after Github Status showed all green, I finally got it to connect

/content/images/2025/01/lovable-22.png

I did a “Create in” my own area

/content/images/2025/01/lovable-23.png

which now has a URL to clone

/content/images/2025/01/lovable-24.png

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:

/content/images/2025/01/lovable-25.png

I cloned and built it locally

/content/images/2025/01/lovable-26.png

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

/content/images/2025/01/lovable-27.png

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

/content/images/2025/01/lovable-28.png

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/

/content/images/2025/01/lovable-29.png

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”.

/content/images/2025/01/lovable-30.png

I can then enable by clicking the button

/content/images/2025/01/lovable-31.png

In one case I had VS Code prompt me to continue

/content/images/2025/01/lovable-32.png

and in another it just prompted me to sign-in (with full MFA of course)

/content/images/2025/01/lovable-33.png

Updating

One of the issues I see right away is that this Dockerfile builds a static Vite app

/content/images/2025/01/lovable-34.png

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

/content/images/2025/01/lovable-35.png

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

/content/images/2025/01/lovable-36.png

I don’t need an AI to show me how that is off and what it should be

/content/images/2025/01/lovable-37.png

I fixed the line and built again

/content/images/2025/01/lovable-38.png

This now gives me a whole new errored

/content/images/2025/01/lovable-39.png

Copilot had some more suggestions

/content/images/2025/01/lovable-40.png

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

/content/images/2025/01/lovable-41.png

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

/content/images/2025/01/lovable-42.png

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.

/content/images/2025/01/lovable-43.png

Interestingly, this fired up but showed a bit error in Lovable

/content/images/2025/01/lovable-44.png

I asked it to automatically correct the error

/content/images/2025/01/lovable-45.png

still no go…

/content/images/2025/01/lovable-46.png

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)

/content/images/2025/01/lovable-47.png

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

/content/images/2025/01/lovable-48.png

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

/content/images/2025/01/lovable-49.png

Compare to v0

I asked myself, “Am I being too hard?”. I took a moment to fire the same query at v0

/content/images/2025/01/lovable-50.png

It initially had an error

/content/images/2025/01/lovable-51.png

I followed the simple click to ‘Fix with v0’. I tried a few times but to no affect

/content/images/2025/01/lovable-52.png

I decided to download locally and build the Dockerfile as to me the code looked good

/content/images/2025/01/lovable-53.png

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

/content/images/2025/01/lovable-54.png

I tried pulling from S3 bucket URLs

/content/images/2025/01/lovable-55.png

and even test bucket URLs

/content/images/2025/01/lovable-56.png

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

Lovable Copilot AI Docker Kubernetes VSCode

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes