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.
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.
| Approach | Pros | Cons |
|---|---|---|
| Manual Nginx | Zero overhead, simple | Manual SSL (Certbot), no UI, no zero-downtime deploys |
| Manual Caddy | Auto SSL, easy config | Still manual VPS file editing, no centralized app monitoring |
| Dockerfile in Repo | Standard containerization | Clutters the repository; frontend code is now container-aware |
| Docker Compose on Dokploy | Good orchestration | Usually requires compose files in the repo or building inside Dokploy |
| Hybrid CI + Dokploy | Repo stays clean; CI builds, Dokploy hosts; full UI, SSL, and rollbacks | Requires 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:
- GitLab CI builds the site and produces the static files.
- A later CI job dynamically creates a
Dockerfile, builds a minimal image, and pushes it to the GitLab Container Registry. - 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]: Thedockerizejob reuses thepublic/artifact produced by thepagesjob. 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
latesttag and a unique SHA tag. Dokploy useslatestfor 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_registryscope
- Registry URL:
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
latestimage.
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:
- Use a GitHub Actions workflow to build your site.
- In a second job, log in to the GitHub Container Registry (ghcr.io) using
docker/login-action. - Dynamically generate a
Dockerfileand push the image toghcr.io/your-org/your-repo:latest. - In Dokploy, register a Docker Hub / GitHub registry using a GitHub Personal Access Token with
read:packagesscope. - 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.