Deploying Nuxt 3 with Kamal 2 on Docker: Statically or with SSR
Guillaume Briday
5 minutes
I often deploy static websites and have experience with various tools like Nuxt, Next, Jekyll, and Gatsby.
Platforms like Netlify, Vercel, and Cloudflare make it easy to deploy a Nuxt app. Personally, I’m a big fan of Netlify because it is both reliable and simple to use. However, it is important to keep an eye on their pricing and terms to avoid surprises.
In fact, there have been some cases where people received unexpectedly high bills, sometimes reaching hundreds of thousands of dollars, for what they thought were basic projects.
I had a similar experience with Netlify, where I assumed my project would stay within the free tier, but then received a bill for Stimulus Components. I had exceeded the free-tier limits without any way to block traffic. You can read more about that here.
To avoid this headache, we’ll use Kamal. Kamal makes the process easier by providing scripts that run Docker commands over SSH. Although it was originally built for Rails apps, Kamal works for any app that can be containerized, including our Nuxt app.
In this blog post, I will guide you through deploying a Node app with Nuxt 3 as our example, though these steps work for almost any Node-based or static site generator, such as Nuxt, Next, and Jekyll.
With Nuxt, there are two main deployment options. You can deploy it to a Node.js server with SSR or set it up as a pre-rendered static site. We will cover both options here. For SSR, you will only need Node, while for static hosting, you will also need Nginx to serve the static files.
In this guide, we will go over how to:
- Install Kamal 2 in a Nuxt project
- Set up your DNS
- Create a Dockerfile (Node-only for SSR, or with Nginx for static hosting)
- Set up Kamal-proxy and Nginx to automatically manage SSL certificates
- Configure Nginx
- Deploy the application
Let’s start with the initial setup and configuration!
1. Install Kamal 2
Kamal comes as a Ruby Gem:
$ gem install kamal
After installation, verify that it's correctly installed by checking the version:
$ kamal version
For more info, see: https://kamal-deploy.org/docs/installation/.
2. Prepare your DNS
It's important to configure your DNS early, as it may take some time to propagate. Kamal-proxy will also need the DNS configuration to generate the SSL certificate.
Here’s an example of how to configure the DNS:
my-awesome-app 86400 IN A 192.168.0.1 ; Configured as a subdomain.
; OR
@ 86400 IN A 192.168.0.1 ; Configured as a top-level domain.
Make sure to replace 192.168.0.1 with the actual IP address of your server.
3. Prepare your server
With Kamal, everything happen in Docker, so theoretically you don't anything more installed on your server. Also, Kamal will automatically install Docker for you.
But if you have just created a new server, you probably want to configure basic security rules and better default config.
To automate these actions, I created an Ansible playbook so you don't have to run every command manually, it's especially handy if you deploy your app on multiple servers with load balancing.
You can follow the instructions here if you are interested: 👉 Kamal Ansible Manager
Basically, it will upgrade already installed packages and install the UFW Firewall, Fail2ban and NTP.
4. Prepare the Configuration Files
Start by generating the required configuration files for Kamal with the following command:
$ kamal init
This command creates two key files: .kamal/secrets
and config/deploy.yml
.
.kamal/secrets
: Define your secret environment variables here, which will be securely pushed to your servers.config/deploy.yml
: Contains the main Kamal configuration settings.
Note: If you’re using Docker Hub, ensure the KAMAL_REGISTRY_PASSWORD
in your .env
file is an access token, not your actual password. For details, check out the Docker access tokens guide.
4.1. For Pre-rendered Static Sites (e.g., Netlify)
In config/deploy.yml
, configure your deployment settings as shown below:
<% require "dotenv"; Dotenv.load(".env") %>
# Name of your application. Used to uniquely configure containers.
service: my_awesome_app
# Name of the container image.
image: your-name/my_awesome_app
# Deploy to these servers.
servers:
web:
- 192.168.0.1
proxy:
app_port: 80 # Nginx exposes port 80 by default
ssl: true
host: my_awesome_app.com
healthcheck:
path: / # Default is /up, adjust if needed
# Credentials for your image host.
registry:
username: your-name
password:
- KAMAL_REGISTRY_PASSWORD # Access token pulled from .kamal/secrets
builder:
arch: amd64
4.2. Prepare your Dockerfile
Below is a sample Dockerfile
for a Nuxt project. Feel free to adjust based on your project’s needs:
FROM node:20-alpine AS build
WORKDIR /app
COPY . ./
RUN corepack enable
RUN yarn install --immutable
RUN yarn run generate
# Use Nginx to serve static files
FROM nginx:alpine-slim
COPY ./config/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY /app/docs/.output/public /usr/share/nginx/html
In this example, the default Nginx configuration is customized to add caching for assets, which can improve performance. For more information, see the Nginx Docker Hub page.
Custom Nginx Configuration
Below is the full default.conf
I use. You can adapt it to fit your project:
View the complete default.conf
file:
server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Suppress logs for common paths
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location /_nuxt/ { access_log off; log_not_found off; }
location ~* _payload\.json$ { access_log off; log_not_found off; }
# Cache static assets (images, CSS, JS, fonts)
location ~* \.(css|js|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
log_not_found off;
}
location ~* \.(svgz?|ttf|ttc|otf|eot|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
access_log off;
}
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}
With Kamal, the build, push, and pull processes for your Docker image are automated, requiring only your registry credentials.
⚠️ You won’t need to build and publish the image manually. Kamal takes care of it!
4.3. For SSR applications
With SSR, there’s no need to use Nginx. The Node application will handle serving all content and assets on its own.
In your config/deploy.yml
, configure the deployment settings like this:
<% require "dotenv"; Dotenv.load(".env") %>
# Name of your application. Used to uniquely configure containers.
service: my_awesome_app
# Name of the container image.
image: your-name/my_awesome_app
# Deploy to these servers.
servers:
web:
- 192.168.0.1
proxy:
app_port: 3000 # Nuxt defaults to exposing port 3000
ssl: true
host: my_awesome_app.com
healthcheck:
path: / # The default is /up, but customize if necessary
# Credentials for your image host.
registry:
username: your-name
password:
- KAMAL_REGISTRY_PASSWORD # Access token from .kamal/secrets
builder:
arch: amd64
4.4. Prepare your Dockerfile
Here’s an example Dockerfile
for an SSR Nuxt project. Adjust it to meet any specific requirements:
FROM node:20-alpine AS build
WORKDIR /app
COPY . ./
RUN corepack enable
RUN yarn install --immutable
RUN yarn run build
# Serve the app directly from Node
FROM build
# Copy the entire .output folder
COPY /app/docs/.output /app
EXPOSE 3000
CMD ["node", "/app/server/index.mjs"]
For further details, you can refer to the Nuxt deployment documentation.
7. First deployment
We're ready to deploy the app! But before the first deployment, let’s set up Kamal:
$ kamal setup
This command will handle several important tasks:
- Install Docker if it isn’t already on your server.
- Start and configure the kamal-proxy container.
- Load all environment variables from your
.kamal/secrets
file. - Build and push your Docker image to the registry.
- Launch a new container with the latest version of your app.
8. Workflow
With everything now running smoothly, here are some essential commands you’ll use frequently:
To deploy a new version of your app:
$ kamal deploy
To view your application logs:
$ kamal app logs
9. Conclusion
It's easy as that, enjoy your reliable and cheap deploy! 🚀