Astro.js & Docker: Deploy Your Site on a VPS (Static & Dynamic)
Astro.js with Docker on your VPS: static (Nginx) or dynamic (Node.js). Full tutorial with current Dockerfiles, Docker Compose, and deployment workflow.
Table of Contents
- How to Prepare Your Development Environment
- How to Create Your First Astro Project
- Understanding Astro: Pages, Layouts, and Components
- Static or Dynamic: Which Build Is Right for You?
- Docker as the Foundation of Your Deployment
- How to Containerize a Static Astro Site
- How to Containerize a Dynamic Astro Site
- Docker Compose: Orchestrating Containers
- How to Deploy Your Astro App to a VPS
- Conclusion
Want to build a modern, fast website with Astro.js and deploy it on your own server? Docker is the right tool for the job — by now, 92% of the IT industry relies on containers (Docker State of App Dev Report, 2025). In this post, I’ll walk you through the entire process: from the development environment to the live deployment of your Astro application using Docker and Docker Compose on a VPS.
Key Takeaways at a Glance
- Astro grew from 29 domains (2021) to over 38,800 (2025), a 1,340× increase (TechnologyChecker.io, 2025).
- Static builds via Nginx: lean (~25 MB image), extremely fast.
- Dynamic builds via Node.js adapter: flexible, required for SSR routes.
- Multi-stage Dockerfiles keep production images small and secure.
- Docker Compose orchestrates the startup with a single command.
How to Prepare Your Development Environment
Three tools form the foundation for Astro and Docker development.
-
Text Editor: Visual Studio Code (VS Code) is free and offers a huge extension library. The Astro extension for VS Code brings syntax highlighting and IntelliSense.
-
Node.js: Astro is built on Node.js. Instead of a direct installation, I recommend using a version manager to switch flexibly between versions.
- Windows: nvm-windows avoids permission issues.
- Linux/macOS: The original
nvm(Node Version Manager) can be installed via a curl or wget script.
nvm install lts # Install the latest LTS version nvm use lts # Activate the LTS version -
Git: Essential for version control and easily transferring code to the server. Download Git.
# During installation on Windows: choose VS Code as the default editor # Set the Default Branch Name to "main"
Open your terminal (in VS Code: Ctrl+Shift+`) and verify the installations:
node -v
npm -v
git --version
nvm version
How to Create Your First Astro Project
Navigate to your project folder and launch the Astro CLI. The framework generates the basic files and installs all dependencies automatically.
# Create project with minimal template
npm create astro@latest -- --template minimal my-astro-project
# Change into the project directory
cd my-astro-project
# Install dependencies (often already done during creation)
npm install
# or pnpm install
# Start the development server
npm run dev
# or pnpm dev
By default, you’ll find the Astro default page at http://localhost:4321.
INFO
This port has been the default since Astro 3.0 because it references a rocket countdown
Changes in src/pages/index.astro are immediately visible via Hot Module Replacement.
Understanding Astro: Pages, Layouts, and Components
Astro projects follow a clear structure. Once you understand it, you can build projects of any size.
src/pages/: Every.astrofile here becomes a route.index.astrobecomes/,about.astrobecomes/about.src/layouts/: Reusable page structures. A typical layout (BaseLayout.astro) contains the HTML skeleton with a<slot />that each using page fills.src/components/: Reusable UI elements like headers, footers, or cards. Importable in pages and layouts.- Styling: Global CSS files are imported in layouts. Within an
.astrofile,<style>tags are scoped by default, meaning they only apply to the respective component.
---
// Example: src/layouts/BaseLayout.astro
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import "../styles/global.css";
interface Props {
title: string;
description?: string;
}
const { title, description = "My Astro Site" } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title} | Astro Demo</title>
</head>
<body>
<Header />
<main class="container">
<slot />
</main>
<Footer />
</body>
</html>
<style>
.container {
max-width: 1100px;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
flex-grow: 1;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>
Static or Dynamic: Which Build Is Right for You?
Nginx serves static files under load approximately 4,874 times faster than a Node.js server, with a median response time of 0.05 ms compared to 253 ms (Benchmark, Adam Jones, 2024). For pure delivery, the Nginx approach is clearly superior. However, not every project can get by with static files alone.
The Static Build (Default)
This is the standard in Astro. npm run build generates pure HTML, CSS, and JavaScript in the dist/ folder. No Node.js server is needed at runtime.
npm run build
You can deploy the dist/ folder on any static web host.
Dynamic Routes with the Node Adapter
Does a page need dynamic data that changes with every request (e.g., user info from request headers)? That’s where Server-Side Rendering (SSR) comes in.
-
Install the adapter:
npx astro add nodeThis adjusts your
astro.config.mjsand installs the necessary packages. -
Disable prerendering: Explicitly turn it off for the dynamic page:
--- // src/pages/request-info.astro export const prerender = false; const userAgent = Astro.request.headers.get("user-agent") || "Unknown"; import BaseLayout from "../layouts/BaseLayout.astro"; --- <BaseLayout title="Request Info"> <h1>Your Request Info</h1> <p>Your User-Agent: {userAgent}</p> </BaseLayout> -
Build result: The
dist/folder now contains not only static assets (client/) but also server code (server/entry.mjs) that must be executed with Node.js at runtime.
Docker as the Foundation of Your Deployment
Docker packages your application along with all its dependencies into an isolated container. The result: your app runs in the same environment, locally and on the server. Docker reports a 92% adoption rate in the IT industry in 2025, an increase of 12 percentage points over the previous year (Docker State of App Dev Report, 2025). It’s also very popular in the self-hosting community for several reasons. Containerization is not a niche topic — it’s the standard.
The .dockerignore: What Doesn’t Belong in the Image
Similar to .gitignore, this file tells Docker which files should not be copied. This speeds up the build and keeps the image small.
# .dockerignore
.DS_Store
node_modules
npm-debug.log
dist
.astro
.env*
*.env
.git
.vscode
Dockerfile*
docker-compose*
compose*
README.md
How to Containerize a Static Astro Site
For purely static sites, we use a two-stage Docker build. The build stage generates the static files with Node.js, while the runtime stage serves them via Nginx. This follows the official Astro Docker recipe.
Dockerfile (static site):
# Dockerfile (static Astro site with Nginx)
# ---- Build Stage ----
FROM node:lts AS build
WORKDIR /app
COPY package*.json ./
# RUN corepack enable
# COPY package.json pnpm-lock.yaml ./
RUN npm install
# RUN pnpm install --frozen-lockfile --ignore-scripts
COPY . .
# ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run build
# RUN pnpm rebuild && pnpm build
# ---- Runtime Stage ----
FROM nginx:alpine AS runtime
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
nginx/nginx.conf:
Place the configuration file in the nginx/ directory at the project root. It enables Gzip compression, caching headers, and blocks access to hidden files.
# nginx/nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1000;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
location ~* \.(?:css|js|svg|gif|png|jpg|jpeg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
location ~ /\. {
deny all;
}
location / {
try_files $uri $uri/ /index.html;
}
error_page 404 /index.html;
}
}
Why worker_processes auto; instead of 1? Inside the container, Nginx has access to all CPU cores of the host. auto uses them all, which makes a noticeable difference under load.
How to Containerize a Dynamic Astro Site
With the Node adapter, you don’t need Nginx. The Dockerfile follows the optimized multi-stage pattern from the Astro docs, which separates production dependencies from build dependencies. This speeds up subsequent builds when only the source code has changed.
Dockerfile (dynamic site with Node.js):
# Dockerfile (dynamic Astro site with Node adapter)
# ---- Base Stage ----
FROM node:lts AS base
WORKDIR /app
COPY package.json package-lock.json ./
# ---- Production Dependencies ----
FROM base AS prod-deps
RUN npm install --omit=dev
# ---- Build Dependencies + Build ----
FROM base AS build-deps
RUN npm install
FROM build-deps AS build
COPY . .
RUN npm run build
# ---- Runtime Stage ----
FROM base AS runtime
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]
The advantage of this split: when only your source code changes (not package.json), Docker skips the npm install steps and goes straight to the build. This saves minutes on large projects.
Docker Compose: Orchestrating Containers
docker compose greatly simplifies starting and configuring. Even for a single container, it’s worth it.
compose.yml:
services:
astro-app:
build:
context: .
dockerfile: Dockerfile
image: my-astro-project:latest
container_name: my-astro-container
ports:
# Static site (Nginx on port 80 in the container)
- "80:80"
# Dynamic site (Node.js on port 4321 in the container)
# - "80:4321"
restart: unless-stopped
The filename can be compose.yml, compose.yaml, or docker-compose.yml. Docker Compose v2 recognizes all three variants. Choose the appropriate ports mapping: 80:80 for Nginx, 80:4321 for the Node adapter. The left number is the host port you access in your browser.
How to Deploy Your Astro App to a VPS
Now let’s put it all together. Prerequisite: a VPS with a Docker-compatible operating system (e.g., Ubuntu 24.04). If you don’t have a server yet, my VPS guide for beginners will help you with selection and initial setup.
1. Install Docker on the Server
Connect via SSH (ssh root@YOUR_SERVER_IP) and install Docker including the Compose plugin. The following applies to Ubuntu:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Docker Compose is already included as a plugin.
Install Git:
sudo apt update && sudo apt install git -y
2. Get the Code onto the Server
Clone your repository (which you previously pushed to GitHub):
git clone YOUR_REPOSITORY_URL
cd your-project-folder
If you’re not yet familiar with Git, my Git tutorial covers the basics.
3. Start the Container
In the project folder on the server:
sudo docker compose up --build -d
What happens here? docker compose up starts the services defined in compose.yml. --build forces a rebuild of the image if code or the Dockerfile has changed. -d starts everything in the background (detached).
Now open http://YOUR_SERVER_IP in your browser. Your Astro website is live.
4. Deploy Updates
When you’ve made changes locally:
- Commit and push the code (Git)
- On the server: run
git pull - Then:
sudo docker compose up --build -d
That’s it. Docker builds the new image and automatically restarts the container.
Can I deploy Astro without Docker?
When do I need Server-Side Rendering?
How do I update my website on the server?
Can I host multiple Astro projects on one VPS?
Nginx or Apache for static Astro sites?
Conclusion
Astro.js and Docker complement each other perfectly. Astro delivers performance and developer-friendliness, while Docker ensures consistent, portable deployments. With the Dockerfiles and Docker Compose configuration from this post, you have everything you need to take your project live.
If you have questions or feedback, feel free to leave a comment. Good luck with your deployment!
Changelog
Dockerfiles updated (node:lts, optimized multi-stage pattern), text expanded, FAQ section added.
Related Articles
Build a Blog with Gatsby.js and Ghost CMS on Your VPS
Step-by-step guide: Build a blog with Gatsby.js and Ghost as a headless CMS. Set up everything on your VPS, configure Docker, and publish your website.
Set Up Ghost CMS with Docker & Traefik: Full Guide
Run Ghost CMS with Docker behind a Traefik reverse proxy. A step-by-step guide for building your own blog system with SSL and a custom domain – get started now!
Install Karakeep: Self-Hosted Bookmark Manager with Docker
Install Karakeep as your self-hosted bookmark manager with Docker. Includes Caddy reverse proxy and optional AI integration – full control over your bookmarks!
Guide for mail server Mailcow behind Traefik with Docker
Set up your own mail server with Mailcow and Docker. Run it behind Traefik as a reverse proxy with SSL, spam filtering, and webmail – complete step-by-step guide!