Deploy SvelteKit App in Docker Container [Behind nginx reverse proxy]
We all use Docker everyday to run our applications in containers. It's a great tool that helps us to package our applications and run them in an isolated environment. In this post, I'll share how I deploy my SvelteKit apps in a Docker container behind the nginx reverse proxy.
Prerequisites
Before we start, make sure you have the following installed on your machine:
- Node.js - Version 20 or higher
- Docker with Docker compose plugin
- If you wish to deploy the app in production environment, you need a VPS server with root access and public IP address attached to it.
If you do not have the VPS server yet, you can use the DigitalOcean or Linode to create a VPS server. Both the providers offer free credit to try their services.
I'll assume that you already have a SvelteKit app ready to deploy. If you don't have one, you can create a new SvelteKit app as described by the SvelteKit documentation.
Following is the script section of the package.json
file of the SvelteKit app:
{
"name": "app.domain.com",
"description": "A demo SvelteKit app",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "ORIGIN=https://app.domain.com PORT=4800 NODE_ENV=production node --require dotenv/config build",
"dev": "vite dev --mode development",
"build": "vite build",
"preview": "cross-env NODE_ENV=production vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint ./src/"
}
}
1. Install Node Adapter for SvelteKit
To deploy the SvelteKit app in a Node environment, we need to install the @sveltejs/adapter-node
package. Install the required dev dependency by running the following command:
npm install --save-dev @sveltejs/adapter-node
@sveltejs/adapter-node
generates an app that can be run in a Node environment. For more information about the adapter, you can check the official documentation.
Once the package is installed your package.json
file should look like this:
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.0",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@types/node": "^20.10.8",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"tailwindcss": "^3.4.17",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"vite": "^5.1.8",
}
2. SvelteKit Configuration
While creating the SvelteKit app using the npx sv
command adapter auto is installed by default. SvelteKit app inside the Docker container runs in a Node environment. Before creating the production build of the app, we need to update the svelte.config.js
file to set the kit.adapter
to @sveltejs/adapter-node
explicitly.
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/**
* SvelteKit configuration
*
* @ref https://svelte.dev/docs/kit/configuration
* @type {import('@sveltejs/kit').Config}
* @since 1.0.0
*/
const config = {
extensions: ['.svelte', '.md'],
preprocess: [vitePreprocess()],
kit: {
adapter: adapter({ out: 'build' }),
env: {
dir: "./"
},
alias: {
$src: "./src",
}
},
};
export default config;
The is the only change required in the SvelteKit app config to generate the app that can be deployed later using the Docker container.
3. Create Dockerfile
Once the node adapter is installed and the SvelteKit app is configured, create a Dockefile
in the root of the project. The Dockerfile
contains the instructions to build the Docker image.
FROM oven/bun:alpine AS builder
WORKDIR /tmp/app
COPY bun.* ./
COPY package*.json ./
RUN bun install
COPY . .
FROM node:20-alpine AS runners
WORKDIR /app
COPY --from=builder /tmp/app/build ./build
COPY --from=builder /tmp/app/package*.json ./
COPY --from=builder /tmp/app/node_modules ./node_modules
ENV PORT=4800
ENV NODE_ENV=production
# Expose the port the app runs on
# The app runs on http://localhost:4800
EXPOSE 4800
# Server the app.
ENTRYPOINT ["npm", "start"]
I am using Docker multi-stage build to create the Docker image. The first stage is the builder
stage where the SvelteKit app is built using the Bun JS
. The second stage is the runners
stage where the app is served using the npm start
command using the Node environment.
Note: You can use Node JS in both the stages to build and serve the app. If you wish to run the app in a different port, you can update the port environment variable in the Dockerfile and expose the port accordingly.
4. Docker Ignore File
Create a .dockerignore
file in the root of the project to ignore the files that are not required in the Docker image.
/docs
/node_modules
/invoices
Docker ignore file helps to reduce the size of the Docker image by ignoring the files that are not required in the image.
5. Create a Docker Network
This step is optional. If you are running multiple services in Docker and want to communicate between them, you can create a Docker network. To create a Docker network use the following command:
docker network create sveltekit-app
If you don't want to create the network, you can remove the networks
section from the docker-compose.yml
file later.
6. Docker Compose File
Create a docker-compose.yml
file in the root of the project to run the Docker container. The docker-compose.yml
file contains the configuration to run the Docker container.
services:
sveltekit-app:
image: sveltekit-app:latest
container_name: sveltekit-app
ports:
- "4800:4800"
restart: always
volumes:
- ./data:/app
networks:
- sveltekit-app
env_file:
- .env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
sveltekit-app-2:
image: sveltekit-app:latest
container_name: sveltekit-app-2
ports:
- "4801:4800"
restart: always
volumes:
- ./data:/app
networks:
- sveltekit-app
env_file:
- .env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
data:
networks:
sveltekit-app:
external: true
The network section is optional. If you have created a network in the previous step, you can use it here. If you don't want to use the network, you can remove the networks
section from the docker-compose.yml
file. The logging option in the docker-compose.yml
file is optional. You can remove it if you don't want to limit the log size of the container.
In the above docker-compose.yml
file, I have created two services sveltekit-app
and sveltekit-app-2
. Both the services run the same SvelteKit app but on different ports. You can run multiple instances of the same app using the same Docker image and perform the load balancing using the nginx reverse proxy. I'll later guide you how you can set up the nginx reverse proxy to load balance the requests between the multiple instances of the SvelteKit app.
Note: If you are copying the docker-compose.yml
file from this post, make sure to fix the indentation of the file. The indentation is important in the docker-compose.yml
file.
7. Build the Image and Run the Docker Container
Once the Dockerfile
and docker-compose.yml
files are created in the root of the project, build the Docker image using the following command:
docker build -t sveltekit-app .
If everything goes well, the Docker image for the SvelteKit app will be created using the multi-step build steps defined in a Dockerfile.
Now, run the following command to check if the Docker image was created successfully:
docker images
The output of the above command should show the sveltekit-app
image in the list of Docker images.
7.1 Run the Docker Container
Running the container is simple. Run the following command to run the Docker container:
docker-compose up -d
The above command will use the docker-compose.yml
file to run the Docker container. The -d
flag is used to run the container in the detached mode. The container will run in the background.
To check if the container is running, run the following command:
docker ps
The output of the above command should show the running container in the list of Docker containers. If the app crashed you can view the logs using the following command:
docker logs sveltekit-app
If the app is running in the Docker container, you can access the app in the browser using the following URL:
http://localhost:4800
Note: Port 4800 was defined in the start
command of the package.json
file. If you have changed the port in the package.json
file, you can access the app accordingly.
If you wish to access the app in your public IP address you might need to allow the port in the firewall settings of your server.
8. Configure nginx
I'll assume you have a basic understanding of the nginx web server. If you do not have nginx installed on your server, now it's time to install it. I'll skip the installation part of nginx in this post.
In this step we need to do few things:
- Setup DNS A record to point the domain name to the server IP address
- Have nginx web server installed on the server. You can verify the status of ngins by running the following command:
nginx -v
If the above command returns the nginx version, it means nginx is installed on the server.
8.1 Setup DNS A Record
Before configuring the nginx reverse proxy, make sure you have a domain name pointing to your server IP address. Create an A
record in your DNS provider to point the domain name to your server IP address.
8.2 Create a nginx Configuration File
Create a new configuration file in the /etc/nginx/sites-available/
directory. The name of the configuration file should be app.domain.com
. The configuration file contains the configuration to load balance the requests between the multiple instances of the SvelteKit app.
sudo nano /etc/nginx/sites-available/app.domain.com
At this point, you might wish to install the SSL certificate for the domain name. I'll assume you are aware of certbot and how to install the SSL certificate for the domain name in nginx server.
Once the certificate is installed, add the following configuration to the nginx configuration file:
# Define the upstream load-balancing group
upstream sveltekit-app {
# Docker container instances
server 127.0.0.1:4800;
server 127.0.0.1:4801;
}
# Redirect all HTTP traffic to HTTPS
server {
listen 80;
server_name app.domain.com;
return 301 https://$host$request_uri;
}
# Main HTTPS server block
server {
listen 443 ssl;
server_name app.domain.com;
# SSL Configuration
ssl_certificate /www/certs/app.domain.com/fullchain.pem; # Update this path to your SSL certificate.
ssl_certificate_key /www/certs/app.domain.com/privkey.pem; # Update this path to your SSL certificate key.
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
error_page 497 https://$host$request_uri;
# Proxy settings for load balancing
location / {
proxy_pass http://sveltekit-app;
# Load balancing configurations
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Prefix /;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
}
# Directory verification settings for SSL certificate application
location ~ \.well-known {
allow all;
}
# Prohibit sensitive files in verification directory
if ($uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$") {
return 403;
}
# Logs
access_log /www/logs/app.domain.com.log; # Update this path to your access log file.
error_log /www/logs/app.domain.com.error.log; # Update this path to your error log file.
}
In the above configuration, I have defined the upstream load-balancing group sveltekit-app
that contains the two Docker container instances running the SvelteKit app. The proxy_pass
directive is used to load balance the requests between the two instances of the SvelteKit app. The upstream group can contain multiple instances of the SvelteKit app. You can add more instances to the upstream group to scale the app horizontally.
Double check the port numbers in the upstream
group and the proxy_pass
directive. The port numbers should match the port numbers defined in the docker-compose.yml
file. To verify you can always check the running Docker containers using the docker ps
command.
8.2.1 nginx Configuration with no SSL Certificate
If you do not have the SSL certificate installed for the domain name, or you do not wish to use the SSL certificate, you can use the following configuration in the nginx configuration file:
# Define the upstream load-balancing group
upstream sveltekit-app {
# Docker container instances
server 127.0.0.1:4800;
server 127.0.0.1:4801;
}
# Main HTTP server block
server {
listen 80;
server_name app.domain.com;
# Proxy settings for load balancing
location / {
proxy_pass http://sveltekit-app;
# Load balancing configurations
proxy_redirect off;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Prefix /;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
}
# Directory verification settings (if needed for other purposes)
location ~ \.well-known {
allow all;
}
# Prohibit sensitive files in verification directory
if ($uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$") {
return 403;
}
# Logs
access_log /www/logs/app.domain.com.log; # Update this path to your access log file
error_log /www/logs/app.domain.com.error.log; # Update this path to your error log file
}
In the above configuration, I have removed the SSL configuration and added the main HTTP server block to load balance the requests between the multiple instances of the SvelteKit app. In this case, nginx server listens on port 80 and forwards the requests to the upstream group.
8.3 Enable the nginx Configuration
Once the configuration file is created, enable the configuration file by creating a symbolic link in the /etc/nginx/sites-enabled
directory. Run the following command to create the symbolic link:
sudo ln -s /etc/nginx/sites-available/app.domain.com /etc/nginx/sites-enabled/app.domain.com
8.4 Verify the nginx Configuration
Before restarting the nginx server, verify the configuration file using the following command:
sudo nginx -t
If the configuration file is correct, you should see the following output:
nginx: configuration file /etc/nginx/nginx.conf test is successful
8.5 Restart nginx Server
Once the configuration file is verified, restart the nginx server using the following command:
sudo systemctl restart nginx
Verify the status of the nginx server using the following command:
sudo systemctl status nginx
If the nginx server is running, you can access the SvelteKit app using the domain name https://app.domain.com
in the browser.
Conclusion
In this post, I shared how I deploy my SvelteKit apps in a Docker container behind the nginx reverse proxy. I hope you find this post helpful. The version of Svelte framework used in this post is 4.2.19.
I am aware that Svelte recently released version 5 with some breaking changes. I haven't tested the Svelte 5 yet but I am confident that the steps mentioned in this post will work with the Svelte 5 version as well.
If you found this post helpful, please share it with others. If you have any questions or feedback, feel free to drop me a line.