How to Publish a Server with Docker and GitHub Actions

A step-by-step guide to automating your server deployment pipeline using Docker containers and GitHub Actions CI/CD.

devops
ServerDockerGitHub

How to Publish a Server with Docker and GitHub Actions

Deploying a server used to mean manually SSHing into a machine, pulling code, and restarting processes. Today, with Docker and GitHub Actions, you can fully automate this workflow — from a git push to a running container.

Prerequisites

Before we start, make sure you have:

  • A server (VPS, Raspberry Pi, or any Linux machine)
  • Docker installed on the server
  • A GitHub repository with your application code
  • SSH access to your server

Step 1: Dockerize Your Application

First, create a Dockerfile at the root of your project:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]

This uses a multi-stage build to keep the final image small — only the production artifacts are included in the runner stage.

Step 2: Set Up Docker Compose on Your Server

On your server, create a docker-compose.yml file:

version: "3.8"
services:
  app:
    image: ghcr.io/your-username/your-repo:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=${DATABASE_URL}

The restart: unless-stopped policy ensures your container automatically recovers after a server reboot.

Step 3: Configure GitHub Secrets

In your GitHub repository, go to Settings → Secrets and variables → Actions and add:

  • SERVER_HOST — your server's IP or domain
  • SERVER_USER — SSH username (e.g., ubuntu)
  • SERVER_SSH_KEY — your private SSH key (the one whose public key is on the server)

Step 4: Create the GitHub Actions Workflow

Create .github/workflows/deploy.yml:

name: Deploy to Server
 
on:
  push:
    branches:
      - main
 
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
 
      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
 
  deploy:
    runs-on: ubuntu-latest
    needs: build-and-push
 
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/your-app
            docker compose pull
            docker compose up -d

The workflow is split into two jobs:

  1. build-and-push — builds the Docker image and pushes it to GitHub Container Registry (GHCR)
  2. deploy — SSHes into your server, pulls the new image, and restarts the container

Step 5: First-Time Setup on the Server

Before the first deployment, log into your server and pull the image manually:

docker login ghcr.io -u your-username --password-stdin
docker compose pull
docker compose up -d

After that, every push to main will automatically build, push, and deploy your application.

Key Takeaways

  • Multi-stage Docker builds reduce image size significantly
  • GitHub Container Registry is free for public repositories and integrated with GitHub Actions
  • The appleboy/ssh-action handles SSH deployment cleanly without needing a self-hosted runner
  • Secrets management keeps credentials out of your codebase
  • With docker compose pull && docker compose up -d, zero-downtime-ish updates are straightforward for simple apps

This pattern scales well from a hobby project to a production service — just add health checks, rollback logic, and notifications as your needs grow.