How to Deploy a Lit App With Docker [Full Repo]

Lit work ships as one of two artifacts: an application people visit, or a component library other projects install. This guide deploys the application — a Vite build inside an nginx container, taken to a public URL and verified.

A Lit project splitting into two artifacts — a component library distributed as a package and an application built by Vite into a Docker image — with the application path followed through nginx to a live HTTPS URL

Ask “how do I deploy Lit?” and you have really asked two questions, because Lit code leaves a repository in two distinct shapes. Shape one is an application: an index.html that loads your components, built by Vite into files a web server hosts at a URL.

Shape two is a component library: code packaged for other projects to consume, distributed through npm, a CDN, or hosting you control — it does not require a visitor-facing application of its own.

This guide publishes the first shape. We built the Vite lit template into a two-stage Docker image and put it on a live Back4app Containers URL; dates, commit, and figures all live in the measurements section, and the companion repo holds the working code.

The library flow gets its own pointers below, mostly so you can recognize which of the two artifacts you actually need to ship.

Which artifact are you shipping?

Sort your project into one of these two rows before touching Docker, because only one row leads through this guide.

Signals in your repoArtifactFlow
An index.html, pages people will visit, vite buildApplicationthis guide: Docker image → container → URL
Components meant for other codebases, a package.json built for consumersLibrarydistribute as a package — npm, CDN, hosted modules, or a private channel; no web server of yours

The useful question is which artifact this repository should produce, and some repos honestly produce both — a design system with a demo site runs both flows side by side: distribute the package, deploy the demo. The companion repo is a pure application: the Vite template’s demo page rendering its my-element component.

Scaffolding the application

For the application flow from zero, this is the template behind the companion repo.

npm create vite@latest lit-container-starter -- --template lit

The dependency list it produced is almost ascetic: lit ^3.3.3 and vite ^8.3.6, resolving to a couple dozen packages — a fraction of what any virtual-DOM framework in this series pulled.

As with the other Vite scaffolds we deployed, the lockfile was on us: our fresh project had none, and the image’s npm ci step will not improvise one. A single local npm install, commit package-lock.json, done.

Build the Docker image

Build and serve stay in separate stages — Node compiles, nginx ships — and the runtime container never sees a Node binary.

# Build stage
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Serve stage
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 8080

The nginx config is the repo’s as-is: compression, a long-cache rule for Vite’s hashed /assets/ files, and a fallback to index.html. No exotic headers — web components need nothing special from a server, and this guide claims nothing beyond what these lines set.

server {
    listen 8080;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    gzip on;
    gzip_proxied any;
    gzip_vary on;
    gzip_comp_level 6;
    gzip_min_length 1024;
    gzip_types text/css application/javascript application/json image/svg+xml text/plain;

    # Vite writes content-hashed files to /assets/ — cache them forever
    location /assets/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
    }

    # SPA refresh fix: unknown paths fall back to index.html
    location / {
        try_files $uri $uri/ /index.html;
    }
}

The fallback deserves a conscious decision rather than a copy-paste. Lit includes no router, so with this config every unknown path returns the application shell — /anything serves the component demo. That is the right trade if you plan to add a client-side router, which is why the repo keeps it.

If this page will stay routerless, answering 404 on nonexistent paths is arguably more correct: replace the last block’s rule with try_files $uri $uri/ =404; and only real files resolve.

We also verified the /assets/* files arrive from the running container with the immutable cache header the config promises — safe here because Vite content-hashes everything in that folder.

Deploy on Back4app

Plan context first, so the choice is informed: a free-plan app answers on a public URL that retires itself 60 minutes after the app’s creation, and a redeploy in that hour did not delay it — our verification needed far less, but hosting for real means a paid plan and its permanent URL.

  1. Get the repo onto GitHub with Dockerfile, nginx.conf, and the lockfile included, then start a New Web Deployment from it.

Selecting GitHub repository as the source for a new Web Deployment

One source screen serves every framework in this series — screenshot from our React deploy, dashboard as of November 2026.

  1. Let it detect the Dockerfile, then do the one thing detection skips: write 8080 into the Port field. Auto never adopts EXPOSE — the trip-up every deploy in this series routes around, documented with its error text in the React article.

The Build & deploy page with Dockerfile selected and Port set to 8080

The identical settings step, captured during the React deploy. Dashboard as of November 2026.

  1. Hit Create and let the short build run (so few packages leave it little to do) — our run’s exact READY time is recorded with the measurements.

The post-deploy dialog with the temporary HTTPS URL and 60-minute countdown

Free-plan URL dialog, reused from the React deploy. Dashboard as of November 2026.

  1. Open the URL and check the component page loads; then curl a deep path — ours returned 200 serving the application shell, which is the configured fallback answering, not a route rendering — and fetch the script with gzip accepted to check the transfer.

Deploy from an AI assistant with MCP

An assistant connected to Back4app’s remote MCP server can run this deploy from the editor. It is the interface for the deploy, not a different way of building: the Dockerfile above still makes the image. Setup per client: official MCP page.

The MCP deploy guide walks through the same flow from connection to update.

In our run on November 10, 2026 (Claude, custom connector, signed in): create_upload, the npx @back4app/cli upload command it returns, then create_web_app with port 8080. builder: DOCKERFILE, READY about 23 seconds after creation, / returning 200 and the fallback returning the application shell on an unknown path.

We used this starter for two extra checks:

  • A wrong port does not fail the deploy. Created with port 3000 while nginx listens on 8080, the app still went READY: the platform reported portDetected (expected 3000, listening 8080), switched to 8080 and saved it for future deploys.
  • Settings changes wait for you. A call to update_web_app adding an environment variable returned CONFIRMATION_REQUIRED with a summary and changed nothing. The change only runs after the person approves it.

Exact fields returned in both checks: MCP evidence file.

What we measured

Local figures are from November 6, 2026; the live deploy on November 9, 2026 ran the repo at commit 0477a27 (deployed lab commit and commands are in the repo’s evidence files).

Sizes on disk and observed transfers are listed separately.

On-disk sizes and build times (local, November 6, 2026):

WhatValue
npm ci (22 packages — the series’ smallest tree)1.46 s
npm run build2.28 s wall (Vite: built in 479 ms)
dist/ on disk84 KB
docker build, base images already present8.01 s
Docker image76.1 MB (arm64; the base image is the image)
First-load JS, raw file size26.13 KB (one file)

Observed transfers and timing:

WhatValueWhere
First-load JS from our nginx, gzip level 68,763 Blocal container, November 6, 2026
Same file at nginx’s default gzip level 19,759 Blocal container
First-load JS on the public URL11,869 B before gzip_proxied any; · 8,763 B afterlive URL, November 9 and 10, 2026
HTML document, gzipped507 Blocal container
Platform build READY13:41:25 UTClive deploy, November 9, 2026
TTFB on / (3 illustrative requests)0.957 s · 0.949 s · 1.435 slive URL

The gap between the two public figures is one nginx directive. Requests relayed by the platform’s CDN are not compressed by nginx unless gzip_proxied allows it; after adding gzip_proxied any; on November 10, 2026, the live URL transferred 8,763 B, the same as the origin.

Update the deployed app

Once live, a component edit reaches users with a push and one click of App actions → Deploy the latest commit, after which the same URL serves the new build. We ran that during the November 9, 2026 session; the fully instrumented update test, caveats included, is in the React article.

Paid plans deploy pushes automatically. Rebuilding the app from scratch hands out a different URL, so never treat it as an update.

Troubleshooting

You deployed when you meant to distribute (or vice versa) — a visitor-facing app helps no package consumer, and a packaged component serves no visitor. Re-check the artifact table at the top; design systems legitimately need both flows.

The npm ci command can only install with an existing package-lock.json — no lockfile is committed. One npm install, one commit, resolved.

Please mention a port in settings to expose — the Port field is still on auto and ignored your EXPOSE 8080. Set it explicitly in Build & deploy.

Every URL shows the same page — that is the configured fallback serving the application shell, as the nginx section explains. Keep it if a client router is coming; switch to the =404 variant if not.

Components render unstyled or not at all — confirm the page loads the built /assets/*.js (view source on the live URL) rather than a dev-server path left in index.html.

Limitations of this flow

These are the real edges of the setup, worth deciding about rather than discovering.

  • It publishes the application artifact only. Distributing components to other codebases is the package flow — npm, CDN, hosted modules, or private channels — and no part of this container setup performs it.
  • The fallback is a stance, not a law. Shell-on-every-path suits a future client router; a permanently routerless page may serve users better with 404s, as shown in the nginx section.
  • Static files only. Server-side rendering or per-request logic means a server artifact and a Node runtime container.
  • Free-plan URLs are short-lived. Sixty minutes per app makes the free plan a proving ground, not a host.
  • Accounts and data are a next step. When components need shared state behind them, options include a managed backend over REST or GraphQL, or an API service you run — the static deploy stays untouched either way.

What’s next?

Decide your artifact, and if it is the application, the companion repo plus the Containers docs take it to a free HTTPS URL in one sitting.

And when those components need sign-in or shared data behind them, wiring in a managed backend is one clean option — with the deploy you just finished unchanged.

Tagslitweb-componentsdockervitenginxcontainersdeploy

Frequently asked questions

Can I use Lit components in React or Vue?

Yes — Lit components are standard custom elements, so any framework that can render HTML can use them. Consuming a component elsewhere is the library flow: you distribute the component as a package — through npm, a CDN, or hosting you control — and the host project pulls it in; no visitor-facing application of its own is required. This guide covers the other artifact: a Lit application with its own page, deployed and verified live.

What is the difference between deploying a Lit app and publishing a Lit component?

An application is an index.html plus built JavaScript that users visit at a URL — you build it with Vite and host the files, which is exactly what this guide does with Docker and nginx. A component library is code packaged for other projects to consume, distributed via npm or another channel (CDN, hosted modules, a private registry); it never needs a web server of yours. Same Lit code at the center, two different artifacts leaving the repo — this tutorial publishes the application.

Does a Lit app need Node.js in production?

No. Node appears only in the Docker build stage, where Vite compiles the app; the container that serves traffic is nginx:1.27-alpine handing out static files. Our deployed app was exactly that — no server-side rendering, no runtime JavaScript on the host, just files and the browser's own custom-elements support.

Why does my Lit app return the same page on every URL?

Because the nginx config in this guide routes unknown paths to index.html via try_files — a server fallback, not routing. Lit ships no router, so with this config a deep path returns the application shell; our live test confirmed a 200 on one. That fallback is the right choice if you plan to add a client router; if unknown paths should instead 404, swap the fallback for an =404 rule as shown in the nginx section.

How big is a Lit app bundle?

The Vite lit template builds a single JavaScript file that transferred as 8,763 bytes from our nginx; the raw file size, the live-URL transfer, and the rest of the figures are in the measurements section. The dependency tree behind it is strikingly short — the smallest of any framework we deployed in this series.

Can I deploy a Lit app for free?

Yes — this guide's deploy ran on Back4app Containers' free plan (0.25 vCPU, 256 MB). One clock to respect: a free app's public URL switches off 60 minutes after the app is created, which is ample for verifying the flow and unsuitable for hosting. Paid plans hold the URL permanently and can deploy on every push.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Nov 9, 2026 — Local: Node 22 LTS, Docker CLI (Colima) on an Apple Silicon Mac · Live: Back4app Containers, free plan (0.25 vCPU / 256 MB), deployed from GitHub, measured November 9, 2026.

Changelog

  • Nov 10, 2026 — Added a section on running this deploy from an AI assistant through the Back4app MCP server, tested on this starter.
  • Nov 10, 2026 — The gap between origin and public transfer sizes is now explained and fixed: nginx needed gzip_proxied any to compress requests relayed by the CDN. Config and figures updated.
  • Nov 10, 2026 — Free-plan URL lifetime restated: 60 minutes from the app's creation, not per deploy. Added a link to the full MCP deploy walkthrough.

More from Back4app Engineering

Web Deployment · alternative

Top 10 Vue Alternatives in 2026 (Compared & Ranked)

Containers · tutorial

How to Deploy a React App: 3 Methods, Step by Step [Full Repo]

Containers · tutorial

How to Host a Static Website on Back4app Containers With Docker and nginx [Full Repo]