Yobotics Logo
devops static-sites dokploy gitlab-ci deployment

Deploying Static Sites to Dokploy Without a Dockerfile

Keeping your static site repository clean while enjoying the power of container orchestration, automatic SSL, and zero-downtime deployments.

Yongkie Wiyogo

Deploying Static Sites to Dokploy Without a Dockerfile

From Nginx to Caddy to Dokploy

When I started self-hosting web applications, my first stack was straightforward: a VPS running Nginx with Certbot for SSL. It worked, but every new site meant writing server blocks, managing certificates manually, and praying that the renewal cron job never broke.

I later moved to Caddy, which eliminated the manual SSL headache with its automatic HTTPS. That was a significant upgrade. Yet, I still found myself logging into the VPS, editing text files, and tracking which services ran on which ports. I missed a unified dashboard where I could see all my applications, their statuses, and their deploy history.

That led me to Dokploy. Dokploy gives me the container orchestration and automatic SSL of a modern platform, but running on my own VPS. The missing piece was deciding how to get my static sites—built by a CI pipeline—into Dokploy without cluttering my repositories with Docker files.

The Problem With Dockerizing a Static Repository

Static site generators like Gridsome, Hugo, Jekyll, and VuePress produce a clean output folder (usually public/ or dist/). In my workflow, GitLab CI already handles the build. The repository itself contains markdown, components, and configuration. It is a frontend project, not an infrastructure project.

Adding a Dockerfile or docker-compose.yml feels like introducing a dependency that does not belong there. It forces every developer on the project to reason about containerization just to understand the codebase. If the CI already produces the static files, the repository should stay clean.

Comparing Deployment Approaches

There are several ways to get a static site onto a VPS. Here is how they compare for a developer who wants modern hosting conveniences without repository clutter.

ApproachProsCons
Manual NginxZero overhead, simpleManual SSL (Certbot), no UI, no zero-downtime deploys
Manual CaddyAuto SSL, easy configStill manual VPS file editing, no centralized app monitoring
Dockerfile in RepoStandard containerizationClutters the repository; frontend code is now container-aware
Docker Compose on DokployGood orchestrationUsually requires compose files in the repo or building inside Dokploy
Hybrid CI + DokployRepo stays clean; CI builds, Dokploy hosts; full UI, SSL, and rollbacksRequires CI runner with Docker-in-Docker and registry credentials

The hybrid approach is the sweet spot. It separates building (CI responsibility) from hosting (platform responsibility).

The Solution: Hybrid CI/C

The idea is simple:

  1. GitLab CI builds the site and produces the static files.
  2. A later CI job dynamically creates a Dockerfile, builds a minimal image, and pushes it to the GitLab Container Registry.
  3. Dokploy on the VPS pulls that image and serves it via Traefik, handling domains and SSL automatically.

The repository itself contains zero Docker files. The CI pipeline generates them on the fly and discards them afterward.

GitLab CI Implementation

Here is the .gitlab-ci.yml I use for a Gridsome project. It builds the site in one stage and packages it into an nginx:alpine image in the next.

Because this project relies on an older version of Webpack, I include the NODE_OPTIONS=--openssl-legacy-provider flag, which is a common requirement when running legacy Node.js builds on modern OpenSSL versions.

stages:
  - build
  - deploy

image: node:19.4.0

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"

# Build the static site and export for GitLab Pages
pages:
  stage: build
  cache:
    paths:
      - node_modules/
  script:
    - npm install
    - export NODE_OPTIONS=--openssl-legacy-provider
    - npm run build
  artifacts:
    paths:
      - public
  only:
    - master

# Package the built site into a Docker image for Dokploy
dockerize:
  stage: deploy
  image: docker:cli
  services:
    - docker:dind
  dependencies:
    - pages
  script:
    # Login to GitLab Container Registry
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

    # Create a minimal .dockerignore to speed up the build context
    - |
      cat > .dockerignore << 'DOC_IGNORE'
      *
      !public
      DOC_IGNORE

    # Dynamically generate a Dockerfile (repository stays Docker-free)
    - |
      cat > Dockerfile << 'DOC_FILE'
      FROM nginx:alpine
      COPY public /usr/share/nginx/html
      EXPOSE 80
      DOC_FILE

    # Build and tag image for GitLab Container Registry
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest
  only:
    - master

Key details to notice:

  • dependencies: [pages]: The dockerize job reuses the public/ artifact produced by the pages job. No rebuild is needed.
  • Dynamic Dockerfile: We write the file to disk during the CI job. It is never committed.
  • Image Tagging: We push both a latest tag and a unique SHA tag. Dokploy uses latest for continuous deployment. The SHA tag is your safety net for instant rollbacks.

Dokploy Configuration

Once the CI pipeline is running, configure Dokploy to pull the image.

1. Register the GitLab Container Registry

  • In Dokploy, go to Registry → Add Registry.
  • Select the GitLab type.
  • Enter:
    • Registry URL: https://registry.gitlab.com
    • Username: Your GitLab username
    • Password: A GitLab Personal Access Token with read_registry scope

2. Create the Application

  • In a Dokploy project, create a new Application.
  • Go to the General tab and select Docker.
  • For the image, enter the full path:
    registry.gitlab.com/your-username/your-repo:latest
  • Select the GitLab registry you configured in Step 1.
  • In the Domains tab, add your domain (e.g., wiyogo.com).
  • Enable HTTPS. Traefik provisions and renews the Let’s Encrypt certificate automatically.
  • Enable Auto-Deploy so Dokploy redeploys the container whenever it detects a new latest image.

3. First Deploy

Hit Deploy. Dokploy pulls the image and starts the container. Within seconds, your domain is live with SSL enabled.

For GitHub Users

The same architecture applies perfectly to GitHub Actions. The only differences are syntax and the registry:

  1. Use a GitHub Actions workflow to build your site.
  2. In a second job, log in to the GitHub Container Registry (ghcr.io) using docker/login-action.
  3. Dynamically generate a Dockerfile and push the image to ghcr.io/your-org/your-repo:latest.
  4. In Dokploy, register a Docker Hub / GitHub registry using a GitHub Personal Access Token with read:packages scope.
  5. Point your Dokploy application to ghcr.io/your-org/your-repo:latest.

The logic remains identical: CI builds, CI packages, Dokploy hosts.

Conclusion

Dokploy bridges the gap between the simplicity of static hosting and the power of container orchestration. By generating the Dockerfile inside the CI pipeline, you keep your repository focused on what it does best—content and frontend logic—while your platform handles SSL, routing, and zero-downtime deployments.

If you are currently managing static sites with Nginx or Caddy and looking for a better way to monitor, deploy, and scale, this hybrid approach is worth considering.