tcds.dev

Learning Clojure from Scratch (Part 2) - Put It on the Internet

October 19, 2026

Part 2 of 5 ยท Learning Clojure from Scratch

This is part two of a series teaching Clojure to people with little or no programming experience. In Part 1 you built a small website and ran it on your computer.

Right now the only person who can see your website is you. In this part we'll fix that. By the end you'll have a link you can open on your phone and send to a friend.

The server code barely changes. Most of the work is teaching the outside world how to run it.

What you need

On top of what you have from Part 1:

  • A free Vercel account. Vercel is where your website will run.
  • Node.js installed on your computer. We need it to install and run Vercel's command line tool.

To check that Node is installed, run npm --version in the terminal.

Open your my-site folder in VS Code and open a terminal (Terminal > New Terminal), just like last time.

Why a few things have to change

On your computer, you run a command and the program runs. When someone else's computer runs it, three things need to be true:

  1. The program has to listen on whichever port they tell it to, not the one you hard-coded.
  2. The program has to be bundled into a single file, so it can be copied around.
  3. They need a recipe that says how to build and start it.

We'll take these one at a time, and after each one you can check that your website still works.

Step 1: Let the outside world choose the port

In Part 1, you hard-coded the port as 8080 ((http/run-server handler {:port 8080})). Vercel requires that your program use a port they define in an environment variable.

Environment variables live outside your code, and you can look them up by name. The one you'll use is called PORT.

Replace -main in src/app/core.clj with:

clojure
(defn -main [& _args]
  (let [port (Integer/parseInt (or (System/getenv "PORT") "8080"))]
    (println (str "Server running on port " port))
    (http/run-server handler {:port port})))

Read it from the inside out:

  • (System/getenv "PORT") asks "is there an environment variable called PORT?" You get its value, or nil (nothing) if there isn't one.
  • (or ... "8080") means "use PORT, or (if PORT is nil) use "8080" instead".
  • (Integer/parseInt ...) turns the text "8080" into the number 8080. Environment variables are always text, so we have to convert.
  • let gives a name to a value, so port refers to either the value of PORT or 8080.

Run it (clojure -M:run) and open localhost:8080. Nothing changed, because PORT was never defined.

Now stop the server (Ctrl+C) and start it with a port of your choosing:

sh
PORT=3000 clojure -M:run

(On Windows PowerShell this would be $env:PORT=3000; clojure -M:run.)

The terminal now says Server running on port 3000. Your website is now running at localhost:3000. It adapts to its environment.

Stop the server again with Ctrl+C.

Step 2: Bundle it into one file

Right now your program is a folder of source files plus a set of libraries that clojure downloads on demand. To copy it to another machine, we want a single file with everything inside. In the Java world that file is called a jar, and one that contains everything is called an uberjar.

We need three changes to create an uberjar.

First, add (:gen-class) to the ns at the top of src/app/core.clj:

clojure
(ns app.core
  (:require [org.httpkit.server :as http])
  (:gen-class))

This tells Java that this file can be the starting point of a packaged program.

Second, replace deps.edn with this. The only new part is the :build alias:

clojure
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.12.0"}
        http-kit/http-kit {:mvn/version "2.8.0"}}
 :aliases
 {:run {:main-opts ["-m" "app.core"]}
  :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.5"}}
          :ns-default build}}}

:run is still a shortcut for starting the program. :build is a shortcut for a tool, tools.build, that knows how to bundle a program.

Third, create a file called build.clj in the root of the project (next to deps.edn, not inside src):

clojure
(ns build
  (:require [clojure.tools.build.api :as b]))

(def class-dir "target/classes")
(def uber-file "target/app.jar")
(def basis (delay (b/create-basis {:project "deps.edn"})))

(defn uber [_]
  (b/delete {:path "target"})
  (b/copy-dir {:src-dirs ["src"] :target-dir class-dir})
  (b/compile-clj {:basis @basis
                  :src-dirs ["src"]
                  :class-dir class-dir})
  (b/uber {:class-dir class-dir
           :uber-file uber-file
           :basis @basis
           :main 'app.core}))

This is a program whose job is to build your program. Read the steps in uber in order and you'll get the idea: delete the old target folder, copy the source in, compile it, and pack it together with all the libraries into target/app.jar.

Now build it:

sh
clojure -T:build uber

The first time, Clojure downloads the build tool, so it can take a minute. It prints nothing when it succeeds. Check that target/app.jar appeared in the file explorer.

Then run the jar directly:

sh
java -jar target/app.jar

Open localhost:8080. You should see Hello, World!.

Checkpoint: this is exactly what Vercel will do with your project. If it works here, the hard part is done.

Stop the server with Ctrl+C.

Step 3: Write the recipe

Create a file called Dockerfile.vercel in the root of the project:

dockerfile
FROM clojure:temurin-21-tools-deps AS build
WORKDIR /app
COPY deps.edn build.clj .
COPY src src
RUN clojure -T:build uber

FROM eclipse-temurin:21-jre-alpine
COPY --from=build /app/target/app.jar /app.jar
CMD ["/opt/java/openjdk/bin/java", "-jar", "/app.jar"]

This is the recipe Vercel follows to build and run your program. Vercel looks for a file with exactly this name, and runs the result.

You don't need to understand the details of this file right now. Its shape is what matters. It has two parts, each starting with FROM:

  1. Build. Start from a machine that has Clojure installed, copy in deps.edn, build.clj and src, and run: clojure -T:build uber.
  2. Run. Start from a much smaller machine that has only Java, copy in app.jar, and start it using java -jar app.jar.

Step 4: Leave some things out

Create two more files in the root of the project.

.dockerignore lists things that shouldn't be sent to the build machine:

text
.git
.gitignore
target
.cpcache
README.md

.gitignore lists things you don't want to save into version control (when you start using Git, it will skip these):

text
.cpcache/
.clj-kondo/
.lsp/
target/
.vercel/

Step 5: Write some documentation

Add one more file called README.md so future-you remembers what this folder is:

markdown
# My Clojure website

Built following the "Learning Clojure from Scratch" series.

Your project should now look like this:

text
my-site/
  .dockerignore
  .gitignore
  Dockerfile.vercel
  README.md
  build.clj
  deps.edn
  src/
    app/
      core.clj

If you compare it with vercel-clojure, the template this series is based on, you'll find the same files. But you wrote them yourself, and you know why each one is there.

Step 6: Deploy

Install Vercel's command line tool:

sh
npm i -g vercel

Log in. This opens your browser to confirm who you are:

sh
vercel login

In your browser, Vercel asks you to authorize your device. Once you do (you'll see a message like "Congratulations! You are now signed in."), run the command below to deploy:

sh
vercel deploy --prod

The first time, Vercel asks a few questions about your project. When asked "Which project?", select "Create a new project". For the other questions, the suggested answers are usually fine. After you answer all of the questions, Vercel will build and deploy your website.

When it finishes you'll get a link that ends in vercel.app. Open it. You should see Hello, World!.

That's your program, running in the cloud, reachable from anywhere in the world.

Checkpoint: open the link on your phone or another computer.

If something went wrong

  • vercel: command not found: the install in Step 6 didn't finish, or your terminal needs to be reopened.
  • The build fails on Vercel: the output in the terminal shows the failing line. The most common cause is a typo in build.clj or Dockerfile.vercel. If clojure -T:build uber works on your computer, the build step should work on Vercel.
  • The link shows an error page: first check that java -jar target/app.jar works locally. If it does, run vercel logs with your deployment's link to see what your program printed.
  • The link asks you to log in to Vercel: Vercel can protect some of the links it creates so only you can see them. Open the link labelled Production in the output of vercel deploy --prod, or find your project in the Vercel dashboard and use its production domain.

What you learned

  • Your program now reads its port from an environment variable (PORT) and falls back to 8080 when there isn't one, so the same code works on your computer and on Vercel.
  • An uberjar is a single file with your code and all of its libraries. clojure -T:build uber creates it and java -jar target/app.jar runs it.
  • build.clj is a small program whose job is to build your program, and the :build alias in deps.edn is how you call it.
  • Dockerfile.vercel is the recipe Vercel follows: one stage builds the jar, the other runs it.
  • Deploying means giving another computer a recipe and the files it needs. vercel deploy --prod does that, and the result is a link anyone on the internet can open.

Next

We'll stop saying "Hello, World!" and put your own words and pages on the website.