We all love Docker, and if you donβt, you should.
Docker is a great tool that allows us to create containers with our applications and run them in any environment. But what if we want to deploy our containers in a VPS? In this post, I will show you how to deploy your Docker containers in a VPS using GitHub Actions.
We are going to build a simple workflow that will allow us to deploy our custom images to GitHub Container Registry, pull them from our VPS and run them.
Essentially, this will be just like having a simple CI/CD pipeline.
Figure 1: Workflow diagram example of the CI/CD pipeline
Before we start, you need to have the following:
You can either create a new repo or use an existing one. For this example, I will use a simple Node.js app.
We will use the Hono framework. You can install it by running:
npm install hono @hono/node-server
// index.js
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))
console.log("Server started!")
serve(app)
// Response from http://localhost:3000/
$> node index.js
Hello Node.js!
Here is the folder structure so far:
.
βββ node_modules/
βββ index.js
βββ package-lock.json
βββ package.json
In the root of your project, create a new file called Dockerfile
and add the following content:
FROM node:18-alpine
WORKDIR /app
COPY package.json .
COPY index.js .
RUN npm install
CMD ["node", "index.js"]
If you are using an M1 Mac or similar you will need to add the
--platform linux/amd64
flag to the Dockerfile
FROM --platform linux/amd64 node:18-alpine
...
This file will create a new image based on the node:18-alpine
image, install the dependencies of your app and then run it.
Now we can build the image by running:
docker build -t vps_tutorial .
And run it with:
docker run -d -p 3000:3000 vps_tutorial
If we go to http://localhost:3000/
we should see the message Hello Node.js!
. Awesome!
Figure 2: Server working as expected
We need to get some secrets to make this work. Iβll explain it in a bit
GH_SECRET
SSH_USER
SSH_HOST
SSH_PRIVATE_KEY
WORK_DIR
Now we need to get a GitHub API key. This will allow the GitHub Action Workflow to push our image to the Registry.
On GitHub, go to Settings
-> Developer settings
-> Personal access tokens
and click on Generate new token
. Select the write:packages
scope and click on Generate token
.
Hereβs a shortcut to the page:
https://github.com/settings/tokens/new?scopes=write:packages,read:packages,delete:packages
I will generate a Classic Token, this will be our GH_SECRET
.
For the next step, we need to get the SSH private key of our VPS. You will need to create a new user for the GitHub actions if you donβt have one already. You can use whatever user you like, of course.
The user will be our SSH_USER
.
The IP address of your VPS will be our SSH_HOST
.
The directory where you want to deploy your app will be our WORK_DIR
.
Log into your VPS. Then, generate one by running:
ssh-keygen -t rsa -b 4096
Copy the content of the public key and add it to the ~/.ssh/authorized_keys
file of the user you want to use.
cat <path/to/public/key> >> ~/.ssh/authorized_keys
Now, copy the content of the private key to a new file.
cat <path/to/private/key>
This will be our SSH_PRIVATE_KEY
.
Note that the public key will be the one with the
.pub
extension. The private key will be the one without it.
Now that we have all the secrets, we can set the secrets in our GitHub repository.
Go to the repo Security
-> Secrets and Variables
-> Actions and click on New repository secret
. Add the secrets with the names mentioned above.
In your GitHub repository, create a new folder called .github/workflows
and add a new file called docker-publish.yml
.
name: publish
on:
push:
branches: ['main']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.actor }}/<image-name>:latest # Change <image-name> to your image name
jobs:
publish:
name: publish image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: login
run: |
echo ${{ secrets.GH_SECRET }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and Publish
run: |
docker build . --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
deploy:
needs: publish
name: deploy image
runs-on: ubuntu-latest
steps:
- name: install ssh keys
run: |
install -m 600 -D /dev/null ~/.ssh/id_rsa
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} > ~/.ssh/known_hosts
- name: connect and pull
run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd ${{ secrets.WORK_DIR }} && docker compose pull && docker compose up -d && exit"
- name: cleanup
run: rm -rf ~/.ssh
The workflow will trigger on every push to the master
branch. It will build the image, push it to the Registry, and then log into the VPS, pull the image and run it.
We are almost ready, but first, we need to generate the image and push it to the GitHub Container Registry so the workflow can pull it.
GH_SECRET
variable in your terminal:export GH_SECRET=<GH_SECRET>
echo $GH_SECRET | docker login ghcr.io -u <username> --password-stdin
docker build . -t ghcr.io/<username>/<image-name>:latest && docker push ghcr.io/<username>/<image-name>:latest
The image will be pushed to the GitHub Container Registry.
In your server, you will need to repeat the steps 1 and 2 so the server can pull the image.
Create a new file called docker-compose.yml
in the same route as the WORK_DIR
:
version: '3.7'
services: # Add more services if needed
<name>:
container_name: <name> # Change this to your container name
image: ghcr.io/{username}/<image>:latest
ports:
- 3000:3000
After creating the file, build the container by running:
docker compose up -d
Congratulations! You have successfully deployed your Docker container to your VPS using GitHub Actions!
Now every time you push to the main
branch, the workflow will trigger and deploy the new image to your VPS.
Here is a link to the full repository if you want to check it out: