the cover image of blog post How to deploy the Next.js app to a VPS server

How to deploy the Next.js app to a VPS server

2025-01-14
9 min read

As mentioned in the previous post, I built a new blog post website with Next.js. Now I'd like to deploy it to a VPS server.

A few things come to my mind,

  • how to build the app
  • how to push the built package
  • how to boot the app in VPS
  • how to expose the app in VPS
  • automation

Let me walk you through one by one.

Build Next.js app

Basically following the Next.js self-hosting deployment docs, I went for a docker building as it provides cross-platform consistency; the only thing worth to mention here is that not forget to change the output in the next.config.js to standalone.

Push the docker image

docker build --platform linux/amd64 -t ghcr.io/xavierchow/xblog:0.0.1 .

After the building step above, now I have a docker image which can be run anywhere with docker environment. Here I use the Github package https://gchr.io as the docker registry.

Caveats:

  • You need to replace the tag name(ghcr.io/xavierchow/xblog), but the prefix ghcr.io has to be kept as part of your tag name.
  • As my VPS is linux amd64 but I'm on OSX for local development so I specify the platform parameter when run building, you probably need to change it according to your case.

Then I can push it to github registry, remember it needs to run docker login before docker push.

docker login docker push ghcr.io/xavierchow/xblog:0.0.1

We can see the package has been pushed to github as follows,

image

Fetch the pacakge and boot it

Now I can pull the image from my VPS server with

docker pull ghcr.io/xavierchow/xblog:0.0.1

and run it as follows,

docker run -p 3000:3000 -e MARKDOWN_FOLDER=/app/myposts/ \ -v /blog_posts:/app/myposts \ -v /blog_posts/images:/app/public/image ghcr.io/xavierchow/xblog:0.0.1

The environment variable MARKDOWN_FOLDER is used to mount the folder containing the markdonw files. Now the app is running with port 3000 on my server, to make it accessible via standard http protocol(i.e. port 80), I use nginx as the reverse proxy.

The following is the nginx config file app.conf, you can see I forward the /blog to the localhost 3000 port.

server { listen 80; listen [::]:80; location /blog { proxy_pass http://localhost:3000; proxy_set_header Host $host; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

Automation

Last but not least, the app is still under development and it gets updates from time to time, I need a pipeline to avoid manual commands like above steps. Github action is a good choice, I added a workflow here, so whenever I push a new git tag, it runs the building and pushes the image to the registry.

On the server side, I have two services need to take care, nginx and the blog app. So I use docker compose for an easier management.

services: nginx: build: ./nginx_home container_name: nginx ports: - "80:80" networks: - webapp restart: always blog: image: ghcr.io/xavierchow/xblog:0.0.1 container_name: xblog ports: - "3000:3000" environment: MARKDOWN_FOLDER: /app/myposts/ volumes: - /root/blog_posts/posts:/app/myposts - /root/blog_posts/images:/app/public/images networks: - webapp restart: always networks: webapp: null

Under the nginx_home directory, I have the nginx Dockerfile and the aforementioned app.conf.

FROM nginx COPY app.conf /etc/nginx/conf.d/app.conf

Bear in mind, as everything is inside docker, the 'localhost' in the app.conf needs to be replaced with the container name.

server { ... location /blog { proxy_pass http://xblog:3000; ... }

As you see the finally flow only needs 2 manual steps. image

Of couse, it could be further automated by other approaches like webhook so I don't need to update the docker-compose.yaml on server and re-run the docker compose but I think it's good for now.

© 2025 Xavier Zhou