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.

Astro.js & Docker: Deploy Your Site on a VPS (Static & Dynamic) hero image

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.

  1. 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.

  2. 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
  3. 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 .astro file here becomes a route. index.astro becomes /, about.astro becomes /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 .astro file, <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.

  1. Install the adapter:

    npx astro add node

    This adjusts your astro.config.mjs and installs the necessary packages.

  2. 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>
  3. 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:

  1. Commit and push the code (Git)
  2. On the server: run git pull
  3. Then: sudo docker compose up --build -d

That’s it. Docker builds the new image and automatically restarts the container.

FAQ
Can I deploy Astro without Docker?
Yes. Astro generates static files that can be hosted on any web server. Alternatively, Astro with the Node.js adapter generates files that run on any Node.js server.
When do I need Server-Side Rendering?
SSR is necessary when pages need up-to-date data on every request, such as user-specific content or real-time queries. For blogs, portfolios, and documentation sites, static rendering is more than sufficient. It's more performant and doesn't require a running server process.
How do I update my website on the server?
Make changes to the code locally, test them, and push via Git. On the server, log in via SSH, run git pull, and then docker compose up --build -d. This rebuilds the image and restarts the container.
Can I host multiple Astro projects on one VPS?
Yes. Use different ports or a reverse proxy like Traefik or Nginx as a gateway. Each project gets its own container.
Nginx or Apache for static Astro sites?
Both work.

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!

Share this post:

Changelog

Dockerfiles updated (node:lts, optimized multi-stage pattern), text expanded, FAQ section added.

Related Articles