How to Deploy Any App with a Dockerfile
Chajio Cloud recognises Node.js, Python and many other stacks on its own. When yours isn’t one of them, or you want full control over how your app is built, add a Dockerfile to your repository and the platform builds exactly what it describes. This guide covers the rules a Dockerfile needs to follow here, with complete examples for Go and Laravel.
On this page
When to use a Dockerfile
- Your language or framework isn’t detected automatically: Go, Rust, .NET, Java, PHP frameworks and more.
- Your app needs system packages, such as image or PDF tools, installed alongside it.
- You already build a Docker image for your app, and want production to use the same one.
There is no setting to change. If your repository has a file named Dockerfile in its root, or in the folder you set as the build context, the platform builds from it instead of detecting your setup. Everything else in the deploy works the same, as described in the guides for Node.js and Python.
Five rules for a Dockerfile that deploys cleanly
1. Listen on PORT, on every interface
When your app runs, the PORT environment variable holds the port it must listen on, matching the Container port setting (3000 unless you change it). Listen on 0.0.0.0, not localhost, or your app starts but can’t be reached. If your server can’t read PORT, set Container port to the port it uses instead. EXPOSE lines are fine to keep, but they don’t change anything here.
2. Answer the health check
New versions only receive visitors once the Health check path (/ by default) answers with a success response. Point it at a route that answers quickly without a login, such as /health.
3. Declare build-time variables with ARG
Every variable on your Environment tab reaches your app when it runs. To use one while the image is built, for example a public API address compiled into a frontend, declare it with an ARG line. Each ARG receives the variable with the same name, and the RUN commands after it can read it. Variables you don’t declare never reach the build.
Keep secrets out of ARG
Values used during the build are recorded in the image. Secrets such as database passwords or API keys don’t need an ARG: read them when your app runs, which keeps them out of the image entirely. The same goes for .env files: don’t copy one into the image.
4. Run one process in the foreground
Start your server with an exec-form CMD, such as CMD ["./server"], and keep it in the foreground. When you deploy, the old version gets a SIGTERM signal and a short time to finish the requests it’s handling; exec form makes sure that signal reaches your app rather than a shell. If you need $PORT in the command line itself, use CMD ["sh", "-c", "exec your-server --port $PORT"].
5. Keep the image lean
Build in one stage and copy only the result into a small final stage, as in the examples below. Smaller images build and roll out faster. Add a .dockerignore so local clutter never reaches the build:
.git
node_modules
.env
*.logRunning as a non-root user, with a USER line, is a good habit too.
Example: a Go web server
package main
import (
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Chajio Cloud"))
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
log.Printf("listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server .
FROM alpine:3.20
RUN adduser -D app
USER app
COPY --from=build /server /server
CMD ["/server"]":" + port listens on every interface. Set the health check path to /health. The final image holds a single binary, so it’s a few megabytes.
Example: Laravel
This uses the official PHP image with Apache, which reads its port from PORT when it starts:
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction
FROM php:8.3-apache
RUN docker-php-ext-install pdo_mysql \
&& a2enmod rewrite \
&& sed -i 's/Listen 80/Listen ${PORT}/' /etc/apache2/ports.conf \
&& sed -i 's/<VirtualHost \*:80>/<VirtualHost *:${PORT}>/' /etc/apache2/sites-available/000-default.conf \
&& sed -i 's#/var/www/html#/var/www/html/public#' /etc/apache2/sites-available/000-default.conf \
&& sed -i 's/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
ENV PORT=3000
WORKDIR /var/www/html
COPY . .
COPY --from=vendor /app/vendor ./vendor
RUN chown -R www-data:www-data storage bootstrap/cacheThe last sed lets Laravel’s .htaccess route every URL through index.php. If your app builds its CSS and JavaScript with Vite, add a Node.js stage that runs npm ci && npm run build and copy its public/build folder into the final image.
Then, on the dashboard:
- Set the Health check path to
/up, the health route built into Laravel 11 and later. - Add
APP_KEY(fromphp artisan key:generate --show),APP_ENV=productionandAPP_URLon the Environment tab. - For a database, create a MariaDB database and name its app variable
DB_URL, which Laravel reads directly, and setDB_CONNECTION=mariadb. - So that Laravel builds
https://links, trust the platform’s proxy inbootstrap/app.php:->withMiddleware(fn ($middleware) => $middleware->trustProxies(at: '*')).
Uploaded files written to storage/ aren’t shared between the two copies of your app, and disappear on the next deploy. Store uploads on an external disk, such as S3-compatible object storage.
Test it on your computer first
Building and running your image locally catches most problems in seconds instead of minutes. With Docker installed:
docker build -t myapp .
docker run --rm -e PORT=3000 -p 3000:3000 myapp
# in another terminal
curl http://localhost:3000/healthIf that answers, it will answer on Chajio Cloud too. Pass build-time variables locally with --build-arg NAME=value.
Common problems
The build log warns about a “missing build argument”
Your Dockerfile declares an ARG that has no variable of the same name on the Environment tab, so it’s empty during the build. Add the variable, then redeploy. The build log lists the variables it received and any it asked for but didn’t get.
The app “did not become healthy”
Check the Logs tab. Usually the server listens on localhost or a fixed port, or the health check path returns an error. Test with the local docker run above.
COPY fails with “not found”
Paths in a Dockerfile are relative to the build context. If your app is in a subfolder, set Build context to that folder and keep the Dockerfile inside it. Also check the file isn’t excluded by .dockerignore or .gitignore: only files in your repository are available.
Learning path
Deploy your first app
- 1
Deploy your app
- 2
- 3
- 4
Keep reading
Deployment Failed? How to Find and Fix the Cause
Every failed deployment tells you which stage broke and why. How to read it, and the fixes for the failures we see most: missing start commands, port mix-ups, health checks and memory.
How to Add a Database to Your App
Create a managed PostgreSQL or MariaDB database next to your app and have its connection details handed to your app automatically, with examples for Node.js, Python and Django.
How to Connect Your Domain to Your App (with Free HTTPS)
Put your app on your own domain in three DNS steps. Covers www versus root domains, what each DNS record does, Cloudflare, and what to check when a domain will not verify.
How to Deploy a Node.js App (Express, Fastify or NestJS)
Deploy a Node.js API or server from GitHub, JavaScript or TypeScript. The two lines of code every app needs, choosing a Node version, and shutting down cleanly for seamless updates.
Ready to deploy?
Start on the Free plan: your first month is on us, and every app runs two copies for high availability.