Self-host a Framer site with Docker on your own server

Self-host a converted Framer site with Docker on a VPS: a Dockerfile for Next.js or the Node targets, Caddy for HTTPS, environment variables and updates.

Published
Reading time
5 min
Deframer cover: “Your server. Your site.” Beside it, a list of Image, Your server, HTTPS and Domain with Your server selected
On this page11 sections

To self-host a Framer site with Docker, convert the published site to a code project, build it into a Docker image, run the container on your server, and put a reverse proxy such as Caddy or nginx in front for HTTPS. Framer itself can't be self-hosted, because it doesn't export files; a converted project can, because it's a standard Node.js app. Next.js is the most common choice, and the TanStack Start, Astro and React Router targets work the same way with a different start command.

Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer. Downloads are for sites you own or have the owner's permission to convert.

What you need

  • A Linux server (any VPS provider) with Docker and Docker Compose installed.
  • A domain whose DNS you control.
  • The converted project. Paste your published Framer address on deframer.co, pick a framework, verify the site is yours and download. Converting is free; a verified download costs 1 credit (pricing).
  • For a fully independent site, choose Assets in the project when converting, so images, fonts and videos are served from your server instead of Framer's CDN.

If you don't want a server at all, a static Vite export on any static host is simpler. This guide is for when you want the box to be yours.

Which target, and how it starts

Target

Build

Start

Node.js

Default port

Next.js

npm run build

npm start (next start)

20.9 or later

3000

TanStack Start

npm run build (Rsbuild)

npm start (srvx)

22.18 or later

see srvx's docs

Astro

npm run build (prerenders every page)

npm start (the project's server.ts)

22.18 or later

4321

React Router

npm run build, then prerender

npm start (the project's server.ts)

22.22 or later

3000

The Astro and React Router server.ts read HOST and PORT; their default host is localhost, so inside a container set HOST=0.0.0.0 or the proxy can't reach them. For Next.js in standalone mode, set HOSTNAME=0.0.0.0 for the same reason; the Dockerfile below does.

Step 1: Build and run locally

sh
npm install
npm run build
npm start

Open the site and click through the routes in the README. Fix anything here first; Docker won't make a broken build better.

Step 2: Write the Dockerfile

Next.js, with standalone output

Next.js can trace exactly the files a production server needs into .next/standalone, which makes a much smaller image. Add one line to next.config.ts (the converted config doesn't set it):

ts
const nextConfig: NextConfig = {
  output: "standalone",
  // … the rest stays as converted
};

Then add a Dockerfile at the project root:

dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]

The standalone server doesn't copy public/ or .next/static itself, which is why the two extra COPY lines are there. Without them, styles and assets are missing.

TanStack Start, Astro or React Router

These run with npm start, so copy the whole built app:

dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
COPY --from=build /app ./
USER node
EXPOSE 3000
CMD ["npm", "start"]

node:22-slim installs the newest Node.js 22 release, which satisfies 22.18 and 22.22. Pin a specific tag if you want reproducible images.

.dockerignore

Add a .dockerignore so local files stay out of the image:

Code
node_modules
.next
dist
.env
.env*.local
.git

Step 3: Compose the app and Caddy

Caddy gets and renews TLS certificates on its own, which makes it a good default. Create compose.yaml:

yaml
services:
  site:
    build: .
    restart: unless-stopped
    env_file: .env.production
    expose:
      - "3000"

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - site

volumes:
  caddy_data:
  caddy_config:

And a Caddyfile:

Code
example.com {
  encode zstd gzip
  reverse_proxy site:3000
}

www.example.com {
  redir https://example.com{uri} permanent
}

Swap the two blocks if your site's primary form is www. Keep the same primary form the Framer site used so canonical URLs match.

Step 4: Environment variables

Put secrets in .env.production on the server, not in the image or in Git:

sh
FORMS_WEBHOOK_URL=https://hooks.example.com/your-endpoint

The converted project's .env.example lists every variable: FORMS_WEBHOOK_URL for all forms and one FORMS_<NAME>_WEBHOOK_URL per form. Without a webhook, submissions are only logged by the server, which in Docker means docker compose logs site.

A self-hosted single container has one advantage for forms: the project's in-memory rate limit is one counter, rather than one per serverless instance.

Step 5: Deploy and point DNS

On the server:

sh
git clone git@github.com:you/your-site.git
cd your-site
# create .env.production
docker compose up -d --build

Test with the server's IP and a Host header before touching DNS:

sh
curl -sI -H "Host: example.com" http://YOUR_SERVER_IP/

Then lower the TTL on your DNS records a day ahead, change the A (and AAAA, if you have IPv6) records to the server, and wait for Caddy to fetch certificates. Remove the domain from your Framer project once the new site answers. Moving a domain off Framer without downtime has the full cutover and rollback plan.

Before the switch, update metadataBase (Next.js, in src/app/layout.tsx), public/robots.txt and public/sitemap.xml if the domain changes.

Updating the site

sh
git pull
docker compose up -d --build

There's a short gap while the container restarts. If that matters, run two site replicas behind Caddy and restart them one at a time, or use a deploy tool that does blue-green swaps. When you change the design in Framer and republish, convert again, merge the new output on a branch (maintaining a converted site with Git) and redeploy.

Hardening checklist

  1. The container runs as the node user, not root (the USER node line).
  2. Only ports 80 and 443 are open on the firewall; the app port stays on the Docker network.
  3. Automatic OS security updates are on.
  4. Security headers are set, in Caddy or in the app. Security headers for a self-hosted Framer site gives a starting set.
  5. You have uptime monitoring and a backup of .env.production.
  6. The Git repository is the source of truth, so a dead server is a rebuild, not a loss. See how to back up a Framer site.

Is self-hosting cheaper?

It depends on how you count your time. A small VPS is inexpensive, but you take on updates, monitoring and incidents. Framer hosting vs self-hosting costs gives a worksheet. If you want a Node.js host without the server work, Render or Railway is a middle ground.

FAQ

Can I self-host a Framer site?

Not as a Framer project; Framer only hosts sites itself. Convert the published site to a code project with Deframer, and that project runs on any server with Node.js, in Docker or not.

Do I need Docker?

No. You can run npm run build and npm start under a process manager such as systemd. Docker makes the environment reproducible and updates simpler.

Which reverse proxy should I use?

Caddy is the simplest because it handles HTTPS certificates automatically. nginx or Traefik work too; point them at the container's port.

Does the site still load anything from Framer?

Not code. With the default asset setting, images, fonts and videos still load from Framer's CDN. Convert with "Assets in the project" to serve everything from your server.

How much memory does the container need?

It depends on the site and traffic. Watch docker stats under real traffic and size the server from that; a marketing site with prerendered pages is usually light.

Keep reading

All posts
  • Deframer cover: “S3 and a CDN, clean URLs.” Beside it, a list of dist/, S3 bucket, Clean URLs and CloudFront with CloudFront selected

    Hosting5 min read

    Host a Framer site on AWS S3 and CloudFront

    Host a converted Framer site on AWS: build a static Vite site, upload dist/ to a private S3 bucket, serve it with CloudFront, clean URLs, 404s and redirects.

  • Deframer cover: “Host it free. Push to Pages.” Beside it, four tiles, Convert, Build, Actions and Pages, with Pages selected

    Hosting8 min read

    Host a Framer site on GitHub Pages

    Convert a Framer site to a static Vite project, build it, push it to GitHub and deploy with Actions. Also works on Netlify, Cloudflare Pages and S3.

  • Deframer cover: “Tailwind, next to CSS modules.” Beside it, a list of Tokens, @theme and Utilities with @theme selected

    CSS5 min read

    Add Tailwind CSS to a converted Framer site

    Add Tailwind CSS v4 to a Framer site converted to Next.js without breaking it. Skip Preflight, map Framer tokens into @theme, and mind cascade layers.