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 |
|---|---|---|---|---|
| | 20.9 or later | 3000 | |
| | 22.18 or later | see srvx's docs | |
| | 22.18 or later | 4321 | |
| | 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
npm install
npm run build
npm startOpen 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):
const nextConfig: NextConfig = {
output: "standalone",
// … the rest stays as converted
};Then add a Dockerfile at the project root:
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:
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:
node_modules
.next
dist
.env
.env*.local
.gitStep 3: Compose the app and Caddy
Caddy gets and renews TLS certificates on its own, which makes it a good default. Create compose.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:
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:
FORMS_WEBHOOK_URL=https://hooks.example.com/your-endpointThe 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:
git clone git@github.com:you/your-site.git
cd your-site
# create .env.production
docker compose up -d --buildTest with the server's IP and a Host header before touching DNS:
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
git pull
docker compose up -d --buildThere'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
- The container runs as the
nodeuser, not root (theUSER nodeline). - Only ports 80 and 443 are open on the firewall; the app port stays on the Docker network.
- Automatic OS security updates are on.
- Security headers are set, in Caddy or in the app. Security headers for a self-hosted Framer site gives a starting set.
- You have uptime monitoring and a backup of
.env.production. - 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.
